From faac3a1cf48526a6f264cab3e5f18245bf8091f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 08:34:23 +0000 Subject: [PATCH 1/3] Initial plan From 42ec0bfcdc9156d9ca090edbe1ca965e417906d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 08:53:55 +0000 Subject: [PATCH 2/3] feat: add OAuth2 authorization page with approve/decline flow - Regenerate API client to include OAuth types from swagger - Add internal data model for OAuth client metadata (OAuthClient.ts) - Add useOAuthClient hook to fetch client metadata - Add useOAuthAuthorize hook for authorization mutation - Add OAuthAuthorizePage component with approve/decline UI - Add /oauth/authorize route under _auth layout (requires auth) - Add i18n translations for OAuth scopes (de, en, es, fr, it) - Add 28 tests (data mapping, route validation, component behavior) --- public/partner-products.openapi.json | 524 +++++++- src/client/@tanstack/react-query.gen.ts | 442 ++++++- src/client/index.ts | 4 +- src/client/sdk.gen.ts | 369 +++++- src/client/types.gen.ts | 1169 ++++++++++++++--- src/components/oauth/OAuthAuthorizePage.tsx | 211 +++ .../__tests__/OAuthAuthorizePage.test.tsx | 316 +++++ .../__tests__/OAuthAuthorizeRoute.test.ts | 115 ++ src/data/internal/oauth/OAuthClient.ts | 23 + .../oauth/__tests__/OAuthClient.test.ts | 72 + src/hooks/oauth/useOAuthAuthorize.ts | 51 + src/hooks/oauth/useOAuthClient.ts | 27 + src/i18n/locales/de/translation.json | 26 + src/i18n/locales/en/translation.json | 26 + src/i18n/locales/es/translation.json | 26 + src/i18n/locales/fr/translation.json | 26 + src/i18n/locales/it/translation.json | 26 + src/routeTree.gen.ts | 21 + src/routes/_auth.oauth.authorize.tsx | 27 + 19 files changed, 3128 insertions(+), 373 deletions(-) create mode 100644 src/components/oauth/OAuthAuthorizePage.tsx create mode 100644 src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx create mode 100644 src/components/oauth/__tests__/OAuthAuthorizeRoute.test.ts create mode 100644 src/data/internal/oauth/OAuthClient.ts create mode 100644 src/data/internal/oauth/__tests__/OAuthClient.test.ts create mode 100644 src/hooks/oauth/useOAuthAuthorize.ts create mode 100644 src/hooks/oauth/useOAuthClient.ts create mode 100644 src/routes/_auth.oauth.authorize.tsx diff --git a/public/partner-products.openapi.json b/public/partner-products.openapi.json index f76ceb58..e18403b2 100644 --- a/public/partner-products.openapi.json +++ b/public/partner-products.openapi.json @@ -23,7 +23,7 @@ "/api/v1/shops/{shopId}/products": { "put": { "summary": "Batch upsert products (Partner API)", - "description": "Creates new products or updates existing ones for a shop in a single batch call,\nusing API key authentication. This endpoint is intended for partner shops \u2014 shops\nthat have been granted partner status and have an API key configured. It does **not**\nuse Cognito JWT authentication.\n\nThe request body is an array of `PutProductData` objects. Each entry is forwarded\nindividually to the asynchronous partner-product ingestion queue as an upsert command.\nWhen the queued command is later ingested:\n- **Existing product** \u2014 only `state` and `price` are updated (other fields are ignored).\n- **New product** \u2014 a full product is created using all provided fields.\n\nThe response returns HTTP 202 with an array containing only the `shopsProductId` values\nthat failed to be forwarded to the queue. An empty array indicates that all upserts were\naccepted for asynchronous processing.\n", + "description": "Creates new products or updates existing ones for a shop in a single batch call,\nusing bearer authentication. This endpoint is intended for partner shops and accepts:\n- a Cognito bearer token for the partner user linked to the shop, or\n- an Aura Historia access token owned by that partner user.\n\nAura Historia access tokens on this endpoint must include the `products:write` scope.\n\nThe request body is an array of `PutProductData` objects. Each entry is forwarded\nindividually to the asynchronous partner-product ingestion queue as an upsert command.\nWhen the queued command is later ingested:\n- **New product** \u2014 a full product is created using all provided fields. Omitting\n `title`, `url`, or `state` causes the backend to fall back to an empty title,\n a placeholder URL, and `LISTED` respectively.\n- **Existing product** \u2014 the backend applies `price`, `priceEstimateMin`,\n `priceEstimateMax`, `state`, `url`, `images`, `auctionStart`, and `auctionEnd`.\n `title`, `description`, `sellerName`, `structuredAddress`, and `geoAddress`\n are ignored on the update path.\n- On the update path, omitting or sending `null` for `price`, `priceEstimateMin`,\n `priceEstimateMax`, `url`, `auctionStart`, or `auctionEnd` leaves the stored value\n unchanged.\n- On the update path, `images` always replaces the stored image set; omitting\n `images` or sending `null` is treated as an empty list and therefore clears all\n stored images.\n\nThe response returns HTTP 202 with an array containing only the `shopsProductId` values\nthat failed to be forwarded to the queue. An empty array indicates that all upserts were\naccepted for asynchronous processing.\n", "operationId": "putPartnerProducts", "tags": [ "Products" @@ -43,7 +43,10 @@ ], "security": [ { - "PartnerApiKeyAuth": [] + "BearerAuth": [] + }, + { + "AccessTokenAuth": [] } ], "requestBody": { @@ -84,11 +87,34 @@ ] }, "update_existing": { - "summary": "Update an existing product's state", + "summary": "Update an existing product while leaving the current price unchanged", "value": [ { "shopsProductId": "baroque-violin-001", - "state": "SOLD" + "state": "SOLD", + "priceEstimateMin": { + "currency": "EUR", + "amount": 4200 + }, + "priceEstimateMax": { + "currency": "EUR", + "amount": 4800 + }, + "url": "https://my-shop.com/products/baroque-violin?status=sold", + "images": [ + "https://my-shop.com/images/violin-1.jpg", + "https://my-shop.com/images/violin-detail.jpg" + ], + "auctionEnd": "2025-05-10T12:30:00Z" + } + ] + }, + "clear_images": { + "summary": "Clear an existing product's images", + "value": [ + { + "shopsProductId": "baroque-violin-001", + "images": null } ] } @@ -159,32 +185,31 @@ } }, "401": { - "description": "Unauthorized \u2014 the `x-api-key` header is missing, malformed, or the API key does not\nmatch the key stored for the shop.\n", + "description": "Unauthorized \u2014 the bearer token is missing, invalid, expired, or the referenced\nAura Historia access token no longer exists.\n", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/ApiError" }, "examples": { - "missing_header": { - "summary": "x-api-key header is missing or empty", + "missing_authorization": { + "summary": "Authorization header is missing", "value": { "status": 401, "title": "Unauthorized", - "error": "BAD_HEADER_VALUE", + "error": "UNAUTHORIZED", "source": { - "field": "x-api-key", + "field": "Authorization", "sourceType": "header" - }, - "detail": "Missing or empty 'x-api-key' header." + } } }, - "api_key_mismatch": { - "summary": "API key does not match the shop's stored key", + "access_token_not_found": { + "summary": "Aura Historia access token is unknown or expired", "value": { "status": 401, "title": "Unauthorized", - "error": "PARTNER_SHOP_API_KEY_MISMATCH" + "error": "ACCESS_TOKEN_NOT_FOUND" } } } @@ -192,7 +217,7 @@ } }, "403": { - "description": "Forbidden \u2014 the shop exists but has not been granted partner status", + "description": "Forbidden \u2014 the authenticated caller is not allowed to ingest products for this shop", "content": { "application/problem+json": { "schema": { @@ -255,7 +280,7 @@ }, "post": { "summary": "Batch create products (Partner API)", - "description": "Creates one or more products for a shop using API key authentication.\nThis endpoint is intended for partner shops \u2014 shops that have been granted partner status\nand have an API key configured. It does **not** use Cognito JWT authentication.\n\nThe request body is an array of `PostProductData` objects. Each entry is forwarded\nindividually to the asynchronous partner-product ingestion queue.\n\nThe response returns HTTP 202 with an array containing only the `shopsProductId` values\nthat failed to be forwarded to the queue. An empty array indicates that all products were\naccepted for asynchronous processing.\n", + "description": "Creates one or more products for a shop using bearer authentication.\nThis endpoint is intended for partner shops and accepts:\n- a Cognito bearer token for the partner user linked to the shop, or\n- an Aura Historia access token owned by that partner user.\n\nAura Historia access tokens on this endpoint must include the `products:write` scope.\n\nThe request body is an array of `PostProductData` objects. Each entry is forwarded\nindividually to the asynchronous partner-product ingestion queue.\n\nThe response returns HTTP 202 with an array containing only the `shopsProductId` values\nthat failed to be forwarded to the queue. An empty array indicates that all products were\naccepted for asynchronous processing.\n", "operationId": "postPartnerProducts", "tags": [ "Products" @@ -275,7 +300,10 @@ ], "security": [ { - "PartnerApiKeyAuth": [] + "BearerAuth": [] + }, + { + "AccessTokenAuth": [] } ], "requestBody": { @@ -378,32 +406,31 @@ } }, "401": { - "description": "Unauthorized \u2014 the `x-api-key` header is missing, malformed, or the API key does not\nmatch the key stored for the shop.\n", + "description": "Unauthorized \u2014 the bearer token is missing, invalid, expired, or the referenced\nAura Historia access token no longer exists.\n", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/ApiError" }, "examples": { - "missing_header": { - "summary": "x-api-key header is missing or empty", + "missing_authorization": { + "summary": "Authorization header is missing", "value": { "status": 401, "title": "Unauthorized", - "error": "BAD_HEADER_VALUE", + "error": "UNAUTHORIZED", "source": { - "field": "x-api-key", + "field": "Authorization", "sourceType": "header" - }, - "detail": "Missing or empty 'x-api-key' header." + } } }, - "api_key_mismatch": { - "summary": "API key does not match the shop's stored key", + "access_token_not_found": { + "summary": "Aura Historia access token is unknown or expired", "value": { "status": 401, "title": "Unauthorized", - "error": "PARTNER_SHOP_API_KEY_MISMATCH" + "error": "ACCESS_TOKEN_NOT_FOUND" } } } @@ -411,7 +438,7 @@ } }, "403": { - "description": "Forbidden \u2014 the shop exists but has not been granted partner status", + "description": "Forbidden \u2014 the authenticated caller is not allowed to ingest products for this shop", "content": { "application/problem+json": { "schema": { @@ -474,7 +501,7 @@ }, "patch": { "summary": "Batch update products (Partner API)", - "description": "Updates one or more existing products for a shop using API key authentication.\nThis endpoint is intended for partner shops \u2014 shops that have been granted partner status\nand have an API key configured. It does **not** use Cognito JWT authentication.\n\nThe request body is an array of `PatchProductData` objects. Only the fields provided in\neach entry are updated; omitted optional fields are left unchanged. Each entry is forwarded\nindividually to the asynchronous partner-product ingestion queue.\n\nThe response returns HTTP 202 with an array containing only the `shopsProductId` values\nthat failed to be forwarded to the queue. An empty array indicates that all updates were\naccepted for asynchronous processing. Because persistence happens asynchronously, acceptance\ndoes not guarantee that the referenced product currently exists or that the update will\nlater succeed.\n", + "description": "Updates one or more existing products for a shop using bearer authentication.\nThis endpoint is intended for partner shops and accepts:\n- a Cognito bearer token for the partner user linked to the shop, or\n- an Aura Historia access token owned by that partner user.\n\nAura Historia access tokens on this endpoint must include the `products:write` scope.\n\nThe request body is an array of `PatchProductData` objects. Only the fields provided in\neach entry are updated; omitted optional fields are left unchanged. Each entry is forwarded\nindividually to the asynchronous partner-product ingestion queue.\n\nThe response returns HTTP 202 with an array containing only the `shopsProductId` values\nthat failed to be forwarded to the queue. An empty array indicates that all updates were\naccepted for asynchronous processing. Because persistence happens asynchronously, acceptance\ndoes not guarantee that the referenced product currently exists or that the update will\nlater succeed.\n", "operationId": "patchPartnerProducts", "tags": [ "Products" @@ -494,7 +521,10 @@ ], "security": [ { - "PartnerApiKeyAuth": [] + "BearerAuth": [] + }, + { + "AccessTokenAuth": [] } ], "requestBody": { @@ -588,32 +618,31 @@ } }, "401": { - "description": "Unauthorized \u2014 the `x-api-key` header is missing, malformed, or the API key does not\nmatch the key stored for the shop.\n", + "description": "Unauthorized \u2014 the bearer token is missing, invalid, expired, or the referenced\nAura Historia access token no longer exists.\n", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/ApiError" }, "examples": { - "missing_header": { - "summary": "x-api-key header is missing or empty", + "missing_authorization": { + "summary": "Authorization header is missing", "value": { "status": 401, "title": "Unauthorized", - "error": "BAD_HEADER_VALUE", + "error": "UNAUTHORIZED", "source": { - "field": "x-api-key", + "field": "Authorization", "sourceType": "header" - }, - "detail": "Missing or empty 'x-api-key' header." + } } }, - "api_key_mismatch": { - "summary": "API key does not match the shop's stored key", + "access_token_not_found": { + "summary": "Aura Historia access token is unknown or expired", "value": { "status": 401, "title": "Unauthorized", - "error": "PARTNER_SHOP_API_KEY_MISMATCH" + "error": "ACCESS_TOKEN_NOT_FOUND" } } } @@ -621,7 +650,7 @@ } }, "403": { - "description": "Forbidden \u2014 the shop exists but has not been granted partner status", + "description": "Forbidden \u2014 the authenticated caller is not allowed to ingest products for this shop", "content": { "application/problem+json": { "schema": { @@ -690,13 +719,13 @@ "type": "http", "scheme": "bearer", "bearerFormat": "JWT", - "description": "Cognito JWT token.\nThe token is used for authenticating and authorizing users as well as personalizing responses.\n" + "description": "Cognito JWT bearer token.\nThis scheme is used for authenticated user routes and for partner routes that accept Cognito user tokens.\n" }, - "PartnerApiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "x-api-key", - "description": "API key for partner shops. Issued to shops that have been granted partner status.\nThe key must be provided in the `x-api-key` request header.\nThis scheme is separate from user authentication (Cognito JWT) and is used\nby partner shops for programmatic product ingestion, WooCommerce webhook delivery,\nand selected partner-authorized shop operations.\n" + "AccessTokenAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "opaque", + "description": "Aura Historia bearer access token created through `/api/v1/me/access-tokens`.\nTokens are sent as standard bearer authorization headers.\nSelected partner routes accept this scheme instead of the removed partner API-key flow.\n" } }, "schemas": { @@ -3311,17 +3340,381 @@ } } }, - "PartnerShopApiKeyResponse": { + "AccessTokenScopeData": { + "type": "string", + "enum": [ + "shops:manage", + "products:write" + ], + "description": "Aura Historia access-token scope string.\n- `shops:manage`: reserved for shop-management flows\n- `products:write`: required for partner batch product ingestion\n", + "example": "products:write" + }, + "AccessTokenTypeData": { + "type": "string", + "enum": [ + "BEARER" + ], + "description": "Token type returned for Aura Historia access tokens.", + "example": "BEARER" + }, + "GetAccessTokenData": { + "type": "object", + "description": "Access-token read model.\n`POST /api/v1/me/access-tokens` returns the plaintext bearer token once;\nlist/get/update endpoints return the masked display value instead.\n", + "required": [ + "accessTokenId", + "name", + "token", + "tokenType", + "created", + "updated" + ], + "properties": { + "accessTokenId": { + "type": "string", + "format": "uuid", + "description": "Unique identifier of the access token.", + "example": "01970f22-2bf0-7000-8000-000000000001" + }, + "name": { + "type": "string", + "maxLength": 128, + "description": "User-defined display name of the access token.", + "example": "Partner product sync" + }, + "scope": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AccessTokenScopeData" + }, + "uniqueItems": true, + "description": "Granted access-token scopes. Omitted when the token has no scopes.", + "example": [ + "products:write" + ] + }, + "token": { + "type": "string", + "description": "Plaintext or masked bearer token value.\nCreate responses return the full token once; subsequent reads return a masked value such as `aurahistoria_abcdefghijk_****`.\n", + "example": "aurahistoria_abcdefghijk_****" + }, + "tokenType": { + "$ref": "#/components/schemas/AccessTokenTypeData" + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Optional absolute expiration timestamp in RFC 3339 format.", + "example": "2026-06-30T00:00:00Z" + }, + "expiresIn": { + "type": "integer", + "minimum": 0, + "nullable": true, + "description": "Optional non-negative number of seconds remaining until expiry.", + "example": 2851200 + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the access token was created.", + "example": "2026-05-28T06:30:00Z" + }, + "updated": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the access token metadata was last updated.", + "example": "2026-05-28T07:00:00Z" + } + } + }, + "PostAccessTokenData": { + "type": "object", + "description": "Request body for creating an Aura Historia access token.", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 128, + "description": "User-defined display name of the new access token.", + "example": "Partner product sync" + }, + "scope": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AccessTokenScopeData" + }, + "uniqueItems": true, + "description": "Optional scopes granted to the access token. Defaults to an empty set.", + "example": [ + "products:write" + ] + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Optional expiration timestamp in RFC 3339 format.", + "example": "2026-06-30T00:00:00Z" + } + } + }, + "PatchAccessTokenData": { + "type": "object", + "description": "Request body for updating access-token metadata.\nOptional fields that are omitted or set to `null` leave the stored value unchanged.\n", + "required": [ + "accessTokenId" + ], + "properties": { + "accessTokenId": { + "type": "string", + "format": "uuid", + "description": "Unique identifier of the access token to update.", + "example": "01970f22-2bf0-7000-8000-000000000001" + }, + "name": { + "type": "string", + "maxLength": 128, + "nullable": true, + "description": "Optional replacement display name.", + "example": "Renamed partner sync token" + }, + "scope": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/AccessTokenScopeData" + }, + "uniqueItems": true, + "description": "Optional replacement scope set.", + "example": [ + "products:write" + ] + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Optional replacement expiration timestamp in RFC 3339 format.", + "example": "2026-07-31T00:00:00Z" + } + } + }, + "OAuthClientMetadataRequestData": { + "type": "object", + "description": "Request body for creating OAuth client metadata.\n`redirect_uris` must contain at least one HTTPS redirect URI.\n", + "required": [ + "client_name", + "redirect_uris" + ], + "properties": { + "client_name": { + "type": "string", + "description": "Display name of the OAuth client.", + "example": "Acceptance OAuth client" + }, + "redirect_uris": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "description": "Registered HTTPS redirect URIs for the OAuth client.", + "items": { + "type": "string", + "format": "uri" + }, + "example": [ + "https://client.example/callback" + ] + }, + "scope": { + "type": "array", + "uniqueItems": true, + "description": "Optional allowed scopes for tokens issued to this OAuth client.", + "items": { + "$ref": "#/components/schemas/AccessTokenScopeData" + }, + "example": [ + "products:write" + ] + } + } + }, + "OAuthClientMetadataPatchData": { + "type": "object", + "description": "Request body for updating OAuth client metadata.\nOmitted or `null` optional properties leave the stored value unchanged.\n", + "properties": { + "client_name": { + "type": "string", + "nullable": true, + "description": "Optional replacement display name for the OAuth client.", + "example": "Updated acceptance OAuth client" + }, + "redirect_uris": { + "type": "array", + "nullable": true, + "minItems": 1, + "uniqueItems": true, + "description": "Optional replacement set of registered HTTPS redirect URIs.", + "items": { + "type": "string", + "format": "uri" + }, + "example": [ + "https://client.example/updated" + ] + }, + "scope": { + "type": "array", + "nullable": true, + "uniqueItems": true, + "description": "Optional replacement set of allowed scopes for the OAuth client.", + "items": { + "$ref": "#/components/schemas/AccessTokenScopeData" + }, + "example": [ + "shops:manage" + ] + } + } + }, + "OAuthClientMetadataResponseData": { "type": "object", - "description": "Response body returned after creating or overwriting a partner shop API key.", + "description": "OAuth client metadata read model.\n`client_secret` contains the plaintext secret only in the create response.\nList/get/update responses return a masked secret display value instead.\n", "required": [ - "apiKey" + "client_id", + "client_secret", + "client_name", + "redirect_uris", + "scope", + "client_id_issued_at" ], "properties": { - "apiKey": { + "client_id": { + "type": "string", + "format": "uuid", + "description": "UUIDv7 identifier of the OAuth client.", + "example": "01970f22-2bf0-7000-8000-000000000010" + }, + "client_secret": { + "type": "string", + "description": "OAuth client secret.\nCreate responses return the plaintext secret once; later reads return a masked value such as `aurahistoria_oauth_client_secret_abcdefghijk_****`.\n", + "example": "aurahistoria_oauth_client_secret_abcdefghijk_****" + }, + "client_name": { "type": "string", - "description": "Plaintext partner shop API key.\nReturned only when the key is created; subsequent verification uses the `x-api-key` request header.\n", - "example": "aurahistoria_abcdefghijk_abcdefghijklmnopqrstuvwxyz1234567" + "description": "Display name of the OAuth client.", + "example": "Acceptance OAuth client" + }, + "redirect_uris": { + "type": "array", + "uniqueItems": true, + "description": "Registered redirect URIs for the OAuth client.", + "items": { + "type": "string", + "format": "uri" + }, + "example": [ + "https://client.example/callback" + ] + }, + "scope": { + "type": "array", + "uniqueItems": true, + "description": "Allowed scopes for tokens issued to this OAuth client.", + "items": { + "$ref": "#/components/schemas/AccessTokenScopeData" + }, + "example": [ + "products:write" + ] + }, + "client_id_issued_at": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp (seconds) when the OAuth client was created and its client ID was issued.", + "example": 1748539200 + } + } + }, + "OAuthTokenResponseData": { + "type": "object", + "description": "Token response returned by `POST /api/v1/oauth/token` after a successful authorization code\nexchange (RFC 6749 \u00a75.1). The `access_token` is an Aura Historia bearer access token.\n`expires_in` is omitted (`null`) for non-expiring tokens.\n", + "required": [ + "access_token", + "token_type", + "scope" + ], + "properties": { + "access_token": { + "type": "string", + "description": "The issued Aura Historia access token (plaintext bearer value).", + "example": "aurahistoria_abcdefghijk_verylongtokenvalue" + }, + "token_type": { + "$ref": "#/components/schemas/AccessTokenTypeData" + }, + "expires_in": { + "type": "integer", + "format": "int64", + "minimum": 0, + "nullable": true, + "description": "Seconds until the access token expires. `null` when the token does not expire.", + "example": null + }, + "scope": { + "type": "string", + "description": "Space-separated list of scopes granted to the access token.", + "example": "products:write" + } + } + }, + "OAuthIntrospectionResponseData": { + "type": "object", + "description": "Introspection response returned by `POST /api/v1/oauth/introspect` (RFC 7662).\nWhen `active` is `false` all other fields are omitted.\nWhen `active` is `true` the available token metadata is populated.\n", + "required": [ + "active" + ], + "properties": { + "active": { + "type": "boolean", + "description": "Whether the token is currently active (not expired, not revoked, and known to this server).", + "example": true + }, + "scope": { + "type": "string", + "description": "Space-separated list of scopes granted to the token. Present only when `active` is `true`.", + "example": "products:write" + }, + "client_id": { + "type": "string", + "format": "uuid", + "description": "UUIDv7 OAuth client identifier that issued the token. Present only when `active` is `true` and the token was issued via the OAuth flow.", + "example": "01970f22-2bf0-7000-8000-000000000010" + }, + "sub": { + "type": "string", + "format": "uuid", + "description": "Subject \u2014 the Aura Historia user ID who authorized the token. Present only when `active` is `true`.", + "example": "0196580c-e4ca-723f-a7e0-1a73588380f1" + }, + "token_type": { + "type": "string", + "description": "Token type. Always `\"Bearer\"` when `active` is `true`.", + "example": "Bearer" + }, + "exp": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp (seconds) when the token expires. Present only when `active` is `true` and the token has an expiry.", + "example": 1780000000 + }, + "iat": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp (seconds) when the token was issued. Present only when `active` is `true` and the issue time is known.", + "example": 1748400000 } } }, @@ -4652,6 +5045,13 @@ "description": "Display name of the shop referenced by the partner application.", "example": "Tech Store" }, + "image": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Optional shop logo URL included with partner-application notifications when the related\napplication or linked existing shop has an image configured.\nAbsent when no shop logo URL is available.\n", + "example": "https://tech-store.example/logo.png" + }, "partnerApplicationPayload": { "$ref": "#/components/schemas/PartnerApplicationPayloadData" } @@ -4772,7 +5172,7 @@ }, "PostProductData": { "type": "object", - "description": "Data for creating a single product via the partner batch-create endpoint.\nwhen omitted from the request.\n", + "description": "Data for creating a single product via the partner batch-create endpoint.\n", "required": [ "shopsProductId", "title", @@ -4977,7 +5377,7 @@ }, "PutProductData": { "type": "object", - "description": "Data for upserting a single product via the partner batch-upsert endpoint.\nOnly `shopsProductId` is required. All other fields are optional.\n\nDuring later asynchronous ingestion:\n- If the product **does not yet exist**, it is created using all provided fields.\n Omitting `title`, `url`, or `state` will result in placeholder defaults being applied\n internally (empty title, a placeholder URL, and `LISTED` state respectively).\n- If the product **already exists**, only `state` and `price` are updated; all other\n fields are ignored for the update path.\n\nto `UNKNOWN` when omitted.\n", + "description": "Data for upserting a single product via the partner batch-upsert endpoint.\nOnly `shopsProductId` is required. All other fields are optional.\n\nDuring later asynchronous ingestion:\n- If the product **does not yet exist**, it is created using all provided fields.\n Omitting `title`, `url`, or `state` will result in placeholder defaults being applied\n internally (empty title, a placeholder URL, and `LISTED` state respectively).\n- If the product **already exists**, `price`, `priceEstimateMin`,\n `priceEstimateMax`, `state`, `url`, `images`, `auctionStart`, and `auctionEnd`\n participate in the update path.\n- On the update path, omitting or sending `null` for `price`, `priceEstimateMin`,\n `priceEstimateMax`, `url`, `auctionStart`, or `auctionEnd` leaves the stored value\n unchanged.\n- On the update path, omitting `images` or sending `null` is treated as an empty list\n and therefore clears all stored images.\n", "required": [ "shopsProductId" ], @@ -5012,7 +5412,7 @@ } ], "nullable": true, - "description": "Optional asking price for the product. Applied on both create and update paths." + "description": "Optional asking price for the product. Applied on create and on update when a non-null value is provided. Omit or send `null` to leave the current stored price unchanged." }, "priceEstimateMin": { "allOf": [ @@ -5021,7 +5421,7 @@ } ], "nullable": true, - "description": "Optional lower bound of the estimated price range. Used only when creating a new product." + "description": "Optional lower bound of the estimated price range. Applied on both create and update paths. Omit or send `null` to leave the current stored lower bound unchanged." }, "priceEstimateMax": { "allOf": [ @@ -5030,7 +5430,7 @@ } ], "nullable": true, - "description": "Optional upper bound of the estimated price range. Used only when creating a new product." + "description": "Optional upper bound of the estimated price range. Applied on both create and update paths. Omit or send `null` to leave the current stored upper bound unchanged." }, "state": { "allOf": [ @@ -5045,7 +5445,7 @@ "type": "string", "format": "uri", "nullable": true, - "description": "URL to the product on the shop's website. Used only when creating a new product.", + "description": "URL to the product on the shop's website. Applied on both create and update paths. Omit or send `null` to leave the current stored URL unchanged.", "example": "https://my-shop.com/products/baroque-violin" }, "images": { @@ -5055,7 +5455,7 @@ "format": "uri" }, "nullable": true, - "description": "List of image URLs for the product. Used only when creating a new product.", + "description": "List of image URLs for the product. Applied on both create and update paths. On update, the provided array replaces the stored image set; omitting `images` or sending `null` is treated as an empty list and therefore clears all stored images.", "example": [ "https://my-shop.com/images/violin-1.jpg", "https://my-shop.com/images/violin-2.jpg" @@ -5065,14 +5465,14 @@ "type": "string", "format": "date-time", "nullable": true, - "description": "RFC3339 timestamp of when the auction for this product starts.\nOnly relevant for auction-house shop types. Used only when creating a new product.\n", + "description": "RFC3339 timestamp of when the auction for this product starts.\nOnly relevant for auction-house shop types. Applied on both create and update paths.\nOmit or send `null` to leave the current stored start timestamp unchanged.\n", "example": "2025-05-01T12:00:00Z" }, "auctionEnd": { "type": "string", "format": "date-time", "nullable": true, - "description": "RFC3339 timestamp of when the auction for this product ends.\nOnly relevant for auction-house shop types. Used only when creating a new product.\n", + "description": "RFC3339 timestamp of when the auction for this product ends.\nOnly relevant for auction-house shop types. Applied on both create and update paths.\nOmit or send `null` to leave the current stored end timestamp unchanged.\n", "example": "2025-05-10T12:00:00Z" }, "sellerName": { diff --git a/src/client/@tanstack/react-query.gen.ts b/src/client/@tanstack/react-query.gen.ts index aef3335c..d91d2df8 100644 --- a/src/client/@tanstack/react-query.gen.ts +++ b/src/client/@tanstack/react-query.gen.ts @@ -3,15 +3,18 @@ import { queryOptions, type UseMutationOptions } from '@tanstack/react-query'; import { client } from '../client.gen'; -import { addWatchlistProduct, adminDeleteUser, adminGetPartnerApplication, adminGetPartnerApplications, adminGetUser, adminPatchPartnerApplication, adminPatchUser, adminPostPartnerApplicationDecision, adminSearchUsers, complexSearchProducts, createUserSearchFilter, deleteAllNotifications, deleteNotification, deletePartnerApplication, deleteUser, deleteUserSearchFilter, deleteWatchlistProduct, getCategories, getCategoryById, getNotifications, getPartnerApplication, getPartnerApplications, getPartnerShops, getPeriodById, getPeriods, getProduct, getProductBySlug, getProductHistory, getSearchFilterLiveProducts, getSearchFilterMatches, getShopById, getShopBySlug, getSimilarProducts, getUserAccount, getUserSearchFilter, getUserSearchFilters, getWatchlistProducts, type Options, patchAllNotifications, patchNotification, patchPartnerApplication, patchPartnerProducts, patchShopById, patchWatchlistProduct, postBillingCheckout, postBillingManage, postBillingPortal, postPartnerApplication, postPartnerProducts, postShop, postWoocommerceWebhook, putNewsletterSubscription, putPartnerProducts, putShopApiKey, searchCategories, searchPeriods, searchShops, simpleSearchProducts, simpleSearchShops, updateSearchFilterMatchFeedback, updateUserAccount, updateUserSearchFilter } from '../sdk.gen'; -import type { AddWatchlistProductData, AddWatchlistProductError, AddWatchlistProductResponse, AdminDeleteUserData, AdminDeleteUserError, AdminDeleteUserResponse, AdminGetPartnerApplicationData, AdminGetPartnerApplicationError, AdminGetPartnerApplicationResponse, AdminGetPartnerApplicationsData, AdminGetPartnerApplicationsError, AdminGetPartnerApplicationsResponse, AdminGetUserData, AdminGetUserError, AdminGetUserResponse, AdminPatchPartnerApplicationData, AdminPatchPartnerApplicationError, AdminPatchPartnerApplicationResponse, AdminPatchUserData, AdminPatchUserError, AdminPatchUserResponse, AdminPostPartnerApplicationDecisionData, AdminPostPartnerApplicationDecisionError, AdminPostPartnerApplicationDecisionResponse, AdminSearchUsersData, AdminSearchUsersError, AdminSearchUsersResponse, ComplexSearchProductsData, ComplexSearchProductsError, ComplexSearchProductsResponse, CreateUserSearchFilterData, CreateUserSearchFilterError, CreateUserSearchFilterResponse, DeleteAllNotificationsData, DeleteAllNotificationsError, DeleteAllNotificationsResponse, DeleteNotificationData, DeleteNotificationError, DeleteNotificationResponse, DeletePartnerApplicationData, DeletePartnerApplicationError, DeletePartnerApplicationResponse, DeleteUserData, DeleteUserError, DeleteUserResponse, DeleteUserSearchFilterData, DeleteUserSearchFilterError, DeleteUserSearchFilterResponse, DeleteWatchlistProductData, DeleteWatchlistProductError, DeleteWatchlistProductResponse, GetCategoriesData, GetCategoriesError, GetCategoriesResponse, GetCategoryByIdData, GetCategoryByIdError, GetCategoryByIdResponse, GetNotificationsData, GetNotificationsError, GetNotificationsResponse, GetPartnerApplicationData, GetPartnerApplicationError, GetPartnerApplicationResponse, GetPartnerApplicationsData, GetPartnerApplicationsError, GetPartnerApplicationsResponse, GetPartnerShopsData, GetPartnerShopsError, GetPartnerShopsResponse, GetPeriodByIdData, GetPeriodByIdError, GetPeriodByIdResponse, GetPeriodsData, GetPeriodsError, GetPeriodsResponse, GetProductBySlugData, GetProductBySlugError, GetProductBySlugResponse, GetProductData2, GetProductError, GetProductHistoryData, GetProductHistoryError, GetProductHistoryResponse, GetProductResponse, GetSearchFilterLiveProductsData, GetSearchFilterLiveProductsError, GetSearchFilterLiveProductsResponse, GetSearchFilterMatchesData, GetSearchFilterMatchesError, GetSearchFilterMatchesResponse, GetShopByIdData, GetShopByIdError, GetShopByIdResponse, GetShopBySlugData, GetShopBySlugError, GetShopBySlugResponse, GetSimilarProductsData, GetSimilarProductsError, GetSimilarProductsResponse, GetUserAccountData2, GetUserAccountError, GetUserAccountResponse, GetUserSearchFilterData, GetUserSearchFilterError, GetUserSearchFilterResponse, GetUserSearchFiltersData, GetUserSearchFiltersError, GetUserSearchFiltersResponse, GetWatchlistProductsData, GetWatchlistProductsError, GetWatchlistProductsResponse, PatchAllNotificationsData, PatchAllNotificationsError, PatchAllNotificationsResponse, PatchNotificationData2, PatchNotificationError, PatchNotificationResponse, PatchPartnerApplicationData, PatchPartnerApplicationError, PatchPartnerApplicationResponse, PatchPartnerProductsData, PatchPartnerProductsError, PatchPartnerProductsResponse, PatchShopByIdData, PatchShopByIdError, PatchShopByIdResponse, PatchWatchlistProductData, PatchWatchlistProductError, PatchWatchlistProductResponse, PostBillingCheckoutData2, PostBillingCheckoutError, PostBillingCheckoutResponse, PostBillingManageData, PostBillingManageError, PostBillingManageResponse, PostBillingPortalData, PostBillingPortalError, PostBillingPortalResponse, PostPartnerApplicationData, PostPartnerApplicationError, PostPartnerApplicationResponse, PostPartnerProductsData, PostPartnerProductsError, PostPartnerProductsResponse, PostShopData2, PostShopError, PostShopResponse, PostWoocommerceWebhookData, PostWoocommerceWebhookError, PutNewsletterSubscriptionData2, PutNewsletterSubscriptionError, PutNewsletterSubscriptionResponse, PutPartnerProductsData, PutPartnerProductsError, PutPartnerProductsResponse, PutShopApiKeyData, PutShopApiKeyError, PutShopApiKeyResponse, SearchCategoriesData, SearchCategoriesError, SearchCategoriesResponse, SearchPeriodsData, SearchPeriodsError, SearchPeriodsResponse, SearchShopsData, SearchShopsError, SearchShopsResponse, SimpleSearchProductsData, SimpleSearchProductsError, SimpleSearchProductsResponse, SimpleSearchShopsData, SimpleSearchShopsError, SimpleSearchShopsResponse, UpdateSearchFilterMatchFeedbackData, UpdateSearchFilterMatchFeedbackError, UpdateSearchFilterMatchFeedbackResponse, UpdateUserAccountData, UpdateUserAccountError, UpdateUserAccountResponse, UpdateUserSearchFilterData, UpdateUserSearchFilterError, UpdateUserSearchFilterResponse } from '../types.gen'; +import { addWatchlistProduct, adminDeleteUser, adminGetPartnerApplication, adminGetPartnerApplications, adminGetUser, adminPatchPartnerApplication, adminPatchUser, adminPostPartnerApplicationDecision, adminSearchUsers, complexSearchProducts, createUserSearchFilter, deleteAllNotifications, deleteMyAccessToken, deleteNotification, deleteOAuthClient, deletePartnerApplication, deleteUser, deleteUserSearchFilter, deleteWatchlistProduct, getCategories, getCategoryById, getMyAccessToken, getMyAccessTokens, getMyPartnerShops, getNotifications, getOAuthClient, getOAuthClients, getPartnerApplication, getPartnerApplications, getPeriodById, getPeriods, getProduct, getProductBySlug, getProductHistory, getSearchFilterLiveProducts, getSearchFilterMatches, getShopById, getShopBySlug, getSimilarProducts, getUserAccount, getUserSearchFilter, getUserSearchFilters, getWatchlistProducts, oauthAuthorize, oauthIntrospect, oauthRevoke, oauthToken, type Options, patchAllNotifications, patchMyAccessToken, patchNotification, patchOAuthClient, patchPartnerApplication, patchPartnerProducts, patchShopById, patchWatchlistProduct, postBillingCheckout, postBillingManage, postBillingPortal, postMyAccessToken, postOAuthClient, postPartnerApplication, postPartnerProducts, postShop, postWoocommerceWebhook, putNewsletterSubscription, putPartnerProducts, searchCategories, searchPeriods, searchShops, simpleSearchProducts, simpleSearchShops, updateSearchFilterMatchFeedback, updateUserAccount, updateUserSearchFilter } from '../sdk.gen'; +import type { AddWatchlistProductData, AddWatchlistProductError, AddWatchlistProductResponse, AdminDeleteUserData, AdminDeleteUserError, AdminDeleteUserResponse, AdminGetPartnerApplicationData, AdminGetPartnerApplicationError, AdminGetPartnerApplicationResponse, AdminGetPartnerApplicationsData, AdminGetPartnerApplicationsError, AdminGetPartnerApplicationsResponse, AdminGetUserData, AdminGetUserError, AdminGetUserResponse, AdminPatchPartnerApplicationData, AdminPatchPartnerApplicationError, AdminPatchPartnerApplicationResponse, AdminPatchUserData, AdminPatchUserError, AdminPatchUserResponse, AdminPostPartnerApplicationDecisionData, AdminPostPartnerApplicationDecisionError, AdminPostPartnerApplicationDecisionResponse, AdminSearchUsersData, AdminSearchUsersError, AdminSearchUsersResponse, ComplexSearchProductsData, ComplexSearchProductsError, ComplexSearchProductsResponse, CreateUserSearchFilterData, CreateUserSearchFilterError, CreateUserSearchFilterResponse, DeleteAllNotificationsData, DeleteAllNotificationsError, DeleteAllNotificationsResponse, DeleteMyAccessTokenData, DeleteMyAccessTokenError, DeleteMyAccessTokenResponse, DeleteNotificationData, DeleteNotificationError, DeleteNotificationResponse, DeleteOAuthClientData, DeleteOAuthClientError, DeleteOAuthClientResponse, DeletePartnerApplicationData, DeletePartnerApplicationError, DeletePartnerApplicationResponse, DeleteUserData, DeleteUserError, DeleteUserResponse, DeleteUserSearchFilterData, DeleteUserSearchFilterError, DeleteUserSearchFilterResponse, DeleteWatchlistProductData, DeleteWatchlistProductError, DeleteWatchlistProductResponse, GetCategoriesData, GetCategoriesError, GetCategoriesResponse, GetCategoryByIdData, GetCategoryByIdError, GetCategoryByIdResponse, GetMyAccessTokenData, GetMyAccessTokenError, GetMyAccessTokenResponse, GetMyAccessTokensData, GetMyAccessTokensError, GetMyAccessTokensResponse, GetMyPartnerShopsData, GetMyPartnerShopsError, GetMyPartnerShopsResponse, GetNotificationsData, GetNotificationsError, GetNotificationsResponse, GetOAuthClientData, GetOAuthClientError, GetOAuthClientResponse, GetOAuthClientsData, GetOAuthClientsError, GetOAuthClientsResponse, GetPartnerApplicationData, GetPartnerApplicationError, GetPartnerApplicationResponse, GetPartnerApplicationsData, GetPartnerApplicationsError, GetPartnerApplicationsResponse, GetPeriodByIdData, GetPeriodByIdError, GetPeriodByIdResponse, GetPeriodsData, GetPeriodsError, GetPeriodsResponse, GetProductBySlugData, GetProductBySlugError, GetProductBySlugResponse, GetProductData2, GetProductError, GetProductHistoryData, GetProductHistoryError, GetProductHistoryResponse, GetProductResponse, GetSearchFilterLiveProductsData, GetSearchFilterLiveProductsError, GetSearchFilterLiveProductsResponse, GetSearchFilterMatchesData, GetSearchFilterMatchesError, GetSearchFilterMatchesResponse, GetShopByIdData, GetShopByIdError, GetShopByIdResponse, GetShopBySlugData, GetShopBySlugError, GetShopBySlugResponse, GetSimilarProductsData, GetSimilarProductsError, GetSimilarProductsResponse, GetUserAccountData2, GetUserAccountError, GetUserAccountResponse, GetUserSearchFilterData, GetUserSearchFilterError, GetUserSearchFilterResponse, GetUserSearchFiltersData, GetUserSearchFiltersError, GetUserSearchFiltersResponse, GetWatchlistProductsData, GetWatchlistProductsError, GetWatchlistProductsResponse, OauthAuthorizeData, OauthAuthorizeError, OauthIntrospectData, OauthIntrospectError, OauthIntrospectResponse, OauthRevokeData, OauthRevokeError, OauthTokenData, OauthTokenError, OauthTokenResponse, PatchAllNotificationsData, PatchAllNotificationsError, PatchAllNotificationsResponse, PatchMyAccessTokenData, PatchMyAccessTokenError, PatchMyAccessTokenResponse, PatchNotificationData2, PatchNotificationError, PatchNotificationResponse, PatchOAuthClientData, PatchOAuthClientError, PatchOAuthClientResponse, PatchPartnerApplicationData, PatchPartnerApplicationError, PatchPartnerApplicationResponse, PatchPartnerProductsData, PatchPartnerProductsError, PatchPartnerProductsResponse, PatchShopByIdData, PatchShopByIdError, PatchShopByIdResponse, PatchWatchlistProductData, PatchWatchlistProductError, PatchWatchlistProductResponse, PostBillingCheckoutData2, PostBillingCheckoutError, PostBillingCheckoutResponse, PostBillingManageData, PostBillingManageError, PostBillingManageResponse, PostBillingPortalData, PostBillingPortalError, PostBillingPortalResponse, PostMyAccessTokenData, PostMyAccessTokenError, PostMyAccessTokenResponse, PostOAuthClientData, PostOAuthClientError, PostOAuthClientResponse, PostPartnerApplicationData, PostPartnerApplicationError, PostPartnerApplicationResponse, PostPartnerProductsData, PostPartnerProductsError, PostPartnerProductsResponse, PostShopData2, PostShopError, PostShopResponse, PostWoocommerceWebhookData, PostWoocommerceWebhookError, PutNewsletterSubscriptionData2, PutNewsletterSubscriptionError, PutNewsletterSubscriptionResponse, PutPartnerProductsData, PutPartnerProductsError, PutPartnerProductsResponse, SearchCategoriesData, SearchCategoriesError, SearchCategoriesResponse, SearchPeriodsData, SearchPeriodsError, SearchPeriodsResponse, SearchShopsData, SearchShopsError, SearchShopsResponse, SimpleSearchProductsData, SimpleSearchProductsError, SimpleSearchProductsResponse, SimpleSearchShopsData, SimpleSearchShopsError, SimpleSearchShopsResponse, UpdateSearchFilterMatchFeedbackData, UpdateSearchFilterMatchFeedbackError, UpdateSearchFilterMatchFeedbackResponse, UpdateUserAccountData, UpdateUserAccountError, UpdateUserAccountResponse, UpdateUserSearchFilterData, UpdateUserSearchFilterError, UpdateUserSearchFilterResponse } from '../types.gen'; /** * Batch update products (Partner API) * - * Updates one or more existing products for a shop using API key authentication. - * This endpoint is intended for partner shops — shops that have been granted partner status - * and have an API key configured. It does **not** use Cognito JWT authentication. + * Updates one or more existing products for a shop using bearer authentication. + * This endpoint is intended for partner shops and accepts: + * - a Cognito bearer token for the partner user linked to the shop, or + * - an Aura Historia access token owned by that partner user. + * + * Aura Historia access tokens on this endpoint must include the `products:write` scope. * * The request body is an array of `PatchProductData` objects. Only the fields provided in * each entry are updated; omitted optional fields are left unchanged. Each entry is forwarded @@ -41,9 +44,12 @@ export const patchPartnerProductsMutation = (options?: Partial) => createQueryKey('getMyAccessTokens', options); + +/** + * List Aura Historia access tokens + * + * Lists the authenticated user's non-expired Aura Historia access tokens. + * The `token` field is masked on this endpoint and does not reveal the plaintext bearer token again. + * Requires valid Cognito JWT authentication. + * + */ +export const getMyAccessTokensOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getMyAccessTokens({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getMyAccessTokensQueryKey(options) +}); + +/** + * Update an Aura Historia access token + * + * Updates metadata for one access token owned by the authenticated user. + * Omitted or `null` optional properties leave the existing value unchanged. + * Requires valid Cognito JWT authentication. + * + */ +export const patchMyAccessTokenMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await patchMyAccessToken({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Create an Aura Historia access token + * + * Creates a new Aura Historia access token for the authenticated user. + * The plaintext bearer token is returned only in this create response; later reads return a masked token value. + * Requires valid Cognito JWT authentication. + * + */ +export const postMyAccessTokenMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postMyAccessToken({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Delete an Aura Historia access token + * + * Deletes one access token owned by the authenticated user. + * The token to delete is identified by the required `{accessTokenId}` path parameter. + * Requires valid Cognito JWT authentication. + * + */ +export const deleteMyAccessTokenMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteMyAccessToken({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getMyAccessTokenQueryKey = (options: Options) => createQueryKey('getMyAccessToken', options); + +/** + * Get one Aura Historia access token + * + * Retrieves one non-expired Aura Historia access token owned by the authenticated user. + * The `token` field is masked on this endpoint and does not reveal the plaintext bearer token again. + * Requires valid Cognito JWT authentication. + * + */ +export const getMyAccessTokenOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getMyAccessToken({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getMyAccessTokenQueryKey(options) +}); + /** * Create Stripe checkout session * @@ -1052,6 +1185,10 @@ export const simpleSearchShopsQueryKey = (options?: Options) => create * Retrieves detailed information about a specific shop by its shop ID (UUID). * Returns complete shop metadata including name, domains, image, address/contact metadata, and timestamps. * + * Authentication is optional on this endpoint: + * - unauthenticated requests return cacheable shared responses + * - authenticated Cognito JWT or Aura Historia access-token requests return `Cache-Control: no-store` + * */ export const getShopByIdOptions = (options: Options) => queryOptions>({ queryFn: async ({ queryKey, signal }) => { @@ -1130,7 +1271,7 @@ export const getShopByIdOptions = (options: Options) => queryOp * * Requires either: * - a valid Cognito JWT for the partner user assigned to the shop or for an `ADMIN`, or - * - the partner shop's `x-api-key` when no Cognito identity is present. + * - a valid Aura Historia access token owned by the partner user assigned to the shop. * * The request body uses `PatchShopData`. Only fields present in the JSON body are applied; * omitted or `null` fields are left unchanged. The HTTP request body itself must not be absent @@ -1151,33 +1292,6 @@ export const patchShopByIdMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await putShopApiKey({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - export const getShopBySlugQueryKey = (options: Options) => createQueryKey('getShopBySlug', options); /** @@ -1186,6 +1300,10 @@ export const getShopBySlugQueryKey = (options: Options) => cr * Retrieves detailed information about a specific shop by its human-readable slug identifier. * Returns complete shop metadata including name, domains, image, address/contact metadata, and timestamps. * + * Authentication is optional on this endpoint: + * - unauthenticated requests return cacheable shared responses + * - authenticated Cognito JWT or Aura Historia access-token requests return `Cache-Control: no-store` + * * **Human-Readable Identifiers**: This endpoint uses slug-based identifiers which are human-readable * kebab-case strings derived from the shop name (e.g., "tech-store-premium" or "christies"). * @@ -1393,23 +1511,20 @@ export const searchPeriodsMutation = (options?: Partial) => createQueryKey('getPartnerShops', options); +export const getMyPartnerShopsQueryKey = (options?: Options) => createQueryKey('getMyPartnerShops', options); /** - * List shops for a partner user + * List the authenticated user's partner shops * - * Returns all shops whose `partner_user_id` matches the requested `partnerId`. + * Returns all shops currently linked to the authenticated user's `partnerShops` set. + * Requires a valid Cognito JWT and always resolves shops for the current user only. * - * Requires a valid Cognito JWT. The caller may request: - * - their own shops (`partnerId` equals the JWT `sub`), or - * - another partner's shops when the caller has the `ADMIN` role. - * - * Returns an empty array when the partner currently has no shops. + * Returns an empty array when the authenticated user currently has no linked partner shops. * */ -export const getPartnerShopsOptions = (options: Options) => queryOptions>({ +export const getMyPartnerShopsOptions = (options?: Options) => queryOptions>({ queryFn: async ({ queryKey, signal }) => { - const { data } = await getPartnerShops({ + const { data } = await getMyPartnerShops({ ...options, ...queryKey[0], signal, @@ -1417,7 +1532,7 @@ export const getPartnerShopsOptions = (options: Options) => }); return data; }, - queryKey: getPartnerShopsQueryKey(options) + queryKey: getMyPartnerShopsQueryKey(options) }); export const getPartnerApplicationsQueryKey = (options?: Options) => createQueryKey('getPartnerApplications', options); @@ -1634,3 +1749,220 @@ export const adminPostPartnerApplicationDecisionMutation = (options?: Partial) => createQueryKey('oauthAuthorize', options); + +/** + * OAuth2 authorization endpoint + * + * Starts the OAuth2 authorization code flow (RFC 6749 §4.1). + * The authenticated user (resource owner) grants the requesting OAuth client access to their + * Aura Historia account. PKCE (RFC 7636) with `S256` is required for all requests. + * + * On success the endpoint redirects (302) to the `redirect_uri` appending `code` and, if + * supplied, `state` as query parameters. The single-use authorization code must be exchanged + * for an access token via `POST /api/v1/oauth/token` before it expires. + * + */ +export const oauthAuthorizeOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await oauthAuthorize({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: oauthAuthorizeQueryKey(options) +}); + +/** + * OAuth2 token endpoint + * + * Exchanges a single-use authorization code for an Aura Historia access token (RFC 6749 §4.1.3). + * The request body must be `application/x-www-form-urlencoded`. Client authentication is + * performed via `client_id` / `client_secret` form fields. PKCE code verification (RFC 7636) + * is mandatory: `code_verifier` must produce the `code_challenge` supplied to the authorize + * endpoint. + * + * The issued access token is non-expiring by default (same lifetime as tokens created via + * `POST /api/v1/me/access-tokens`). + * + */ +export const oauthTokenMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await oauthToken({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * OAuth2 token revocation endpoint + * + * Revokes an Aura Historia access token that was issued through the OAuth authorization code + * flow (RFC 7009). The request body must be `application/x-www-form-urlencoded`. Client + * authentication is performed via `client_id` / `client_secret` form fields. + * + * After revocation the token is immediately inactive and introspection will return + * `active: false`. Attempting to revoke an unknown or already-revoked token is not an error. + * + */ +export const oauthRevokeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await oauthRevoke({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * OAuth2 token introspection endpoint + * + * Returns metadata about an Aura Historia access token (RFC 7662). The request body must be + * `application/x-www-form-urlencoded`. Client authentication is performed via `client_id` / + * `client_secret` form fields. + * + * If the token is unknown, expired, or revoked, `active` is `false` and all other fields are + * omitted. Otherwise `active` is `true` and the available token metadata is populated. + * + */ +export const oauthIntrospectMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await oauthIntrospect({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getOAuthClientsQueryKey = (options?: Options) => createQueryKey('getOAuthClients', options); + +/** + * List OAuth client metadata + * + * Lists registered OAuth client metadata records. + * The returned `client_secret` value is masked and does not reveal the plaintext secret. + * Requires valid Cognito JWT authentication. + * + */ +export const getOAuthClientsOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getOAuthClients({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getOAuthClientsQueryKey(options) +}); + +/** + * Create OAuth client metadata + * + * Creates a new OAuth client metadata record. + * The plaintext `client_secret` is returned only in this create response; later reads return + * a masked secret display value. + * Requires valid Cognito JWT authentication and the `ADMIN` role. + * + */ +export const postOAuthClientMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postOAuthClient({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Delete OAuth client metadata + * + * Deletes one OAuth client metadata record. + * Requires valid Cognito JWT authentication and the `ADMIN` role. + * + */ +export const deleteOAuthClientMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteOAuthClient({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getOAuthClientQueryKey = (options: Options) => createQueryKey('getOAuthClient', options); + +/** + * Get OAuth client metadata + * + * Retrieves one OAuth client metadata record. + * The returned `client_secret` value is masked and does not reveal the plaintext secret. + * Requires valid Cognito JWT authentication. + * + */ +export const getOAuthClientOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getOAuthClient({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getOAuthClientQueryKey(options) +}); + +/** + * Update OAuth client metadata + * + * Updates one OAuth client metadata record. + * Omitted or `null` optional properties leave the existing value unchanged. + * The returned `client_secret` value is masked and does not reveal the plaintext secret. + * Requires valid Cognito JWT authentication and the `ADMIN` role. + * + */ +export const patchOAuthClientMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await patchOAuthClient({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; diff --git a/src/client/index.ts b/src/client/index.ts index 864a1853..fd4fc9bc 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addWatchlistProduct, adminDeleteUser, adminGetPartnerApplication, adminGetPartnerApplications, adminGetUser, adminPatchPartnerApplication, adminPatchUser, adminPostPartnerApplicationDecision, adminSearchUsers, complexSearchProducts, createUserSearchFilter, deleteAllNotifications, deleteNotification, deletePartnerApplication, deleteUser, deleteUserSearchFilter, deleteWatchlistProduct, getCategories, getCategoryById, getNotifications, getPartnerApplication, getPartnerApplications, getPartnerShops, getPeriodById, getPeriods, getProduct, getProductBySlug, getProductHistory, getSearchFilterLiveProducts, getSearchFilterMatches, getShopById, getShopBySlug, getSimilarProducts, getUserAccount, getUserSearchFilter, getUserSearchFilters, getWatchlistProducts, type Options, patchAllNotifications, patchNotification, patchPartnerApplication, patchPartnerProducts, patchShopById, patchWatchlistProduct, postBillingCheckout, postBillingManage, postBillingPortal, postPartnerApplication, postPartnerProducts, postShop, postWoocommerceWebhook, putNewsletterSubscription, putPartnerProducts, putShopApiKey, searchCategories, searchPeriods, searchShops, simpleSearchProducts, simpleSearchShops, updateSearchFilterMatchFeedback, updateUserAccount, updateUserSearchFilter } from './sdk.gen'; -export type { AddWatchlistProductData, AddWatchlistProductError, AddWatchlistProductErrors, AddWatchlistProductResponse, AddWatchlistProductResponses, AdminDeleteUserData, AdminDeleteUserError, AdminDeleteUserErrors, AdminDeleteUserResponse, AdminDeleteUserResponses, AdminGetPartnerApplicationData, AdminGetPartnerApplicationError, AdminGetPartnerApplicationErrors, AdminGetPartnerApplicationResponse, AdminGetPartnerApplicationResponses, AdminGetPartnerApplicationsData, AdminGetPartnerApplicationsError, AdminGetPartnerApplicationsErrors, AdminGetPartnerApplicationsResponse, AdminGetPartnerApplicationsResponses, AdminGetUserData, AdminGetUserError, AdminGetUserErrors, AdminGetUserResponse, AdminGetUserResponses, AdminPatchPartnerApplicationData, AdminPatchPartnerApplicationError, AdminPatchPartnerApplicationErrors, AdminPatchPartnerApplicationResponse, AdminPatchPartnerApplicationResponses, AdminPatchPartnerShopApplicationData, AdminPatchUserData, AdminPatchUserError, AdminPatchUserErrors, AdminPatchUserResponse, AdminPatchUserResponses, AdminPostPartnerApplicationDecisionData, AdminPostPartnerApplicationDecisionError, AdminPostPartnerApplicationDecisionErrors, AdminPostPartnerApplicationDecisionResponse, AdminPostPartnerApplicationDecisionResponses, AdminSearchUsersData, AdminSearchUsersError, AdminSearchUsersErrors, AdminSearchUsersResponse, AdminSearchUsersResponses, ApiError, ApiErrorSource, ApprovedPartnerApplicationPayloadData, AuctionData, BillingCycleData, BillingPlanData, BillingSessionUrlData, CategorySearchData, ClientOptions, ComplexSearchProductsData, ComplexSearchProductsError, ComplexSearchProductsErrors, ComplexSearchProductsResponse, ComplexSearchProductsResponses, ContinentData, CountryCodeData, CreateUserSearchFilterData, CreateUserSearchFilterError, CreateUserSearchFilterErrors, CreateUserSearchFilterResponse, CreateUserSearchFilterResponses, CurrencyData, DeleteAllNotificationsData, DeleteAllNotificationsError, DeleteAllNotificationsErrors, DeleteAllNotificationsResponse, DeleteAllNotificationsResponses, DeleteNotificationData, DeleteNotificationError, DeleteNotificationErrors, DeleteNotificationResponse, DeleteNotificationResponses, DeletePartnerApplicationData, DeletePartnerApplicationError, DeletePartnerApplicationErrors, DeletePartnerApplicationResponse, DeletePartnerApplicationResponses, DeleteUserData, DeleteUserError, DeleteUserErrors, DeleteUserResponse, DeleteUserResponses, DeleteUserSearchFilterData, DeleteUserSearchFilterError, DeleteUserSearchFilterErrors, DeleteUserSearchFilterResponse, DeleteUserSearchFilterResponses, DeleteWatchlistProductData, DeleteWatchlistProductError, DeleteWatchlistProductErrors, DeleteWatchlistProductResponse, DeleteWatchlistProductResponses, DistanceData, DistanceUnitData, ExecutionStateData, GeoAddressData, GeoDistanceQueryData, GetCategoriesData, GetCategoriesError, GetCategoriesErrors, GetCategoriesResponse, GetCategoriesResponses, GetCategoryByIdData, GetCategoryByIdError, GetCategoryByIdErrors, GetCategoryByIdResponse, GetCategoryByIdResponses, GetCategoryData, GetCategorySummaryData, GetNotificationData, GetNotificationsData, GetNotificationsError, GetNotificationsErrors, GetNotificationsResponse, GetNotificationsResponses, GetPartnerApplicationData, GetPartnerApplicationError, GetPartnerApplicationErrors, GetPartnerApplicationResponse, GetPartnerApplicationResponses, GetPartnerApplicationsData, GetPartnerApplicationsError, GetPartnerApplicationsErrors, GetPartnerApplicationsResponse, GetPartnerApplicationsResponses, GetPartnerShopApplicationData, GetPartnerShopApplicationPayloadData, GetPartnerShopsData, GetPartnerShopsError, GetPartnerShopsErrors, GetPartnerShopsResponse, GetPartnerShopsResponses, GetPeriodByIdData, GetPeriodByIdError, GetPeriodByIdErrors, GetPeriodByIdResponse, GetPeriodByIdResponses, GetPeriodData, GetPeriodsData, GetPeriodsError, GetPeriodsErrors, GetPeriodsResponse, GetPeriodsResponses, GetPeriodSummaryData, GetProductBySlugData, GetProductBySlugError, GetProductBySlugErrors, GetProductBySlugResponse, GetProductBySlugResponses, GetProductData, GetProductData2, GetProductError, GetProductErrors, GetProductEventData, GetProductHistoryData, GetProductHistoryError, GetProductHistoryErrors, GetProductHistoryResponse, GetProductHistoryResponses, GetProductResponse, GetProductResponses, GetProductSummaryData, GetSearchFilterLiveProductsData, GetSearchFilterLiveProductsError, GetSearchFilterLiveProductsErrors, GetSearchFilterLiveProductsResponse, GetSearchFilterLiveProductsResponses, GetSearchFilterMatchesData, GetSearchFilterMatchesError, GetSearchFilterMatchesErrors, GetSearchFilterMatchesResponse, GetSearchFilterMatchesResponses, GetShopByIdData, GetShopByIdError, GetShopByIdErrors, GetShopByIdResponse, GetShopByIdResponses, GetShopBySlugData, GetShopBySlugError, GetShopBySlugErrors, GetShopBySlugResponse, GetShopBySlugResponses, GetShopData, GetSimilarProductsData, GetSimilarProductsError, GetSimilarProductsErrors, GetSimilarProductsResponse, GetSimilarProductsResponses, GetUserAccountData, GetUserAccountData2, GetUserAccountError, GetUserAccountErrors, GetUserAccountResponse, GetUserAccountResponses, GetUserSearchFilterData, GetUserSearchFilterError, GetUserSearchFilterErrors, GetUserSearchFilterResponse, GetUserSearchFilterResponses, GetUserSearchFiltersData, GetUserSearchFiltersError, GetUserSearchFiltersErrors, GetUserSearchFiltersResponse, GetUserSearchFiltersResponses, GetWatchlistProductsData, GetWatchlistProductsError, GetWatchlistProductsErrors, GetWatchlistProductsResponse, GetWatchlistProductsResponses, LanguageData, LocalizedTextData, NotificationCollectionData, NotificationPayloadData, NotificationUserStateData, PartnerApplicationNotificationPayloadData, PartnerApplicationPayloadData, PartnerProductEnqueueFailuresResponse, PartnerShopApiKeyResponse, PartnerShopApplicationDecisionData, PartnerShopApplicationStateData, PatchAdminUserData, PatchAllNotificationsData, PatchAllNotificationsError, PatchAllNotificationsErrors, PatchAllNotificationsResponse, PatchAllNotificationsResponses, PatchNotificationData, PatchNotificationData2, PatchNotificationError, PatchNotificationErrors, PatchNotificationResponse, PatchNotificationResponses, PatchPartnerApplicationData, PatchPartnerApplicationError, PatchPartnerApplicationErrors, PatchPartnerApplicationResponse, PatchPartnerApplicationResponses, PatchPartnerProductsData, PatchPartnerProductsError, PatchPartnerProductsErrors, PatchPartnerProductsResponse, PatchPartnerProductsResponses, PatchPartnerShopApplicationData, PatchProductData, PatchProductSearchData, PatchResourceStateData, PatchShopByIdData, PatchShopByIdError, PatchShopByIdErrors, PatchShopByIdResponse, PatchShopByIdResponses, PatchShopData, PatchShopDataWritable, PatchUserAccountData, PatchUserSearchFilterData, PatchUserSearchFilterMatchData, PatchWatchlistProductData, PatchWatchlistProductError, PatchWatchlistProductErrors, PatchWatchlistProductResponse, PatchWatchlistProductResponses, PeriodSearchData, PersonalizedGetProductData, PersonalizedGetProductSummaryData, PersonalizedProductSearchResultData, PostBillingCheckoutData, PostBillingCheckoutData2, PostBillingCheckoutError, PostBillingCheckoutErrors, PostBillingCheckoutResponse, PostBillingCheckoutResponses, PostBillingManageData, PostBillingManageError, PostBillingManageErrors, PostBillingManageResponse, PostBillingManageResponses, PostBillingPortalData, PostBillingPortalError, PostBillingPortalErrors, PostBillingPortalResponse, PostBillingPortalResponses, PostPartnerApplicationData, PostPartnerApplicationError, PostPartnerApplicationErrors, PostPartnerApplicationResponse, PostPartnerApplicationResponses, PostPartnerProductsData, PostPartnerProductsError, PostPartnerProductsErrors, PostPartnerProductsResponse, PostPartnerProductsResponses, PostPartnerShopApplicationDecisionData, PostPartnerShopApplicationPayloadData, PostProductData, PostShopData, PostShopData2, PostShopDataWritable, PostShopError, PostShopErrors, PostShopResponse, PostShopResponses, PostUserSearchFilterData, PostWoocommerceWebhookData, PostWoocommerceWebhookError, PostWoocommerceWebhookErrors, PostWoocommerceWebhookResponses, PriceChangeWatchlistPayloadData, PriceData, PriceEstimateData, PricingData, ProductCreatedEventPayloadData, ProductEventAuctionTimeChangedPayloadData, ProductEventEstimatePriceChangedPayloadData, ProductEventImagesChangedPayloadData, ProductEventPayloadData, ProductEventPriceChangedPayloadData, ProductEventStateChangedPayloadData, ProductEventTypeData, ProductEventUrlChangedPayloadData, ProductImageData, ProductKeyData, ProductSearchData, ProductStateData, ProductUserStateData, ProhibitedContentData, ProhibitedContentUserStateData, PutNewsletterSubscriptionData, PutNewsletterSubscriptionData2, PutNewsletterSubscriptionError, PutNewsletterSubscriptionErrors, PutNewsletterSubscriptionResponse, PutNewsletterSubscriptionResponses, PutPartnerProductsData, PutPartnerProductsError, PutPartnerProductsErrors, PutPartnerProductsResponse, PutPartnerProductsResponses, PutProductData, PutShopApiKeyData, PutShopApiKeyError, PutShopApiKeyErrors, PutShopApiKeyResponse, PutShopApiKeyResponses, RangeQueryDateTime, RangeQueryInt32, RangeQueryUInt64, RejectedPartnerApplicationPayloadData, ResourceStateData, SearchCategoriesData, SearchCategoriesError, SearchCategoriesErrors, SearchCategoriesResponse, SearchCategoriesResponses, SearchFilterMatchProductCollectionData, SearchFilterNotificationPayloadData, SearchFilterPayloadData, SearchFilterProductMatchData, SearchFilterUserStateData, SearchPeriodsData, SearchPeriodsError, SearchPeriodsErrors, SearchPeriodsResponse, SearchPeriodsResponses, SearchShopsData, SearchShopsError, SearchShopsErrors, SearchShopsResponse, SearchShopsResponses, ShopPartnerStatusData, ShopSearchData, ShopSearchResultData, ShopTypeData, SimpleSearchProductsData, SimpleSearchProductsError, SimpleSearchProductsErrors, SimpleSearchProductsResponse, SimpleSearchProductsResponses, SimpleSearchShopsData, SimpleSearchShopsError, SimpleSearchShopsErrors, SimpleSearchShopsResponse, SimpleSearchShopsResponses, SortCategoryFieldData, SortPeriodFieldData, SortProductFieldData, SortSearchFilterMatchFieldData, SortShopFieldData, SortUserFieldData, SortUserSearchFilterFieldData, SortWatchlistProductFieldData, StateChangeWatchlistPayloadData, StructuredAddressData, UpdateSearchFilterMatchFeedbackData, UpdateSearchFilterMatchFeedbackError, UpdateSearchFilterMatchFeedbackErrors, UpdateSearchFilterMatchFeedbackResponse, UpdateSearchFilterMatchFeedbackResponses, UpdateUserAccountData, UpdateUserAccountError, UpdateUserAccountErrors, UpdateUserAccountResponse, UpdateUserAccountResponses, UpdateUserSearchFilterData, UpdateUserSearchFilterError, UpdateUserSearchFilterErrors, UpdateUserSearchFilterResponse, UpdateUserSearchFilterResponses, UserCollectionData, UserRoleData, UserSearchData, UserSearchFilterCollectionData, UserSearchFilterData, UserTierData, WatchlistCollectionData, WatchlistNotificationPayloadData, WatchlistPayloadData, WatchlistProductPatch, WatchlistUserStateData, WoocommerceProductWebhookDeleteData, WoocommerceProductWebhookImageData, WoocommerceProductWebhookUpsertData } from './types.gen'; +export { addWatchlistProduct, adminDeleteUser, adminGetPartnerApplication, adminGetPartnerApplications, adminGetUser, adminPatchPartnerApplication, adminPatchUser, adminPostPartnerApplicationDecision, adminSearchUsers, complexSearchProducts, createUserSearchFilter, deleteAllNotifications, deleteMyAccessToken, deleteNotification, deleteOAuthClient, deletePartnerApplication, deleteUser, deleteUserSearchFilter, deleteWatchlistProduct, getCategories, getCategoryById, getMyAccessToken, getMyAccessTokens, getMyPartnerShops, getNotifications, getOAuthClient, getOAuthClients, getPartnerApplication, getPartnerApplications, getPeriodById, getPeriods, getProduct, getProductBySlug, getProductHistory, getSearchFilterLiveProducts, getSearchFilterMatches, getShopById, getShopBySlug, getSimilarProducts, getUserAccount, getUserSearchFilter, getUserSearchFilters, getWatchlistProducts, oauthAuthorize, oauthIntrospect, oauthRevoke, oauthToken, type Options, patchAllNotifications, patchMyAccessToken, patchNotification, patchOAuthClient, patchPartnerApplication, patchPartnerProducts, patchShopById, patchWatchlistProduct, postBillingCheckout, postBillingManage, postBillingPortal, postMyAccessToken, postOAuthClient, postPartnerApplication, postPartnerProducts, postShop, postWoocommerceWebhook, putNewsletterSubscription, putPartnerProducts, searchCategories, searchPeriods, searchShops, simpleSearchProducts, simpleSearchShops, updateSearchFilterMatchFeedback, updateUserAccount, updateUserSearchFilter } from './sdk.gen'; +export type { AccessTokenScopeData, AccessTokenTypeData, AddWatchlistProductData, AddWatchlistProductError, AddWatchlistProductErrors, AddWatchlistProductResponse, AddWatchlistProductResponses, AdminDeleteUserData, AdminDeleteUserError, AdminDeleteUserErrors, AdminDeleteUserResponse, AdminDeleteUserResponses, AdminGetPartnerApplicationData, AdminGetPartnerApplicationError, AdminGetPartnerApplicationErrors, AdminGetPartnerApplicationResponse, AdminGetPartnerApplicationResponses, AdminGetPartnerApplicationsData, AdminGetPartnerApplicationsError, AdminGetPartnerApplicationsErrors, AdminGetPartnerApplicationsResponse, AdminGetPartnerApplicationsResponses, AdminGetUserData, AdminGetUserError, AdminGetUserErrors, AdminGetUserResponse, AdminGetUserResponses, AdminPatchPartnerApplicationData, AdminPatchPartnerApplicationError, AdminPatchPartnerApplicationErrors, AdminPatchPartnerApplicationResponse, AdminPatchPartnerApplicationResponses, AdminPatchPartnerShopApplicationData, AdminPatchUserData, AdminPatchUserError, AdminPatchUserErrors, AdminPatchUserResponse, AdminPatchUserResponses, AdminPostPartnerApplicationDecisionData, AdminPostPartnerApplicationDecisionError, AdminPostPartnerApplicationDecisionErrors, AdminPostPartnerApplicationDecisionResponse, AdminPostPartnerApplicationDecisionResponses, AdminSearchUsersData, AdminSearchUsersError, AdminSearchUsersErrors, AdminSearchUsersResponse, AdminSearchUsersResponses, ApiError, ApiErrorSource, ApprovedPartnerApplicationPayloadData, AuctionData, BillingCycleData, BillingPlanData, BillingSessionUrlData, CategorySearchData, ClientOptions, ComplexSearchProductsData, ComplexSearchProductsError, ComplexSearchProductsErrors, ComplexSearchProductsResponse, ComplexSearchProductsResponses, ContinentData, CountryCodeData, CreateUserSearchFilterData, CreateUserSearchFilterError, CreateUserSearchFilterErrors, CreateUserSearchFilterResponse, CreateUserSearchFilterResponses, CurrencyData, DeleteAllNotificationsData, DeleteAllNotificationsError, DeleteAllNotificationsErrors, DeleteAllNotificationsResponse, DeleteAllNotificationsResponses, DeleteMyAccessTokenData, DeleteMyAccessTokenError, DeleteMyAccessTokenErrors, DeleteMyAccessTokenResponse, DeleteMyAccessTokenResponses, DeleteNotificationData, DeleteNotificationError, DeleteNotificationErrors, DeleteNotificationResponse, DeleteNotificationResponses, DeleteOAuthClientData, DeleteOAuthClientError, DeleteOAuthClientErrors, DeleteOAuthClientResponse, DeleteOAuthClientResponses, DeletePartnerApplicationData, DeletePartnerApplicationError, DeletePartnerApplicationErrors, DeletePartnerApplicationResponse, DeletePartnerApplicationResponses, DeleteUserData, DeleteUserError, DeleteUserErrors, DeleteUserResponse, DeleteUserResponses, DeleteUserSearchFilterData, DeleteUserSearchFilterError, DeleteUserSearchFilterErrors, DeleteUserSearchFilterResponse, DeleteUserSearchFilterResponses, DeleteWatchlistProductData, DeleteWatchlistProductError, DeleteWatchlistProductErrors, DeleteWatchlistProductResponse, DeleteWatchlistProductResponses, DistanceData, DistanceUnitData, ExecutionStateData, GeoAddressData, GeoDistanceQueryData, GetAccessTokenData, GetCategoriesData, GetCategoriesError, GetCategoriesErrors, GetCategoriesResponse, GetCategoriesResponses, GetCategoryByIdData, GetCategoryByIdError, GetCategoryByIdErrors, GetCategoryByIdResponse, GetCategoryByIdResponses, GetCategoryData, GetCategorySummaryData, GetMyAccessTokenData, GetMyAccessTokenError, GetMyAccessTokenErrors, GetMyAccessTokenResponse, GetMyAccessTokenResponses, GetMyAccessTokensData, GetMyAccessTokensError, GetMyAccessTokensErrors, GetMyAccessTokensResponse, GetMyAccessTokensResponses, GetMyPartnerShopsData, GetMyPartnerShopsError, GetMyPartnerShopsErrors, GetMyPartnerShopsResponse, GetMyPartnerShopsResponses, GetNotificationData, GetNotificationsData, GetNotificationsError, GetNotificationsErrors, GetNotificationsResponse, GetNotificationsResponses, GetOAuthClientData, GetOAuthClientError, GetOAuthClientErrors, GetOAuthClientResponse, GetOAuthClientResponses, GetOAuthClientsData, GetOAuthClientsError, GetOAuthClientsErrors, GetOAuthClientsResponse, GetOAuthClientsResponses, GetPartnerApplicationData, GetPartnerApplicationError, GetPartnerApplicationErrors, GetPartnerApplicationResponse, GetPartnerApplicationResponses, GetPartnerApplicationsData, GetPartnerApplicationsError, GetPartnerApplicationsErrors, GetPartnerApplicationsResponse, GetPartnerApplicationsResponses, GetPartnerShopApplicationData, GetPartnerShopApplicationPayloadData, GetPeriodByIdData, GetPeriodByIdError, GetPeriodByIdErrors, GetPeriodByIdResponse, GetPeriodByIdResponses, GetPeriodData, GetPeriodsData, GetPeriodsError, GetPeriodsErrors, GetPeriodsResponse, GetPeriodsResponses, GetPeriodSummaryData, GetProductBySlugData, GetProductBySlugError, GetProductBySlugErrors, GetProductBySlugResponse, GetProductBySlugResponses, GetProductData, GetProductData2, GetProductError, GetProductErrors, GetProductEventData, GetProductHistoryData, GetProductHistoryError, GetProductHistoryErrors, GetProductHistoryResponse, GetProductHistoryResponses, GetProductResponse, GetProductResponses, GetProductSummaryData, GetSearchFilterLiveProductsData, GetSearchFilterLiveProductsError, GetSearchFilterLiveProductsErrors, GetSearchFilterLiveProductsResponse, GetSearchFilterLiveProductsResponses, GetSearchFilterMatchesData, GetSearchFilterMatchesError, GetSearchFilterMatchesErrors, GetSearchFilterMatchesResponse, GetSearchFilterMatchesResponses, GetShopByIdData, GetShopByIdError, GetShopByIdErrors, GetShopByIdResponse, GetShopByIdResponses, GetShopBySlugData, GetShopBySlugError, GetShopBySlugErrors, GetShopBySlugResponse, GetShopBySlugResponses, GetShopData, GetSimilarProductsData, GetSimilarProductsError, GetSimilarProductsErrors, GetSimilarProductsResponse, GetSimilarProductsResponses, GetUserAccountData, GetUserAccountData2, GetUserAccountError, GetUserAccountErrors, GetUserAccountResponse, GetUserAccountResponses, GetUserSearchFilterData, GetUserSearchFilterError, GetUserSearchFilterErrors, GetUserSearchFilterResponse, GetUserSearchFilterResponses, GetUserSearchFiltersData, GetUserSearchFiltersError, GetUserSearchFiltersErrors, GetUserSearchFiltersResponse, GetUserSearchFiltersResponses, GetWatchlistProductsData, GetWatchlistProductsError, GetWatchlistProductsErrors, GetWatchlistProductsResponse, GetWatchlistProductsResponses, LanguageData, LocalizedTextData, NotificationCollectionData, NotificationPayloadData, NotificationUserStateData, OauthAuthorizeData, OauthAuthorizeError, OauthAuthorizeErrors, OAuthClientMetadataPatchData, OAuthClientMetadataRequestData, OAuthClientMetadataResponseData, OauthIntrospectData, OauthIntrospectError, OauthIntrospectErrors, OAuthIntrospectionResponseData, OauthIntrospectResponse, OauthIntrospectResponses, OauthRevokeData, OauthRevokeError, OauthRevokeErrors, OauthRevokeResponses, OauthTokenData, OauthTokenError, OauthTokenErrors, OauthTokenResponse, OAuthTokenResponseData, OauthTokenResponses, PartnerApplicationNotificationPayloadData, PartnerApplicationPayloadData, PartnerProductEnqueueFailuresResponse, PartnerShopApplicationDecisionData, PartnerShopApplicationStateData, PatchAccessTokenData, PatchAdminUserData, PatchAllNotificationsData, PatchAllNotificationsError, PatchAllNotificationsErrors, PatchAllNotificationsResponse, PatchAllNotificationsResponses, PatchMyAccessTokenData, PatchMyAccessTokenError, PatchMyAccessTokenErrors, PatchMyAccessTokenResponse, PatchMyAccessTokenResponses, PatchNotificationData, PatchNotificationData2, PatchNotificationError, PatchNotificationErrors, PatchNotificationResponse, PatchNotificationResponses, PatchOAuthClientData, PatchOAuthClientError, PatchOAuthClientErrors, PatchOAuthClientResponse, PatchOAuthClientResponses, PatchPartnerApplicationData, PatchPartnerApplicationError, PatchPartnerApplicationErrors, PatchPartnerApplicationResponse, PatchPartnerApplicationResponses, PatchPartnerProductsData, PatchPartnerProductsError, PatchPartnerProductsErrors, PatchPartnerProductsResponse, PatchPartnerProductsResponses, PatchPartnerShopApplicationData, PatchProductData, PatchProductSearchData, PatchResourceStateData, PatchShopByIdData, PatchShopByIdError, PatchShopByIdErrors, PatchShopByIdResponse, PatchShopByIdResponses, PatchShopData, PatchShopDataWritable, PatchUserAccountData, PatchUserSearchFilterData, PatchUserSearchFilterMatchData, PatchWatchlistProductData, PatchWatchlistProductError, PatchWatchlistProductErrors, PatchWatchlistProductResponse, PatchWatchlistProductResponses, PeriodSearchData, PersonalizedGetProductData, PersonalizedGetProductSummaryData, PersonalizedProductSearchResultData, PostAccessTokenData, PostBillingCheckoutData, PostBillingCheckoutData2, PostBillingCheckoutError, PostBillingCheckoutErrors, PostBillingCheckoutResponse, PostBillingCheckoutResponses, PostBillingManageData, PostBillingManageError, PostBillingManageErrors, PostBillingManageResponse, PostBillingManageResponses, PostBillingPortalData, PostBillingPortalError, PostBillingPortalErrors, PostBillingPortalResponse, PostBillingPortalResponses, PostMyAccessTokenData, PostMyAccessTokenError, PostMyAccessTokenErrors, PostMyAccessTokenResponse, PostMyAccessTokenResponses, PostOAuthClientData, PostOAuthClientError, PostOAuthClientErrors, PostOAuthClientResponse, PostOAuthClientResponses, PostPartnerApplicationData, PostPartnerApplicationError, PostPartnerApplicationErrors, PostPartnerApplicationResponse, PostPartnerApplicationResponses, PostPartnerProductsData, PostPartnerProductsError, PostPartnerProductsErrors, PostPartnerProductsResponse, PostPartnerProductsResponses, PostPartnerShopApplicationDecisionData, PostPartnerShopApplicationPayloadData, PostProductData, PostShopData, PostShopData2, PostShopDataWritable, PostShopError, PostShopErrors, PostShopResponse, PostShopResponses, PostUserSearchFilterData, PostWoocommerceWebhookData, PostWoocommerceWebhookError, PostWoocommerceWebhookErrors, PostWoocommerceWebhookResponses, PriceChangeWatchlistPayloadData, PriceData, PriceEstimateData, PricingData, ProductCreatedEventPayloadData, ProductEventAuctionTimeChangedPayloadData, ProductEventEstimatePriceChangedPayloadData, ProductEventImagesChangedPayloadData, ProductEventPayloadData, ProductEventPriceChangedPayloadData, ProductEventStateChangedPayloadData, ProductEventTypeData, ProductEventUrlChangedPayloadData, ProductImageData, ProductKeyData, ProductSearchData, ProductStateData, ProductUserStateData, ProhibitedContentData, ProhibitedContentUserStateData, PutNewsletterSubscriptionData, PutNewsletterSubscriptionData2, PutNewsletterSubscriptionError, PutNewsletterSubscriptionErrors, PutNewsletterSubscriptionResponse, PutNewsletterSubscriptionResponses, PutPartnerProductsData, PutPartnerProductsError, PutPartnerProductsErrors, PutPartnerProductsResponse, PutPartnerProductsResponses, PutProductData, RangeQueryDateTime, RangeQueryInt32, RangeQueryUInt64, RejectedPartnerApplicationPayloadData, ResourceStateData, SearchCategoriesData, SearchCategoriesError, SearchCategoriesErrors, SearchCategoriesResponse, SearchCategoriesResponses, SearchFilterMatchProductCollectionData, SearchFilterNotificationPayloadData, SearchFilterPayloadData, SearchFilterProductMatchData, SearchFilterUserStateData, SearchPeriodsData, SearchPeriodsError, SearchPeriodsErrors, SearchPeriodsResponse, SearchPeriodsResponses, SearchShopsData, SearchShopsError, SearchShopsErrors, SearchShopsResponse, SearchShopsResponses, ShopPartnerStatusData, ShopSearchData, ShopSearchResultData, ShopTypeData, SimpleSearchProductsData, SimpleSearchProductsError, SimpleSearchProductsErrors, SimpleSearchProductsResponse, SimpleSearchProductsResponses, SimpleSearchShopsData, SimpleSearchShopsError, SimpleSearchShopsErrors, SimpleSearchShopsResponse, SimpleSearchShopsResponses, SortCategoryFieldData, SortPeriodFieldData, SortProductFieldData, SortSearchFilterMatchFieldData, SortShopFieldData, SortUserFieldData, SortUserSearchFilterFieldData, SortWatchlistProductFieldData, StateChangeWatchlistPayloadData, StructuredAddressData, UpdateSearchFilterMatchFeedbackData, UpdateSearchFilterMatchFeedbackError, UpdateSearchFilterMatchFeedbackErrors, UpdateSearchFilterMatchFeedbackResponse, UpdateSearchFilterMatchFeedbackResponses, UpdateUserAccountData, UpdateUserAccountError, UpdateUserAccountErrors, UpdateUserAccountResponse, UpdateUserAccountResponses, UpdateUserSearchFilterData, UpdateUserSearchFilterError, UpdateUserSearchFilterErrors, UpdateUserSearchFilterResponse, UpdateUserSearchFilterResponses, UserCollectionData, UserRoleData, UserSearchData, UserSearchFilterCollectionData, UserSearchFilterData, UserTierData, WatchlistCollectionData, WatchlistNotificationPayloadData, WatchlistPayloadData, WatchlistProductPatch, WatchlistUserStateData, WoocommerceProductWebhookDeleteData, WoocommerceProductWebhookImageData, WoocommerceProductWebhookUpsertData } from './types.gen'; diff --git a/src/client/sdk.gen.ts b/src/client/sdk.gen.ts index 2a6bb87c..b910e24c 100644 --- a/src/client/sdk.gen.ts +++ b/src/client/sdk.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as Options2, TDataShape } from './client'; +import { type Client, type Options as Options2, type TDataShape, urlSearchParamsBodySerializer } from './client'; import { client } from './client.gen'; -import type { AddWatchlistProductData, AddWatchlistProductErrors, AddWatchlistProductResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetPartnerApplicationData, AdminGetPartnerApplicationErrors, AdminGetPartnerApplicationResponses, AdminGetPartnerApplicationsData, AdminGetPartnerApplicationsErrors, AdminGetPartnerApplicationsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminPatchPartnerApplicationData, AdminPatchPartnerApplicationErrors, AdminPatchPartnerApplicationResponses, AdminPatchUserData, AdminPatchUserErrors, AdminPatchUserResponses, AdminPostPartnerApplicationDecisionData, AdminPostPartnerApplicationDecisionErrors, AdminPostPartnerApplicationDecisionResponses, AdminSearchUsersData, AdminSearchUsersErrors, AdminSearchUsersResponses, ComplexSearchProductsData, ComplexSearchProductsErrors, ComplexSearchProductsResponses, CreateUserSearchFilterData, CreateUserSearchFilterErrors, CreateUserSearchFilterResponses, DeleteAllNotificationsData, DeleteAllNotificationsErrors, DeleteAllNotificationsResponses, DeleteNotificationData, DeleteNotificationErrors, DeleteNotificationResponses, DeletePartnerApplicationData, DeletePartnerApplicationErrors, DeletePartnerApplicationResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, DeleteUserSearchFilterData, DeleteUserSearchFilterErrors, DeleteUserSearchFilterResponses, DeleteWatchlistProductData, DeleteWatchlistProductErrors, DeleteWatchlistProductResponses, GetCategoriesData, GetCategoriesErrors, GetCategoriesResponses, GetCategoryByIdData, GetCategoryByIdErrors, GetCategoryByIdResponses, GetNotificationsData, GetNotificationsErrors, GetNotificationsResponses, GetPartnerApplicationData, GetPartnerApplicationErrors, GetPartnerApplicationResponses, GetPartnerApplicationsData, GetPartnerApplicationsErrors, GetPartnerApplicationsResponses, GetPartnerShopsData, GetPartnerShopsErrors, GetPartnerShopsResponses, GetPeriodByIdData, GetPeriodByIdErrors, GetPeriodByIdResponses, GetPeriodsData, GetPeriodsErrors, GetPeriodsResponses, GetProductBySlugData, GetProductBySlugErrors, GetProductBySlugResponses, GetProductData2, GetProductErrors, GetProductHistoryData, GetProductHistoryErrors, GetProductHistoryResponses, GetProductResponses, GetSearchFilterLiveProductsData, GetSearchFilterLiveProductsErrors, GetSearchFilterLiveProductsResponses, GetSearchFilterMatchesData, GetSearchFilterMatchesErrors, GetSearchFilterMatchesResponses, GetShopByIdData, GetShopByIdErrors, GetShopByIdResponses, GetShopBySlugData, GetShopBySlugErrors, GetShopBySlugResponses, GetSimilarProductsData, GetSimilarProductsErrors, GetSimilarProductsResponses, GetUserAccountData2, GetUserAccountErrors, GetUserAccountResponses, GetUserSearchFilterData, GetUserSearchFilterErrors, GetUserSearchFilterResponses, GetUserSearchFiltersData, GetUserSearchFiltersErrors, GetUserSearchFiltersResponses, GetWatchlistProductsData, GetWatchlistProductsErrors, GetWatchlistProductsResponses, PatchAllNotificationsData, PatchAllNotificationsErrors, PatchAllNotificationsResponses, PatchNotificationData2, PatchNotificationErrors, PatchNotificationResponses, PatchPartnerApplicationData, PatchPartnerApplicationErrors, PatchPartnerApplicationResponses, PatchPartnerProductsData, PatchPartnerProductsErrors, PatchPartnerProductsResponses, PatchShopByIdData, PatchShopByIdErrors, PatchShopByIdResponses, PatchWatchlistProductData, PatchWatchlistProductErrors, PatchWatchlistProductResponses, PostBillingCheckoutData2, PostBillingCheckoutErrors, PostBillingCheckoutResponses, PostBillingManageData, PostBillingManageErrors, PostBillingManageResponses, PostBillingPortalData, PostBillingPortalErrors, PostBillingPortalResponses, PostPartnerApplicationData, PostPartnerApplicationErrors, PostPartnerApplicationResponses, PostPartnerProductsData, PostPartnerProductsErrors, PostPartnerProductsResponses, PostShopData2, PostShopErrors, PostShopResponses, PostWoocommerceWebhookData, PostWoocommerceWebhookErrors, PostWoocommerceWebhookResponses, PutNewsletterSubscriptionData2, PutNewsletterSubscriptionErrors, PutNewsletterSubscriptionResponses, PutPartnerProductsData, PutPartnerProductsErrors, PutPartnerProductsResponses, PutShopApiKeyData, PutShopApiKeyErrors, PutShopApiKeyResponses, SearchCategoriesData, SearchCategoriesErrors, SearchCategoriesResponses, SearchPeriodsData, SearchPeriodsErrors, SearchPeriodsResponses, SearchShopsData, SearchShopsErrors, SearchShopsResponses, SimpleSearchProductsData, SimpleSearchProductsErrors, SimpleSearchProductsResponses, SimpleSearchShopsData, SimpleSearchShopsErrors, SimpleSearchShopsResponses, UpdateSearchFilterMatchFeedbackData, UpdateSearchFilterMatchFeedbackErrors, UpdateSearchFilterMatchFeedbackResponses, UpdateUserAccountData, UpdateUserAccountErrors, UpdateUserAccountResponses, UpdateUserSearchFilterData, UpdateUserSearchFilterErrors, UpdateUserSearchFilterResponses } from './types.gen'; +import type { AddWatchlistProductData, AddWatchlistProductErrors, AddWatchlistProductResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetPartnerApplicationData, AdminGetPartnerApplicationErrors, AdminGetPartnerApplicationResponses, AdminGetPartnerApplicationsData, AdminGetPartnerApplicationsErrors, AdminGetPartnerApplicationsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminPatchPartnerApplicationData, AdminPatchPartnerApplicationErrors, AdminPatchPartnerApplicationResponses, AdminPatchUserData, AdminPatchUserErrors, AdminPatchUserResponses, AdminPostPartnerApplicationDecisionData, AdminPostPartnerApplicationDecisionErrors, AdminPostPartnerApplicationDecisionResponses, AdminSearchUsersData, AdminSearchUsersErrors, AdminSearchUsersResponses, ComplexSearchProductsData, ComplexSearchProductsErrors, ComplexSearchProductsResponses, CreateUserSearchFilterData, CreateUserSearchFilterErrors, CreateUserSearchFilterResponses, DeleteAllNotificationsData, DeleteAllNotificationsErrors, DeleteAllNotificationsResponses, DeleteMyAccessTokenData, DeleteMyAccessTokenErrors, DeleteMyAccessTokenResponses, DeleteNotificationData, DeleteNotificationErrors, DeleteNotificationResponses, DeleteOAuthClientData, DeleteOAuthClientErrors, DeleteOAuthClientResponses, DeletePartnerApplicationData, DeletePartnerApplicationErrors, DeletePartnerApplicationResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, DeleteUserSearchFilterData, DeleteUserSearchFilterErrors, DeleteUserSearchFilterResponses, DeleteWatchlistProductData, DeleteWatchlistProductErrors, DeleteWatchlistProductResponses, GetCategoriesData, GetCategoriesErrors, GetCategoriesResponses, GetCategoryByIdData, GetCategoryByIdErrors, GetCategoryByIdResponses, GetMyAccessTokenData, GetMyAccessTokenErrors, GetMyAccessTokenResponses, GetMyAccessTokensData, GetMyAccessTokensErrors, GetMyAccessTokensResponses, GetMyPartnerShopsData, GetMyPartnerShopsErrors, GetMyPartnerShopsResponses, GetNotificationsData, GetNotificationsErrors, GetNotificationsResponses, GetOAuthClientData, GetOAuthClientErrors, GetOAuthClientResponses, GetOAuthClientsData, GetOAuthClientsErrors, GetOAuthClientsResponses, GetPartnerApplicationData, GetPartnerApplicationErrors, GetPartnerApplicationResponses, GetPartnerApplicationsData, GetPartnerApplicationsErrors, GetPartnerApplicationsResponses, GetPeriodByIdData, GetPeriodByIdErrors, GetPeriodByIdResponses, GetPeriodsData, GetPeriodsErrors, GetPeriodsResponses, GetProductBySlugData, GetProductBySlugErrors, GetProductBySlugResponses, GetProductData2, GetProductErrors, GetProductHistoryData, GetProductHistoryErrors, GetProductHistoryResponses, GetProductResponses, GetSearchFilterLiveProductsData, GetSearchFilterLiveProductsErrors, GetSearchFilterLiveProductsResponses, GetSearchFilterMatchesData, GetSearchFilterMatchesErrors, GetSearchFilterMatchesResponses, GetShopByIdData, GetShopByIdErrors, GetShopByIdResponses, GetShopBySlugData, GetShopBySlugErrors, GetShopBySlugResponses, GetSimilarProductsData, GetSimilarProductsErrors, GetSimilarProductsResponses, GetUserAccountData2, GetUserAccountErrors, GetUserAccountResponses, GetUserSearchFilterData, GetUserSearchFilterErrors, GetUserSearchFilterResponses, GetUserSearchFiltersData, GetUserSearchFiltersErrors, GetUserSearchFiltersResponses, GetWatchlistProductsData, GetWatchlistProductsErrors, GetWatchlistProductsResponses, OauthAuthorizeData, OauthAuthorizeErrors, OauthIntrospectData, OauthIntrospectErrors, OauthIntrospectResponses, OauthRevokeData, OauthRevokeErrors, OauthRevokeResponses, OauthTokenData, OauthTokenErrors, OauthTokenResponses, PatchAllNotificationsData, PatchAllNotificationsErrors, PatchAllNotificationsResponses, PatchMyAccessTokenData, PatchMyAccessTokenErrors, PatchMyAccessTokenResponses, PatchNotificationData2, PatchNotificationErrors, PatchNotificationResponses, PatchOAuthClientData, PatchOAuthClientErrors, PatchOAuthClientResponses, PatchPartnerApplicationData, PatchPartnerApplicationErrors, PatchPartnerApplicationResponses, PatchPartnerProductsData, PatchPartnerProductsErrors, PatchPartnerProductsResponses, PatchShopByIdData, PatchShopByIdErrors, PatchShopByIdResponses, PatchWatchlistProductData, PatchWatchlistProductErrors, PatchWatchlistProductResponses, PostBillingCheckoutData2, PostBillingCheckoutErrors, PostBillingCheckoutResponses, PostBillingManageData, PostBillingManageErrors, PostBillingManageResponses, PostBillingPortalData, PostBillingPortalErrors, PostBillingPortalResponses, PostMyAccessTokenData, PostMyAccessTokenErrors, PostMyAccessTokenResponses, PostOAuthClientData, PostOAuthClientErrors, PostOAuthClientResponses, PostPartnerApplicationData, PostPartnerApplicationErrors, PostPartnerApplicationResponses, PostPartnerProductsData, PostPartnerProductsErrors, PostPartnerProductsResponses, PostShopData2, PostShopErrors, PostShopResponses, PostWoocommerceWebhookData, PostWoocommerceWebhookErrors, PostWoocommerceWebhookResponses, PutNewsletterSubscriptionData2, PutNewsletterSubscriptionErrors, PutNewsletterSubscriptionResponses, PutPartnerProductsData, PutPartnerProductsErrors, PutPartnerProductsResponses, SearchCategoriesData, SearchCategoriesErrors, SearchCategoriesResponses, SearchPeriodsData, SearchPeriodsErrors, SearchPeriodsResponses, SearchShopsData, SearchShopsErrors, SearchShopsResponses, SimpleSearchProductsData, SimpleSearchProductsErrors, SimpleSearchProductsResponses, SimpleSearchShopsData, SimpleSearchShopsErrors, SimpleSearchShopsResponses, UpdateSearchFilterMatchFeedbackData, UpdateSearchFilterMatchFeedbackErrors, UpdateSearchFilterMatchFeedbackResponses, UpdateUserAccountData, UpdateUserAccountErrors, UpdateUserAccountResponses, UpdateUserSearchFilterData, UpdateUserSearchFilterErrors, UpdateUserSearchFilterResponses } from './types.gen'; export type Options = Options2 & { /** @@ -21,9 +21,12 @@ export type Options(options: Options) => (options.client ?? client).patch({ - security: [{ name: 'x-api-key', type: 'apiKey' }], + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], url: '/api/v1/shops/{shopId}/products', ...options, headers: { @@ -49,9 +52,12 @@ export const patchPartnerProducts = (optio /** * Batch create products (Partner API) * - * Creates one or more products for a shop using API key authentication. - * This endpoint is intended for partner shops — shops that have been granted partner status - * and have an API key configured. It does **not** use Cognito JWT authentication. + * Creates one or more products for a shop using bearer authentication. + * This endpoint is intended for partner shops and accepts: + * - a Cognito bearer token for the partner user linked to the shop, or + * - an Aura Historia access token owned by that partner user. + * + * Aura Historia access tokens on this endpoint must include the `products:write` scope. * * The request body is an array of `PostProductData` objects. Each entry is forwarded * individually to the asynchronous partner-product ingestion queue. @@ -62,7 +68,7 @@ export const patchPartnerProducts = (optio * */ export const postPartnerProducts = (options: Options) => (options.client ?? client).post({ - security: [{ name: 'x-api-key', type: 'apiKey' }], + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], url: '/api/v1/shops/{shopId}/products', ...options, headers: { @@ -75,15 +81,28 @@ export const postPartnerProducts = (option * Batch upsert products (Partner API) * * Creates new products or updates existing ones for a shop in a single batch call, - * using API key authentication. This endpoint is intended for partner shops — shops - * that have been granted partner status and have an API key configured. It does **not** - * use Cognito JWT authentication. + * using bearer authentication. This endpoint is intended for partner shops and accepts: + * - a Cognito bearer token for the partner user linked to the shop, or + * - an Aura Historia access token owned by that partner user. + * + * Aura Historia access tokens on this endpoint must include the `products:write` scope. * * The request body is an array of `PutProductData` objects. Each entry is forwarded * individually to the asynchronous partner-product ingestion queue as an upsert command. * When the queued command is later ingested: - * - **Existing product** — only `state` and `price` are updated (other fields are ignored). - * - **New product** — a full product is created using all provided fields. + * - **New product** — a full product is created using all provided fields. Omitting + * `title`, `url`, or `state` causes the backend to fall back to an empty title, + * a placeholder URL, and `LISTED` respectively. + * - **Existing product** — the backend applies `price`, `priceEstimateMin`, + * `priceEstimateMax`, `state`, `url`, `images`, `auctionStart`, and `auctionEnd`. + * `title`, `description`, `sellerName`, `structuredAddress`, and `geoAddress` + * are ignored on the update path. + * - On the update path, omitting or sending `null` for `price`, `priceEstimateMin`, + * `priceEstimateMax`, `url`, `auctionStart`, or `auctionEnd` leaves the stored value + * unchanged. + * - On the update path, `images` always replaces the stored image set; omitting + * `images` or sending `null` is treated as an empty list and therefore clears all + * stored images. * * The response returns HTTP 202 with an array containing only the `shopsProductId` values * that failed to be forwarded to the queue. An empty array indicates that all upserts were @@ -91,7 +110,7 @@ export const postPartnerProducts = (option * */ export const putPartnerProducts = (options: Options) => (options.client ?? client).put({ - security: [{ name: 'x-api-key', type: 'apiKey' }], + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], url: '/api/v1/shops/{shopId}/products', ...options, headers: { @@ -103,11 +122,13 @@ export const putPartnerProducts = (options /** * Ingest a WooCommerce product webhook (Partner API) * - * Accepts a single WooCommerce product webhook event for a partner shop using API key authentication. - * This endpoint is intended for partner shops and does **not** use Cognito JWT authentication. + * Accepts a single WooCommerce product webhook event for a partner shop using bearer authentication. + * This endpoint is intended for partner shops and accepts: + * - a Cognito bearer token for the partner user linked to the shop, or + * - an Aura Historia access token owned by that partner user. * * The caller must provide: - * - the partner shop API key in the `x-api-key` header, + * - the bearer token in the `Authorization` header, * - the WooCommerce topic in `x-wc-webhook-topic`, and * - the base64-encoded HMAC-SHA256 signature of the raw request body in `x-wc-webhook-signature`. * @@ -121,7 +142,7 @@ export const putPartnerProducts = (options * */ export const postWoocommerceWebhook = (options: Options) => (options.client ?? client).post({ - security: [{ name: 'x-api-key', type: 'apiKey' }], + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], url: '/api/v1/webhooks/woocommerce/{shopId}', ...options, headers: { @@ -572,6 +593,84 @@ export const updateUserAccount = (options: } }); +/** + * List Aura Historia access tokens + * + * Lists the authenticated user's non-expired Aura Historia access tokens. + * The `token` field is masked on this endpoint and does not reveal the plaintext bearer token again. + * Requires valid Cognito JWT authentication. + * + */ +export const getMyAccessTokens = (options?: Options) => (options?.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/me/access-tokens', + ...options +}); + +/** + * Update an Aura Historia access token + * + * Updates metadata for one access token owned by the authenticated user. + * Omitted or `null` optional properties leave the existing value unchanged. + * Requires valid Cognito JWT authentication. + * + */ +export const patchMyAccessToken = (options: Options) => (options.client ?? client).patch({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/me/access-tokens', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Create an Aura Historia access token + * + * Creates a new Aura Historia access token for the authenticated user. + * The plaintext bearer token is returned only in this create response; later reads return a masked token value. + * Requires valid Cognito JWT authentication. + * + */ +export const postMyAccessToken = (options: Options) => (options.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/me/access-tokens', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete an Aura Historia access token + * + * Deletes one access token owned by the authenticated user. + * The token to delete is identified by the required `{accessTokenId}` path parameter. + * Requires valid Cognito JWT authentication. + * + */ +export const deleteMyAccessToken = (options: Options) => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/me/access-tokens/{accessTokenId}', + ...options +}); + +/** + * Get one Aura Historia access token + * + * Retrieves one non-expired Aura Historia access token owned by the authenticated user. + * The `token` field is masked on this endpoint and does not reveal the plaintext bearer token again. + * Requires valid Cognito JWT authentication. + * + */ +export const getMyAccessToken = (options: Options) => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/me/access-tokens/{accessTokenId}', + ...options +}); + /** * Create Stripe checkout session * @@ -775,6 +874,10 @@ export const putNewsletterSubscription = ( * Performs shop search using query parameters instead of a JSON request body. * This is the cache-friendly equivalent of `POST /api/v1/shops/search`. * + * Authentication is optional on this endpoint: + * - unauthenticated requests return cacheable shared responses + * - authenticated Cognito JWT or Aura Historia access-token requests return `Cache-Control: no-store` + * * All optional filters from `ShopSearchData` are supported as query parameters * using the same field names: * - `shopNameQuery` @@ -786,7 +889,11 @@ export const putNewsletterSubscription = ( * - `updated[min]`, `updated[max]` * */ -export const simpleSearchShops = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/shops', ...options }); +export const simpleSearchShops = (options?: Options) => (options?.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], + url: '/api/v1/shops', + ...options +}); /** * Create a shop @@ -815,8 +922,16 @@ export const postShop = (options: Options< * Retrieves detailed information about a specific shop by its shop ID (UUID). * Returns complete shop metadata including name, domains, image, address/contact metadata, and timestamps. * + * Authentication is optional on this endpoint: + * - unauthenticated requests return cacheable shared responses + * - authenticated Cognito JWT or Aura Historia access-token requests return `Cache-Control: no-store` + * */ -export const getShopById = (options: Options) => (options.client ?? client).get({ url: '/api/v1/shops/{shopId}', ...options }); +export const getShopById = (options: Options) => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], + url: '/api/v1/shops/{shopId}', + ...options +}); /** * Update shop details @@ -825,7 +940,7 @@ export const getShopById = (options: Optio * * Requires either: * - a valid Cognito JWT for the partner user assigned to the shop or for an `ADMIN`, or - * - the partner shop's `x-api-key` when no Cognito identity is present. + * - a valid Aura Historia access token owned by the partner user assigned to the shop. * * The request body uses `PatchShopData`. Only fields present in the JSON body are applied; * omitted or `null` fields are left unchanged. The HTTP request body itself must not be absent @@ -833,7 +948,7 @@ export const getShopById = (options: Optio * */ export const patchShopById = (options: Options) => (options.client ?? client).patch({ - security: [{ scheme: 'bearer', type: 'http' }, { name: 'x-api-key', type: 'apiKey' }], + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], url: '/api/v1/shops/{shopId}', ...options, headers: { @@ -842,36 +957,25 @@ export const patchShopById = (options: Opt } }); -/** - * Create or overwrite a partner shop API key - * - * Creates a new partner API key for the specified shop and returns the plaintext key in the response. - * If a key already exists for the shop, it is overwritten. - * - * Requires a valid Cognito JWT. The caller must either: - * - be the partner user assigned to the shop, or - * - have the `ADMIN` role. - * - * The returned key is only available in this response. The backend stores only a hash. - * - */ -export const putShopApiKey = (options: Options) => (options.client ?? client).put({ - security: [{ scheme: 'bearer', type: 'http' }], - url: '/api/v1/shops/{shopId}/api-key', - ...options -}); - /** * Get shop details by slug * * Retrieves detailed information about a specific shop by its human-readable slug identifier. * Returns complete shop metadata including name, domains, image, address/contact metadata, and timestamps. * + * Authentication is optional on this endpoint: + * - unauthenticated requests return cacheable shared responses + * - authenticated Cognito JWT or Aura Historia access-token requests return `Cache-Control: no-store` + * * **Human-Readable Identifiers**: This endpoint uses slug-based identifiers which are human-readable * kebab-case strings derived from the shop name (e.g., "tech-store-premium" or "christies"). * */ -export const getShopBySlug = (options: Options) => (options.client ?? client).get({ url: '/api/v1/by-slug/shops/{shopSlugId}', ...options }); +export const getShopBySlug = (options: Options) => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }, { scheme: 'bearer', type: 'http' }], + url: '/api/v1/by-slug/shops/{shopSlugId}', + ...options +}); /** * Search shops @@ -997,20 +1101,17 @@ export const searchPeriods = (options: Opt }); /** - * List shops for a partner user - * - * Returns all shops whose `partner_user_id` matches the requested `partnerId`. + * List the authenticated user's partner shops * - * Requires a valid Cognito JWT. The caller may request: - * - their own shops (`partnerId` equals the JWT `sub`), or - * - another partner's shops when the caller has the `ADMIN` role. + * Returns all shops currently linked to the authenticated user's `partnerShops` set. + * Requires a valid Cognito JWT and always resolves shops for the current user only. * - * Returns an empty array when the partner currently has no shops. + * Returns an empty array when the authenticated user currently has no linked partner shops. * */ -export const getPartnerShops = (options: Options) => (options.client ?? client).get({ +export const getMyPartnerShops = (options?: Options) => (options?.client ?? client).get({ security: [{ scheme: 'bearer', type: 'http' }], - url: '/api/v1/partner/{partnerId}/shops', + url: '/api/v1/me/partner-shops', ...options }); @@ -1168,3 +1269,165 @@ export const adminPostPartnerApplicationDecision = (options: Options) => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/oauth/authorize', + ...options +}); + +/** + * OAuth2 token endpoint + * + * Exchanges a single-use authorization code for an Aura Historia access token (RFC 6749 §4.1.3). + * The request body must be `application/x-www-form-urlencoded`. Client authentication is + * performed via `client_id` / `client_secret` form fields. PKCE code verification (RFC 7636) + * is mandatory: `code_verifier` must produce the `code_challenge` supplied to the authorize + * endpoint. + * + * The issued access token is non-expiring by default (same lifetime as tokens created via + * `POST /api/v1/me/access-tokens`). + * + */ +export const oauthToken = (options: Options) => (options.client ?? client).post({ + ...urlSearchParamsBodySerializer, + url: '/api/v1/oauth/token', + ...options, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...options.headers + } +}); + +/** + * OAuth2 token revocation endpoint + * + * Revokes an Aura Historia access token that was issued through the OAuth authorization code + * flow (RFC 7009). The request body must be `application/x-www-form-urlencoded`. Client + * authentication is performed via `client_id` / `client_secret` form fields. + * + * After revocation the token is immediately inactive and introspection will return + * `active: false`. Attempting to revoke an unknown or already-revoked token is not an error. + * + */ +export const oauthRevoke = (options: Options) => (options.client ?? client).post({ + ...urlSearchParamsBodySerializer, + url: '/api/v1/oauth/revoke', + ...options, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...options.headers + } +}); + +/** + * OAuth2 token introspection endpoint + * + * Returns metadata about an Aura Historia access token (RFC 7662). The request body must be + * `application/x-www-form-urlencoded`. Client authentication is performed via `client_id` / + * `client_secret` form fields. + * + * If the token is unknown, expired, or revoked, `active` is `false` and all other fields are + * omitted. Otherwise `active` is `true` and the available token metadata is populated. + * + */ +export const oauthIntrospect = (options: Options) => (options.client ?? client).post({ + ...urlSearchParamsBodySerializer, + url: '/api/v1/oauth/introspect', + ...options, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...options.headers + } +}); + +/** + * List OAuth client metadata + * + * Lists registered OAuth client metadata records. + * The returned `client_secret` value is masked and does not reveal the plaintext secret. + * Requires valid Cognito JWT authentication. + * + */ +export const getOAuthClients = (options?: Options) => (options?.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/oauth/clients', + ...options +}); + +/** + * Create OAuth client metadata + * + * Creates a new OAuth client metadata record. + * The plaintext `client_secret` is returned only in this create response; later reads return + * a masked secret display value. + * Requires valid Cognito JWT authentication and the `ADMIN` role. + * + */ +export const postOAuthClient = (options: Options) => (options.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/oauth/clients', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete OAuth client metadata + * + * Deletes one OAuth client metadata record. + * Requires valid Cognito JWT authentication and the `ADMIN` role. + * + */ +export const deleteOAuthClient = (options: Options) => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/oauth/clients/{clientId}', + ...options +}); + +/** + * Get OAuth client metadata + * + * Retrieves one OAuth client metadata record. + * The returned `client_secret` value is masked and does not reveal the plaintext secret. + * Requires valid Cognito JWT authentication. + * + */ +export const getOAuthClient = (options: Options) => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/oauth/clients/{clientId}', + ...options +}); + +/** + * Update OAuth client metadata + * + * Updates one OAuth client metadata record. + * Omitted or `null` optional properties leave the existing value unchanged. + * The returned `client_secret` value is masked and does not reveal the plaintext secret. + * Requires valid Cognito JWT authentication and the `ADMIN` role. + * + */ +export const patchOAuthClient = (options: Options) => (options.client ?? client).patch({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/oauth/clients/{clientId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); diff --git a/src/client/types.gen.ts b/src/client/types.gen.ts index cab871b1..6933923c 100644 --- a/src/client/types.gen.ts +++ b/src/client/types.gen.ts @@ -1600,15 +1600,236 @@ export type PostShopData = { }; /** - * Response body returned after creating or overwriting a partner shop API key. + * Aura Historia access-token scope string. + * - `shops:manage`: reserved for shop-management flows + * - `products:write`: required for partner batch product ingestion + * + */ +export type AccessTokenScopeData = 'shops:manage' | 'products:write'; + +/** + * Token type returned for Aura Historia access tokens. + */ +export type AccessTokenTypeData = 'BEARER'; + +/** + * Access-token read model. + * `POST /api/v1/me/access-tokens` returns the plaintext bearer token once; + * list/get/update endpoints return the masked display value instead. + * + */ +export type GetAccessTokenData = { + /** + * Unique identifier of the access token. + */ + accessTokenId: string; + /** + * User-defined display name of the access token. + */ + name: string; + /** + * Granted access-token scopes. Omitted when the token has no scopes. + */ + scope?: Array; + /** + * Plaintext or masked bearer token value. + * Create responses return the full token once; subsequent reads return a masked value such as `aurahistoria_abcdefghijk_****`. + * + */ + token: string; + tokenType: AccessTokenTypeData; + /** + * Optional absolute expiration timestamp in RFC 3339 format. + */ + expiresAt?: string | null; + /** + * Optional non-negative number of seconds remaining until expiry. + */ + expiresIn?: number | null; + /** + * Timestamp when the access token was created. + */ + created: string; + /** + * Timestamp when the access token metadata was last updated. + */ + updated: string; +}; + +/** + * Request body for creating an Aura Historia access token. + */ +export type PostAccessTokenData = { + /** + * User-defined display name of the new access token. + */ + name: string; + /** + * Optional scopes granted to the access token. Defaults to an empty set. + */ + scope?: Array; + /** + * Optional expiration timestamp in RFC 3339 format. + */ + expiresAt?: string | null; +}; + +/** + * Request body for updating access-token metadata. + * Optional fields that are omitted or set to `null` leave the stored value unchanged. + * + */ +export type PatchAccessTokenData = { + /** + * Unique identifier of the access token to update. + */ + accessTokenId: string; + /** + * Optional replacement display name. + */ + name?: string | null; + /** + * Optional replacement scope set. + */ + scope?: Array | null; + /** + * Optional replacement expiration timestamp in RFC 3339 format. + */ + expiresAt?: string | null; +}; + +/** + * Request body for creating OAuth client metadata. + * `redirect_uris` must contain at least one HTTPS redirect URI. + * + */ +export type OAuthClientMetadataRequestData = { + /** + * Display name of the OAuth client. + */ + client_name: string; + /** + * Registered HTTPS redirect URIs for the OAuth client. + */ + redirect_uris: Array; + /** + * Optional allowed scopes for tokens issued to this OAuth client. + */ + scope?: Array; +}; + +/** + * Request body for updating OAuth client metadata. + * Omitted or `null` optional properties leave the stored value unchanged. + * + */ +export type OAuthClientMetadataPatchData = { + /** + * Optional replacement display name for the OAuth client. + */ + client_name?: string | null; + /** + * Optional replacement set of registered HTTPS redirect URIs. + */ + redirect_uris?: Array | null; + /** + * Optional replacement set of allowed scopes for the OAuth client. + */ + scope?: Array | null; +}; + +/** + * OAuth client metadata read model. + * `client_secret` contains the plaintext secret only in the create response. + * List/get/update responses return a masked secret display value instead. + * */ -export type PartnerShopApiKeyResponse = { +export type OAuthClientMetadataResponseData = { /** - * Plaintext partner shop API key. - * Returned only when the key is created; subsequent verification uses the `x-api-key` request header. + * UUIDv7 identifier of the OAuth client. + */ + client_id: string; + /** + * OAuth client secret. + * Create responses return the plaintext secret once; later reads return a masked value such as `aurahistoria_oauth_client_secret_abcdefghijk_****`. * */ - apiKey: string; + client_secret: string; + /** + * Display name of the OAuth client. + */ + client_name: string; + /** + * Registered redirect URIs for the OAuth client. + */ + redirect_uris: Array; + /** + * Allowed scopes for tokens issued to this OAuth client. + */ + scope: Array; + /** + * Unix timestamp (seconds) when the OAuth client was created and its client ID was issued. + */ + client_id_issued_at: number; +}; + +/** + * Token response returned by `POST /api/v1/oauth/token` after a successful authorization code + * exchange (RFC 6749 §5.1). The `access_token` is an Aura Historia bearer access token. + * `expires_in` is omitted (`null`) for non-expiring tokens. + * + */ +export type OAuthTokenResponseData = { + /** + * The issued Aura Historia access token (plaintext bearer value). + */ + access_token: string; + token_type: AccessTokenTypeData; + /** + * Seconds until the access token expires. `null` when the token does not expire. + */ + expires_in?: number | null; + /** + * Space-separated list of scopes granted to the access token. + */ + scope: string; +}; + +/** + * Introspection response returned by `POST /api/v1/oauth/introspect` (RFC 7662). + * When `active` is `false` all other fields are omitted. + * When `active` is `true` the available token metadata is populated. + * + */ +export type OAuthIntrospectionResponseData = { + /** + * Whether the token is currently active (not expired, not revoked, and known to this server). + */ + active: boolean; + /** + * Space-separated list of scopes granted to the token. Present only when `active` is `true`. + */ + scope?: string; + /** + * UUIDv7 OAuth client identifier that issued the token. Present only when `active` is `true` and the token was issued via the OAuth flow. + */ + client_id?: string; + /** + * Subject — the Aura Historia user ID who authorized the token. Present only when `active` is `true`. + */ + sub?: string; + /** + * Token type. Always `"Bearer"` when `active` is `true`. + */ + token_type?: string; + /** + * Unix timestamp (seconds) when the token expires. Present only when `active` is `true` and the token has an expiry. + */ + exp?: number; + /** + * Unix timestamp (seconds) when the token was issued. Present only when `active` is `true` and the issue time is known. + */ + iat?: number; }; /** @@ -3272,13 +3493,13 @@ export type PatchPartnerProductsErrors = { */ 400: ApiError; /** - * Unauthorized — the `x-api-key` header is missing, malformed, or the API key does not - * match the key stored for the shop. + * Unauthorized — the bearer token is missing, invalid, expired, or the referenced + * Aura Historia access token no longer exists. * */ 401: ApiError; /** - * Forbidden — the shop exists but has not been granted partner status + * Forbidden — the authenticated caller is not allowed to ingest products for this shop */ 403: ApiError; /** @@ -3330,13 +3551,13 @@ export type PostPartnerProductsErrors = { */ 400: ApiError; /** - * Unauthorized — the `x-api-key` header is missing, malformed, or the API key does not - * match the key stored for the shop. + * Unauthorized — the bearer token is missing, invalid, expired, or the referenced + * Aura Historia access token no longer exists. * */ 401: ApiError; /** - * Forbidden — the shop exists but has not been granted partner status + * Forbidden — the authenticated caller is not allowed to ingest products for this shop */ 403: ApiError; /** @@ -3388,13 +3609,13 @@ export type PutPartnerProductsErrors = { */ 400: ApiError; /** - * Unauthorized — the `x-api-key` header is missing, malformed, or the API key does not - * match the key stored for the shop. + * Unauthorized — the bearer token is missing, invalid, expired, or the referenced + * Aura Historia access token no longer exists. * */ 401: ApiError; /** - * Forbidden — the shop exists but has not been granted partner status + * Forbidden — the authenticated caller is not allowed to ingest products for this shop */ 403: ApiError; /** @@ -3467,13 +3688,13 @@ export type PostWoocommerceWebhookErrors = { */ 400: ApiError; /** - * Unauthorized — the `x-api-key` header is missing, malformed, or does not match the - * stored partner key, or the WooCommerce signature header is missing/invalid. + * Unauthorized — the bearer token is missing or invalid, the Aura Historia access token + * is unknown, or the WooCommerce signature header is missing/invalid. * */ 401: ApiError; /** - * Forbidden — the shop exists but has not been granted partner status + * Forbidden — the authenticated caller is not linked to the requested partner shop */ 403: ApiError; /** @@ -4883,21 +5104,14 @@ export type UpdateUserAccountResponses = { export type UpdateUserAccountResponse = UpdateUserAccountResponses[keyof UpdateUserAccountResponses]; -export type PostBillingCheckoutData2 = { - /** - * Desired subscription plan and billing cycle for the new Stripe checkout session. - */ - body: PostBillingCheckoutData; +export type GetMyAccessTokensData = { + body?: never; path?: never; query?: never; - url: '/api/v1/me/billing/checkout'; + url: '/api/v1/me/access-tokens'; }; -export type PostBillingCheckoutErrors = { - /** - * Bad request - request body is missing, empty, malformed JSON, or contains unsupported enum values - */ - 400: ApiError; +export type GetMyAccessTokensErrors = { /** * Unauthorized - invalid or missing JWT token */ @@ -4906,77 +5120,76 @@ export type PostBillingCheckoutErrors = { * User not found */ 404: ApiError; - /** - * Conflict - the authenticated user already has a Stripe customer record - */ - 409: ApiError; /** * Internal server error */ 500: ApiError; }; -export type PostBillingCheckoutError = PostBillingCheckoutErrors[keyof PostBillingCheckoutErrors]; +export type GetMyAccessTokensError = GetMyAccessTokensErrors[keyof GetMyAccessTokensErrors]; -export type PostBillingCheckoutResponses = { +export type GetMyAccessTokensResponses = { /** - * Checkout session created successfully + * Access tokens retrieved successfully */ - 201: BillingSessionUrlData; + 200: Array; }; -export type PostBillingCheckoutResponse = PostBillingCheckoutResponses[keyof PostBillingCheckoutResponses]; +export type GetMyAccessTokensResponse = GetMyAccessTokensResponses[keyof GetMyAccessTokensResponses]; -export type PostBillingPortalData = { - body?: never; +export type PatchMyAccessTokenData = { + /** + * Partial access-token update payload. + */ + body: PatchAccessTokenData; path?: never; query?: never; - url: '/api/v1/me/billing/portal'; + url: '/api/v1/me/access-tokens'; }; -export type PostBillingPortalErrors = { +export type PatchMyAccessTokenErrors = { + /** + * Bad request - missing, empty, or invalid JSON body + */ + 400: ApiError; /** * Unauthorized - invalid or missing JWT token */ 401: ApiError; /** - * User not found + * Access token not found */ 404: ApiError; - /** - * Unprocessable Content - the authenticated user has no Stripe customer record yet - */ - 422: ApiError; /** * Internal server error */ 500: ApiError; }; -export type PostBillingPortalError = PostBillingPortalErrors[keyof PostBillingPortalErrors]; +export type PatchMyAccessTokenError = PatchMyAccessTokenErrors[keyof PatchMyAccessTokenErrors]; -export type PostBillingPortalResponses = { +export type PatchMyAccessTokenResponses = { /** - * Billing portal session created successfully + * Access token updated successfully */ - 201: BillingSessionUrlData; + 200: GetAccessTokenData; }; -export type PostBillingPortalResponse = PostBillingPortalResponses[keyof PostBillingPortalResponses]; +export type PatchMyAccessTokenResponse = PatchMyAccessTokenResponses[keyof PatchMyAccessTokenResponses]; -export type PostBillingManageData = { +export type PostMyAccessTokenData = { /** - * Desired subscription plan and billing cycle. Required for all callers. + * Access-token creation payload. */ - body: PostBillingCheckoutData; + body: PostAccessTokenData; path?: never; query?: never; - url: '/api/v1/me/billing/manage'; + url: '/api/v1/me/access-tokens'; }; -export type PostBillingManageErrors = { +export type PostMyAccessTokenErrors = { /** - * Bad request - request body is missing, empty, malformed JSON, or contains unsupported enum values + * Bad request - missing, empty, or invalid JSON body */ 400: ApiError; /** @@ -4987,46 +5200,38 @@ export type PostBillingManageErrors = { * User not found */ 404: ApiError; - /** - * Unprocessable Content - the authenticated paid user has no Stripe customer record yet - */ - 422: ApiError; /** * Internal server error */ 500: ApiError; }; -export type PostBillingManageError = PostBillingManageErrors[keyof PostBillingManageErrors]; +export type PostMyAccessTokenError = PostMyAccessTokenErrors[keyof PostMyAccessTokenErrors]; -export type PostBillingManageResponses = { +export type PostMyAccessTokenResponses = { /** - * Billing management session created successfully + * Access token created successfully */ - 201: BillingSessionUrlData; + 201: GetAccessTokenData; }; -export type PostBillingManageResponse = PostBillingManageResponses[keyof PostBillingManageResponses]; +export type PostMyAccessTokenResponse = PostMyAccessTokenResponses[keyof PostMyAccessTokenResponses]; -export type DeleteWatchlistProductData = { +export type DeleteMyAccessTokenData = { body?: never; path: { /** - * Unique identifier of the shop - */ - shopId: string; - /** - * Shop's unique identifier for the product + * Unique identifier of the access token to delete */ - shopsProductId: string; + accessTokenId: string; }; query?: never; - url: '/api/v1/me/watchlist/{shopId}/{shopsProductId}'; + url: '/api/v1/me/access-tokens/{accessTokenId}'; }; -export type DeleteWatchlistProductErrors = { +export type DeleteMyAccessTokenErrors = { /** - * Bad request - invalid parameters + * Bad request - invalid or missing access-token path parameter */ 400: ApiError; /** @@ -5034,7 +5239,7 @@ export type DeleteWatchlistProductErrors = { */ 401: ApiError; /** - * Watchlist entry not found + * Access token not found */ 404: ApiError; /** @@ -5043,50 +5248,32 @@ export type DeleteWatchlistProductErrors = { 500: ApiError; }; -export type DeleteWatchlistProductError = DeleteWatchlistProductErrors[keyof DeleteWatchlistProductErrors]; +export type DeleteMyAccessTokenError = DeleteMyAccessTokenErrors[keyof DeleteMyAccessTokenErrors]; -export type DeleteWatchlistProductResponses = { +export type DeleteMyAccessTokenResponses = { /** - * Product removed from watchlist successfully + * Access token deleted successfully */ 204: void; }; -export type DeleteWatchlistProductResponse = DeleteWatchlistProductResponses[keyof DeleteWatchlistProductResponses]; +export type DeleteMyAccessTokenResponse = DeleteMyAccessTokenResponses[keyof DeleteMyAccessTokenResponses]; -export type PatchWatchlistProductData = { - /** - * Patch object containing fields to update - */ - body: WatchlistProductPatch; +export type GetMyAccessTokenData = { + body?: never; path: { /** - * Unique identifier of the shop - */ - shopId: string; - /** - * Shop's unique identifier for the product - */ - shopsProductId: string; - }; - query?: { - /** - * Preferred language for localized content in the response. - * Defaults to `en` when omitted. - * - */ - language?: LanguageData; - /** - * Currency for price display in the response. + * Unique identifier of the access token */ - currency?: CurrencyData; + accessTokenId: string; }; - url: '/api/v1/me/watchlist/{shopId}/{shopsProductId}'; + query?: never; + url: '/api/v1/me/access-tokens/{accessTokenId}'; }; -export type PatchWatchlistProductErrors = { +export type GetMyAccessTokenErrors = { /** - * Bad request - invalid parameters or body + * Bad request - invalid or missing access-token path parameter */ 400: ApiError; /** @@ -5094,49 +5281,280 @@ export type PatchWatchlistProductErrors = { */ 401: ApiError; /** - * Watchlist entry not found + * Access token not found */ 404: ApiError; - /** - * Unprocessable Entity - watchlist quota exceeded during reactivation - */ - 422: ApiError; /** * Internal server error */ 500: ApiError; }; -export type PatchWatchlistProductError = PatchWatchlistProductErrors[keyof PatchWatchlistProductErrors]; +export type GetMyAccessTokenError = GetMyAccessTokenErrors[keyof GetMyAccessTokenErrors]; -export type PatchWatchlistProductResponses = { +export type GetMyAccessTokenResponses = { /** - * Watchlist product updated successfully + * Access token retrieved successfully */ - 200: PersonalizedGetProductData; + 200: GetAccessTokenData; }; -export type PatchWatchlistProductResponse = PatchWatchlistProductResponses[keyof PatchWatchlistProductResponses]; +export type GetMyAccessTokenResponse = GetMyAccessTokenResponses[keyof GetMyAccessTokenResponses]; -export type DeleteAllNotificationsData = { - body?: never; +export type PostBillingCheckoutData2 = { + /** + * Desired subscription plan and billing cycle for the new Stripe checkout session. + */ + body: PostBillingCheckoutData; path?: never; query?: never; - url: '/api/v1/me/notifications'; + url: '/api/v1/me/billing/checkout'; }; -export type DeleteAllNotificationsErrors = { +export type PostBillingCheckoutErrors = { /** - * Unauthorized – invalid or missing JWT token. + * Bad request - request body is missing, empty, malformed JSON, or contains unsupported enum values + */ + 400: ApiError; + /** + * Unauthorized - invalid or missing JWT token */ 401: ApiError; /** - * Internal server error. + * User not found + */ + 404: ApiError; + /** + * Conflict - the authenticated user already has a Stripe customer record + */ + 409: ApiError; + /** + * Internal server error */ 500: ApiError; }; -export type DeleteAllNotificationsError = DeleteAllNotificationsErrors[keyof DeleteAllNotificationsErrors]; +export type PostBillingCheckoutError = PostBillingCheckoutErrors[keyof PostBillingCheckoutErrors]; + +export type PostBillingCheckoutResponses = { + /** + * Checkout session created successfully + */ + 201: BillingSessionUrlData; +}; + +export type PostBillingCheckoutResponse = PostBillingCheckoutResponses[keyof PostBillingCheckoutResponses]; + +export type PostBillingPortalData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/me/billing/portal'; +}; + +export type PostBillingPortalErrors = { + /** + * Unauthorized - invalid or missing JWT token + */ + 401: ApiError; + /** + * User not found + */ + 404: ApiError; + /** + * Unprocessable Content - the authenticated user has no Stripe customer record yet + */ + 422: ApiError; + /** + * Internal server error + */ + 500: ApiError; +}; + +export type PostBillingPortalError = PostBillingPortalErrors[keyof PostBillingPortalErrors]; + +export type PostBillingPortalResponses = { + /** + * Billing portal session created successfully + */ + 201: BillingSessionUrlData; +}; + +export type PostBillingPortalResponse = PostBillingPortalResponses[keyof PostBillingPortalResponses]; + +export type PostBillingManageData = { + /** + * Desired subscription plan and billing cycle. Required for all callers. + */ + body: PostBillingCheckoutData; + path?: never; + query?: never; + url: '/api/v1/me/billing/manage'; +}; + +export type PostBillingManageErrors = { + /** + * Bad request - request body is missing, empty, malformed JSON, or contains unsupported enum values + */ + 400: ApiError; + /** + * Unauthorized - invalid or missing JWT token + */ + 401: ApiError; + /** + * User not found + */ + 404: ApiError; + /** + * Unprocessable Content - the authenticated paid user has no Stripe customer record yet + */ + 422: ApiError; + /** + * Internal server error + */ + 500: ApiError; +}; + +export type PostBillingManageError = PostBillingManageErrors[keyof PostBillingManageErrors]; + +export type PostBillingManageResponses = { + /** + * Billing management session created successfully + */ + 201: BillingSessionUrlData; +}; + +export type PostBillingManageResponse = PostBillingManageResponses[keyof PostBillingManageResponses]; + +export type DeleteWatchlistProductData = { + body?: never; + path: { + /** + * Unique identifier of the shop + */ + shopId: string; + /** + * Shop's unique identifier for the product + */ + shopsProductId: string; + }; + query?: never; + url: '/api/v1/me/watchlist/{shopId}/{shopsProductId}'; +}; + +export type DeleteWatchlistProductErrors = { + /** + * Bad request - invalid parameters + */ + 400: ApiError; + /** + * Unauthorized - invalid or missing JWT token + */ + 401: ApiError; + /** + * Watchlist entry not found + */ + 404: ApiError; + /** + * Internal server error + */ + 500: ApiError; +}; + +export type DeleteWatchlistProductError = DeleteWatchlistProductErrors[keyof DeleteWatchlistProductErrors]; + +export type DeleteWatchlistProductResponses = { + /** + * Product removed from watchlist successfully + */ + 204: void; +}; + +export type DeleteWatchlistProductResponse = DeleteWatchlistProductResponses[keyof DeleteWatchlistProductResponses]; + +export type PatchWatchlistProductData = { + /** + * Patch object containing fields to update + */ + body: WatchlistProductPatch; + path: { + /** + * Unique identifier of the shop + */ + shopId: string; + /** + * Shop's unique identifier for the product + */ + shopsProductId: string; + }; + query?: { + /** + * Preferred language for localized content in the response. + * Defaults to `en` when omitted. + * + */ + language?: LanguageData; + /** + * Currency for price display in the response. + */ + currency?: CurrencyData; + }; + url: '/api/v1/me/watchlist/{shopId}/{shopsProductId}'; +}; + +export type PatchWatchlistProductErrors = { + /** + * Bad request - invalid parameters or body + */ + 400: ApiError; + /** + * Unauthorized - invalid or missing JWT token + */ + 401: ApiError; + /** + * Watchlist entry not found + */ + 404: ApiError; + /** + * Unprocessable Entity - watchlist quota exceeded during reactivation + */ + 422: ApiError; + /** + * Internal server error + */ + 500: ApiError; +}; + +export type PatchWatchlistProductError = PatchWatchlistProductErrors[keyof PatchWatchlistProductErrors]; + +export type PatchWatchlistProductResponses = { + /** + * Watchlist product updated successfully + */ + 200: PersonalizedGetProductData; +}; + +export type PatchWatchlistProductResponse = PatchWatchlistProductResponses[keyof PatchWatchlistProductResponses]; + +export type DeleteAllNotificationsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/me/notifications'; +}; + +export type DeleteAllNotificationsErrors = { + /** + * Unauthorized – invalid or missing JWT token. + */ + 401: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type DeleteAllNotificationsError = DeleteAllNotificationsErrors[keyof DeleteAllNotificationsErrors]; export type DeleteAllNotificationsResponses = { /** @@ -5554,7 +5972,7 @@ export type PatchShopByIdErrors = { */ 400: ApiError; /** - * Unauthorized – invalid or missing Cognito JWT when using bearer auth, or missing/malformed/mismatched `x-api-key` when using partner API-key auth. + * Unauthorized – the bearer token is missing or invalid. */ 401: ApiError; /** @@ -5582,31 +6000,23 @@ export type PatchShopByIdResponses = { export type PatchShopByIdResponse = PatchShopByIdResponses[keyof PatchShopByIdResponses]; -export type PutShopApiKeyData = { +export type GetShopBySlugData = { body?: never; path: { /** - * Unique identifier of the shop (UUID format) + * Human-readable slug identifier of the shop (kebab-case, derived from shop name) */ - shopId: string; + shopSlugId: string; }; query?: never; - url: '/api/v1/shops/{shopId}/api-key'; + url: '/api/v1/by-slug/shops/{shopSlugId}'; }; -export type PutShopApiKeyErrors = { +export type GetShopBySlugErrors = { /** - * Bad request - invalid or missing shop ID + * Bad request - invalid or missing shop slug identifier */ 400: ApiError; - /** - * Unauthorized – invalid or missing JWT token. - */ - 401: ApiError; - /** - * Forbidden – caller is not allowed to manage an API key for this shop. - */ - 403: ApiError; /** * Shop not found */ @@ -5617,56 +6027,18 @@ export type PutShopApiKeyErrors = { 500: ApiError; }; -export type PutShopApiKeyError = PutShopApiKeyErrors[keyof PutShopApiKeyErrors]; +export type GetShopBySlugError = GetShopBySlugErrors[keyof GetShopBySlugErrors]; -export type PutShopApiKeyResponses = { +export type GetShopBySlugResponses = { /** - * API key created successfully + * Shop found and returned successfully */ - 200: PartnerShopApiKeyResponse; + 200: GetShopData; }; -export type PutShopApiKeyResponse = PutShopApiKeyResponses[keyof PutShopApiKeyResponses]; - -export type GetShopBySlugData = { - body?: never; - path: { - /** - * Human-readable slug identifier of the shop (kebab-case, derived from shop name) - */ - shopSlugId: string; - }; - query?: never; - url: '/api/v1/by-slug/shops/{shopSlugId}'; -}; +export type GetShopBySlugResponse = GetShopBySlugResponses[keyof GetShopBySlugResponses]; -export type GetShopBySlugErrors = { - /** - * Bad request - invalid or missing shop slug identifier - */ - 400: ApiError; - /** - * Shop not found - */ - 404: ApiError; - /** - * Internal server error - */ - 500: ApiError; -}; - -export type GetShopBySlugError = GetShopBySlugErrors[keyof GetShopBySlugErrors]; - -export type GetShopBySlugResponses = { - /** - * Shop found and returned successfully - */ - 200: GetShopData; -}; - -export type GetShopBySlugResponse = GetShopBySlugResponses[keyof GetShopBySlugResponses]; - -export type SearchShopsData = { +export type SearchShopsData = { /** * Shop search filter configuration with all filtering criteria. * Allows filtering by shop name, shop type, partner status, @@ -5992,47 +6364,38 @@ export type SearchPeriodsResponses = { export type SearchPeriodsResponse = SearchPeriodsResponses[keyof SearchPeriodsResponses]; -export type GetPartnerShopsData = { +export type GetMyPartnerShopsData = { body?: never; - path: { - /** - * Unique identifier of the partner user - */ - partnerId: string; - }; + path?: never; query?: never; - url: '/api/v1/partner/{partnerId}/shops'; + url: '/api/v1/me/partner-shops'; }; -export type GetPartnerShopsErrors = { - /** - * Bad request - invalid or missing partner ID - */ - 400: ApiError; +export type GetMyPartnerShopsErrors = { /** * Unauthorized – invalid or missing JWT token. */ 401: ApiError; /** - * Forbidden – this endpoint requires the `ADMIN` role when requesting another partner's shops. + * User not found – the authenticated requester does not have a persisted user record. */ - 403: ApiError; + 404: ApiError; /** * Internal server error */ 500: ApiError; }; -export type GetPartnerShopsError = GetPartnerShopsErrors[keyof GetPartnerShopsErrors]; +export type GetMyPartnerShopsError = GetMyPartnerShopsErrors[keyof GetMyPartnerShopsErrors]; -export type GetPartnerShopsResponses = { +export type GetMyPartnerShopsResponses = { /** * Partner shops retrieved successfully */ 200: Array; }; -export type GetPartnerShopsResponse = GetPartnerShopsResponses[keyof GetPartnerShopsResponses]; +export type GetMyPartnerShopsResponse = GetMyPartnerShopsResponses[keyof GetMyPartnerShopsResponses]; export type GetPartnerApplicationsData = { body?: never; @@ -6417,3 +6780,407 @@ export type AdminPostPartnerApplicationDecisionResponses = { }; export type AdminPostPartnerApplicationDecisionResponse = AdminPostPartnerApplicationDecisionResponses[keyof AdminPostPartnerApplicationDecisionResponses]; + +export type OauthAuthorizeData = { + body?: never; + path?: never; + query: { + /** + * OAuth response type. Must be `code`. + */ + response_type: 'code'; + /** + * UUIDv7 identifier of the registered OAuth client. + */ + client_id: string; + /** + * Redirect URI registered for the OAuth client. Must exactly match one of the client's registered redirect URIs. + */ + redirect_uri: string; + /** + * Space-separated list of requested scopes. Must be a subset of the client's allowed scopes. + */ + scope?: string; + /** + * Opaque client state value. Returned unchanged in the redirect to prevent CSRF attacks. + */ + state?: string; + /** + * PKCE code challenge. Must be the base64url-encoded SHA256 hash of the `code_verifier` (S256 method). + */ + code_challenge: string; + /** + * PKCE challenge method. Must be `S256`. + */ + code_challenge_method: 'S256'; + }; + url: '/api/v1/oauth/authorize'; +}; + +export type OauthAuthorizeErrors = { + /** + * Bad request — a required query parameter is missing or has an unsupported value. + */ + 400: ApiError; + /** + * Unauthorized — the Cognito bearer token is missing, invalid, or the OAuth client was not found / credentials are invalid. + */ + 401: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type OauthAuthorizeError = OauthAuthorizeErrors[keyof OauthAuthorizeErrors]; + +export type OauthTokenData = { + /** + * Authorization code exchange payload (form-urlencoded). + */ + body: { + /** + * OAuth grant type. Must be `authorization_code`. + */ + grant_type: 'authorization_code'; + /** + * Single-use authorization code received from the authorize redirect. + */ + code: string; + /** + * Must exactly match the `redirect_uri` used in the corresponding authorization request. + */ + redirect_uri: string; + /** + * UUIDv7 OAuth client identifier. + */ + client_id: string; + /** + * OAuth client secret. + */ + client_secret: string; + /** + * PKCE code verifier. The SHA256 hash of this value must match the `code_challenge` from the authorization request. + */ + code_verifier: string; + }; + path?: never; + query?: never; + url: '/api/v1/oauth/token'; +}; + +export type OauthTokenErrors = { + /** + * Bad request — a required form field is missing, `client_id` is not a valid UUID, or an authorization code error occurred (code not found, expired, mismatched client or redirect URI, or invalid PKCE verifier). + */ + 400: ApiError; + /** + * Unauthorized — invalid OAuth client credentials. + */ + 401: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type OauthTokenError = OauthTokenErrors[keyof OauthTokenErrors]; + +export type OauthTokenResponses = { + /** + * Access token issued successfully. + */ + 200: OAuthTokenResponseData; +}; + +export type OauthTokenResponse = OauthTokenResponses[keyof OauthTokenResponses]; + +export type OauthRevokeData = { + /** + * Token revocation payload (form-urlencoded). + */ + body: { + /** + * The access token to revoke. + */ + token: string; + /** + * UUIDv7 OAuth client identifier. + */ + client_id: string; + /** + * OAuth client secret. + */ + client_secret: string; + }; + path?: never; + query?: never; + url: '/api/v1/oauth/revoke'; +}; + +export type OauthRevokeErrors = { + /** + * Bad request — a required form field is missing, `client_id` is not a valid UUID, or the token value is invalid. + */ + 400: ApiError; + /** + * Unauthorized — invalid OAuth client credentials. + */ + 401: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type OauthRevokeError = OauthRevokeErrors[keyof OauthRevokeErrors]; + +export type OauthRevokeResponses = { + /** + * Token revoked successfully (or token was unknown/already inactive). Empty response body. + */ + 200: unknown; +}; + +export type OauthIntrospectData = { + /** + * Token introspection payload (form-urlencoded). + */ + body: { + /** + * The access token to introspect. + */ + token: string; + /** + * UUIDv7 OAuth client identifier. + */ + client_id: string; + /** + * OAuth client secret. + */ + client_secret: string; + }; + path?: never; + query?: never; + url: '/api/v1/oauth/introspect'; +}; + +export type OauthIntrospectErrors = { + /** + * Bad request — a required form field is missing, `client_id` is not a valid UUID, or the token value is invalid. + */ + 400: ApiError; + /** + * Unauthorized — invalid OAuth client credentials. + */ + 401: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type OauthIntrospectError = OauthIntrospectErrors[keyof OauthIntrospectErrors]; + +export type OauthIntrospectResponses = { + /** + * Introspection result. `active: false` means the token is unknown, expired, or revoked. + */ + 200: OAuthIntrospectionResponseData; +}; + +export type OauthIntrospectResponse = OauthIntrospectResponses[keyof OauthIntrospectResponses]; + +export type GetOAuthClientsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/oauth/clients'; +}; + +export type GetOAuthClientsErrors = { + /** + * Unauthorized — invalid or missing JWT token. + */ + 401: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type GetOAuthClientsError = GetOAuthClientsErrors[keyof GetOAuthClientsErrors]; + +export type GetOAuthClientsResponses = { + /** + * OAuth client metadata retrieved successfully. + */ + 200: Array; +}; + +export type GetOAuthClientsResponse = GetOAuthClientsResponses[keyof GetOAuthClientsResponses]; + +export type PostOAuthClientData = { + /** + * OAuth client metadata creation payload. + */ + body: OAuthClientMetadataRequestData; + path?: never; + query?: never; + url: '/api/v1/oauth/clients'; +}; + +export type PostOAuthClientErrors = { + /** + * Bad request — missing, empty, malformed JSON body, or invalid OAuth client metadata. + */ + 400: ApiError; + /** + * Unauthorized — invalid or missing JWT token. + */ + 401: ApiError; + /** + * Forbidden — the authenticated user is not an admin. + */ + 403: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type PostOAuthClientError = PostOAuthClientErrors[keyof PostOAuthClientErrors]; + +export type PostOAuthClientResponses = { + /** + * OAuth client metadata created successfully. + */ + 201: OAuthClientMetadataResponseData; +}; + +export type PostOAuthClientResponse = PostOAuthClientResponses[keyof PostOAuthClientResponses]; + +export type DeleteOAuthClientData = { + body?: never; + path: { + /** + * UUIDv7 identifier of the OAuth client. + */ + clientId: string; + }; + query?: never; + url: '/api/v1/oauth/clients/{clientId}'; +}; + +export type DeleteOAuthClientErrors = { + /** + * Bad request — invalid or missing OAuth client path parameter. + */ + 400: ApiError; + /** + * Unauthorized — invalid or missing JWT token, or the OAuth client does not exist. + */ + 401: ApiError; + /** + * Forbidden — the authenticated user is not an admin. + */ + 403: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type DeleteOAuthClientError = DeleteOAuthClientErrors[keyof DeleteOAuthClientErrors]; + +export type DeleteOAuthClientResponses = { + /** + * OAuth client metadata deleted successfully. Empty response body. + */ + 204: void; +}; + +export type DeleteOAuthClientResponse = DeleteOAuthClientResponses[keyof DeleteOAuthClientResponses]; + +export type GetOAuthClientData = { + body?: never; + path: { + /** + * UUIDv7 identifier of the OAuth client. + */ + clientId: string; + }; + query?: never; + url: '/api/v1/oauth/clients/{clientId}'; +}; + +export type GetOAuthClientErrors = { + /** + * Bad request — invalid or missing OAuth client path parameter. + */ + 400: ApiError; + /** + * Unauthorized — invalid or missing JWT token, or the OAuth client does not exist. + */ + 401: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type GetOAuthClientError = GetOAuthClientErrors[keyof GetOAuthClientErrors]; + +export type GetOAuthClientResponses = { + /** + * OAuth client metadata retrieved successfully. + */ + 200: OAuthClientMetadataResponseData; +}; + +export type GetOAuthClientResponse = GetOAuthClientResponses[keyof GetOAuthClientResponses]; + +export type PatchOAuthClientData = { + /** + * Partial OAuth client metadata update payload. + */ + body: OAuthClientMetadataPatchData; + path: { + /** + * UUIDv7 identifier of the OAuth client. + */ + clientId: string; + }; + query?: never; + url: '/api/v1/oauth/clients/{clientId}'; +}; + +export type PatchOAuthClientErrors = { + /** + * Bad request — invalid or missing OAuth client path parameter, empty body, malformed JSON body, or invalid OAuth client metadata. + */ + 400: ApiError; + /** + * Unauthorized — invalid or missing JWT token, or the OAuth client does not exist. + */ + 401: ApiError; + /** + * Forbidden — the authenticated user is not an admin. + */ + 403: ApiError; + /** + * Internal server error. + */ + 500: ApiError; +}; + +export type PatchOAuthClientError = PatchOAuthClientErrors[keyof PatchOAuthClientErrors]; + +export type PatchOAuthClientResponses = { + /** + * OAuth client metadata updated successfully. + */ + 200: OAuthClientMetadataResponseData; +}; + +export type PatchOAuthClientResponse = PatchOAuthClientResponses[keyof PatchOAuthClientResponses]; diff --git a/src/components/oauth/OAuthAuthorizePage.tsx b/src/components/oauth/OAuthAuthorizePage.tsx new file mode 100644 index 00000000..f26f2afe --- /dev/null +++ b/src/components/oauth/OAuthAuthorizePage.tsx @@ -0,0 +1,211 @@ +import { useTranslation } from "react-i18next"; +import { useOAuthClient } from "@/hooks/oauth/useOAuthClient.ts"; +import { useOAuthAuthorize } from "@/hooks/oauth/useOAuthAuthorize.ts"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { H1 } from "@/components/typography/H1.tsx"; +import { Spinner } from "@/components/ui/spinner.tsx"; +import { ShieldCheck, ShieldAlert, AlertTriangle } from "lucide-react"; +import type { OAuthScope } from "@/data/internal/oauth/OAuthClient.ts"; + +type OAuthAuthorizeSearchParams = { + readonly response_type: string; + readonly client_id: string; + readonly redirect_uri: string; + readonly scope?: string; + readonly state?: string; + readonly code_challenge: string; + readonly code_challenge_method: string; +}; + +interface OAuthAuthorizePageProps { + readonly searchParams: OAuthAuthorizeSearchParams; +} + +export function OAuthAuthorizePage({ searchParams }: OAuthAuthorizePageProps) { + const { t } = useTranslation(); + const { data: client, isLoading, isError } = useOAuthClient(searchParams.client_id); + const authorize = useOAuthAuthorize(); + + const requestedScopes = searchParams.scope?.split(" ").filter(Boolean) ?? []; + + const handleApprove = () => { + authorize.mutate( + { + clientId: searchParams.client_id, + redirectUri: searchParams.redirect_uri, + codeChallenge: searchParams.code_challenge, + scope: searchParams.scope, + state: searchParams.state, + }, + { + onSuccess: (result) => { + window.location.href = result.redirectUrl; + }, + }, + ); + }; + + const handleDeny = () => { + const url = new URL(searchParams.redirect_uri); + url.searchParams.set("error", "access_denied"); + url.searchParams.set("error_description", "The user denied the authorization request."); + if (searchParams.state) { + url.searchParams.set("state", searchParams.state); + } + window.location.href = url.toString(); + }; + + if (isLoading) { + return ( + + + + + + + + ); + } + + if (isError || !client) { + return ( + + + + + + + +

+ {t("oauth.authorize.error.description")} +

+
+
+
+ ); + } + + return ( + +
+

{t("oauth.authorize.title")}

+ + + + + + + + +

+ {t("oauth.authorize.description", { + appName: client.clientName, + })} +

+ + {requestedScopes.length > 0 && ( +
+

+ {t("oauth.authorize.scopesTitle")} +

+
    + {requestedScopes.map((scope) => ( + + ))} +
+
+ )} + +
+
+
+ + + + + +
+ + {authorize.isError && ( + + +

{authorize.error.message}

+
+
+ )} +
+
+ ); +} + +function PageContainer({ children }: { readonly children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +const SCOPE_ICONS: Record = { + "products:write": "📦", + "shops:manage": "🏪", +}; + +function ScopeItem({ scope }: { readonly scope: OAuthScope }) { + const { t } = useTranslation(); + + const scopeKey = scope.replace(":", "_"); + const label = t(`oauth.scopes.${scopeKey}.label`); + const description = t(`oauth.scopes.${scopeKey}.description`); + const icon = SCOPE_ICONS[scope] ?? "🔑"; + + return ( +
  • + +
    + {label} + {description} +
    +
  • + ); +} diff --git a/src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx b/src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx new file mode 100644 index 00000000..ca3a7309 --- /dev/null +++ b/src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx @@ -0,0 +1,316 @@ +import { act, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { OAuthAuthorizePage } from "../OAuthAuthorizePage.tsx"; +import { renderWithRouter } from "@/test/utils.tsx"; + +const mockMutate = vi.hoisted(() => vi.fn()); + +const mockClientData = vi.hoisted(() => ({ + clientId: "01970f22-2bf0-7000-8000-000000000010", + clientName: "Test Partner App", + redirectUris: ["https://client.example/callback"], + scopes: ["products:write" as const, "shops:manage" as const], +})); + +const mockUseOAuthClient = vi.hoisted(() => + vi.fn().mockReturnValue({ + data: mockClientData, + isLoading: false, + isError: false, + }), +); + +const mockUseOAuthAuthorize = vi.hoisted(() => + vi.fn().mockReturnValue({ + mutate: mockMutate, + isPending: false, + isError: false, + error: null, + }), +); + +vi.mock("@/hooks/oauth/useOAuthClient", () => ({ + useOAuthClient: mockUseOAuthClient, +})); + +vi.mock("@/hooks/oauth/useOAuthAuthorize", () => ({ + useOAuthAuthorize: mockUseOAuthAuthorize, +})); + +const defaultSearchParams = { + response_type: "code", + client_id: "01970f22-2bf0-7000-8000-000000000010", + redirect_uri: "https://client.example/callback", + scope: "products:write shops:manage", + state: "csrf-state-123", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "S256", +}; + +describe("OAuthAuthorizePage", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseOAuthClient.mockReturnValue({ + data: mockClientData, + isLoading: false, + isError: false, + }); + mockUseOAuthAuthorize.mockReturnValue({ + mutate: mockMutate, + isPending: false, + isError: false, + error: null, + }); + }); + + it("renders the authorization page with client name", async () => { + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText("Anwendung autorisieren")).toBeInTheDocument(); + expect(screen.getByText("Test Partner App")).toBeInTheDocument(); + }); + + it("displays requested scopes with labels and descriptions", async () => { + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText("Produkte verwalten")).toBeInTheDocument(); + expect( + screen.getByText( + "Produktangebote in Ihrem Namen erstellen, aktualisieren und verwalten.", + ), + ).toBeInTheDocument(); + expect(screen.getByText("Shops verwalten")).toBeInTheDocument(); + expect( + screen.getByText( + "Auf Ihre Shop-Einstellungen und -Konfiguration zugreifen und diese verwalten.", + ), + ).toBeInTheDocument(); + }); + + it("displays the authorization description with app name", async () => { + await act(async () => + renderWithRouter(), + ); + + expect( + screen.getByText(/"Test Partner App" möchte auf Ihr Aura-Historia-Konto zugreifen/), + ).toBeInTheDocument(); + }); + + it("displays security note", async () => { + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText(/Sie können diesen Zugriff jederzeit/)).toBeInTheDocument(); + }); + + it("renders authorize and decline buttons", async () => { + await act(async () => + renderWithRouter(), + ); + + expect( + screen.getByRole("button", { + name: "Test Partner App den Zugriff auf Ihr Konto erlauben", + }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: "Autorisierung für Test Partner App ablehnen", + }), + ).toBeInTheDocument(); + }); + + it("calls authorize mutation on approve click", async () => { + const user = userEvent.setup(); + await act(async () => + renderWithRouter(), + ); + + await user.click( + screen.getByRole("button", { + name: "Test Partner App den Zugriff auf Ihr Konto erlauben", + }), + ); + + expect(mockMutate).toHaveBeenCalledWith( + { + clientId: "01970f22-2bf0-7000-8000-000000000010", + redirectUri: "https://client.example/callback", + codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + scope: "products:write shops:manage", + state: "csrf-state-123", + }, + expect.objectContaining({ + onSuccess: expect.any(Function), + }), + ); + }); + + it("redirects to redirect_uri with access_denied on deny click", async () => { + const user = userEvent.setup(); + const mockLocationHref = vi.fn(); + const locationProxy = new Proxy( + {}, + { + set(_target, prop, value) { + if (prop === "href") { + mockLocationHref(value); + } + return true; + }, + }, + ); + Object.defineProperty(window, "location", { + value: locationProxy, + writable: true, + configurable: true, + }); + + await act(async () => + renderWithRouter(), + ); + + await user.click( + screen.getByRole("button", { + name: "Autorisierung für Test Partner App ablehnen", + }), + ); + + expect(mockLocationHref).toHaveBeenCalledWith( + expect.stringContaining("error=access_denied"), + ); + expect(mockLocationHref).toHaveBeenCalledWith( + expect.stringContaining("state=csrf-state-123"), + ); + }); + + it("shows loading spinner when client data is loading", async () => { + mockUseOAuthClient.mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + }); + + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByRole("status", { name: "Loading" })).toBeInTheDocument(); + expect(screen.queryByText("Test Partner App")).not.toBeInTheDocument(); + }); + + it("shows error state when client fetch fails", async () => { + mockUseOAuthClient.mockReturnValue({ + data: undefined, + isLoading: false, + isError: true, + }); + + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText("Ungültige Autorisierungsanfrage")).toBeInTheDocument(); + expect( + screen.getByText(/Die Anwendung konnte nicht identifiziert werden/), + ).toBeInTheDocument(); + }); + + it("disables buttons when authorization is pending", async () => { + mockUseOAuthAuthorize.mockReturnValue({ + mutate: mockMutate, + isPending: true, + isError: false, + error: null, + }); + + await act(async () => + renderWithRouter(), + ); + + const approveButton = screen.getByRole("button", { + name: "Test Partner App den Zugriff auf Ihr Konto erlauben", + }); + const denyButton = screen.getByRole("button", { + name: "Autorisierung für Test Partner App ablehnen", + }); + + expect(approveButton).toBeDisabled(); + expect(denyButton).toBeDisabled(); + }); + + it("displays authorization error message when mutation fails", async () => { + mockUseOAuthAuthorize.mockReturnValue({ + mutate: mockMutate, + isPending: false, + isError: true, + error: new Error("Authorization failed: invalid scope."), + }); + + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText("Authorization failed: invalid scope.")).toBeInTheDocument(); + }); + + it("renders without scopes when scope param is missing", async () => { + const searchParamsWithoutScope = { + ...defaultSearchParams, + scope: undefined, + }; + + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText("Test Partner App")).toBeInTheDocument(); + expect(screen.queryByText("Produkte verwalten")).not.toBeInTheDocument(); + expect( + screen.queryByText("Diese Anwendung fordert folgende Berechtigungen an:"), + ).not.toBeInTheDocument(); + }); + + it("renders scope list with correct aria-label", async () => { + await act(async () => + renderWithRouter(), + ); + + const scopeList = screen.getByRole("list", { + name: "Diese Anwendung fordert folgende Berechtigungen an:", + }); + expect(scopeList).toBeInTheDocument(); + + const scopeItems = screen.getAllByRole("listitem"); + expect(scopeItems).toHaveLength(2); + }); + + it("passes correct client_id to useOAuthClient hook", async () => { + await act(async () => + renderWithRouter(), + ); + + expect(mockUseOAuthClient).toHaveBeenCalledWith("01970f22-2bf0-7000-8000-000000000010"); + }); + + it("handles single scope correctly", async () => { + const singleScopeParams = { + ...defaultSearchParams, + scope: "products:write", + }; + + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText("Produkte verwalten")).toBeInTheDocument(); + expect(screen.queryByText("Shops verwalten")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/oauth/__tests__/OAuthAuthorizeRoute.test.ts b/src/components/oauth/__tests__/OAuthAuthorizeRoute.test.ts new file mode 100644 index 00000000..4c6528b5 --- /dev/null +++ b/src/components/oauth/__tests__/OAuthAuthorizeRoute.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "vitest"; +import { Route } from "../../../routes/_auth.oauth.authorize.tsx"; + +const validateSearch = Route.options.validateSearch as ( + search: Record, +) => Record; + +describe("_auth.oauth.authorize route", () => { + it("adds noindex robots meta tag", () => { + const head = Route.options.head; + expect(head).toBeDefined(); + const context = {} as Parameters>[0]; + expect(head?.(context)).toEqual({ + meta: [{ name: "robots", content: "noindex, nofollow" }], + }); + }); + + it("has SSR disabled", () => { + expect(Route.options.ssr).toBe(false); + }); + + it("validates search params with required fields", () => { + expect(validateSearch).toBeDefined(); + + const validSearch = { + response_type: "code", + client_id: "01970f22-2bf0-7000-8000-000000000010", + redirect_uri: "https://client.example/callback", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "S256", + }; + + const result = validateSearch(validSearch); + expect(result).toEqual( + expect.objectContaining({ + response_type: "code", + client_id: "01970f22-2bf0-7000-8000-000000000010", + redirect_uri: "https://client.example/callback", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "S256", + }), + ); + }); + + it("validates search params with optional scope and state", () => { + const searchWithOptionals = { + response_type: "code", + client_id: "01970f22-2bf0-7000-8000-000000000010", + redirect_uri: "https://client.example/callback", + scope: "products:write", + state: "csrf-token-xyz", + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "S256", + }; + + const result = validateSearch(searchWithOptionals); + expect(result).toHaveProperty("scope", "products:write"); + expect(result).toHaveProperty("state", "csrf-token-xyz"); + }); + + it("throws when required client_id is missing", () => { + expect(() => + validateSearch({ + response_type: "code", + redirect_uri: "https://client.example/callback", + code_challenge: "test", + code_challenge_method: "S256", + }), + ).toThrow(); + }); + + it("throws when required redirect_uri is missing", () => { + expect(() => + validateSearch({ + response_type: "code", + client_id: "01970f22-2bf0-7000-8000-000000000010", + code_challenge: "test", + code_challenge_method: "S256", + }), + ).toThrow(); + }); + + it("throws when required code_challenge is missing", () => { + expect(() => + validateSearch({ + response_type: "code", + client_id: "01970f22-2bf0-7000-8000-000000000010", + redirect_uri: "https://client.example/callback", + code_challenge_method: "S256", + }), + ).toThrow(); + }); + + it("defaults response_type to 'code' when not provided", () => { + const result = validateSearch({ + client_id: "01970f22-2bf0-7000-8000-000000000010", + redirect_uri: "https://client.example/callback", + code_challenge: "test-challenge", + code_challenge_method: "S256", + }); + + expect(result).toHaveProperty("response_type", "code"); + }); + + it("defaults code_challenge_method to 'S256' when not provided", () => { + const result = validateSearch({ + response_type: "code", + client_id: "01970f22-2bf0-7000-8000-000000000010", + redirect_uri: "https://client.example/callback", + code_challenge: "test-challenge", + }); + + expect(result).toHaveProperty("code_challenge_method", "S256"); + }); +}); diff --git a/src/data/internal/oauth/OAuthClient.ts b/src/data/internal/oauth/OAuthClient.ts new file mode 100644 index 00000000..3176f01c --- /dev/null +++ b/src/data/internal/oauth/OAuthClient.ts @@ -0,0 +1,23 @@ +import type { AccessTokenScopeData, OAuthClientMetadataResponseData } from "@/client"; + +export type OAuthScope = "shops:manage" | "products:write"; + +export type OAuthClient = { + readonly clientId: string; + readonly clientName: string; + readonly redirectUris: readonly string[]; + readonly scopes: readonly OAuthScope[]; +}; + +function mapScope(scope: AccessTokenScopeData): OAuthScope { + return scope; +} + +export function mapToInternalOAuthClient(data: OAuthClientMetadataResponseData): OAuthClient { + return { + clientId: data.client_id, + clientName: data.client_name, + redirectUris: data.redirect_uris, + scopes: data.scope.map(mapScope), + }; +} diff --git a/src/data/internal/oauth/__tests__/OAuthClient.test.ts b/src/data/internal/oauth/__tests__/OAuthClient.test.ts new file mode 100644 index 00000000..709ae6a2 --- /dev/null +++ b/src/data/internal/oauth/__tests__/OAuthClient.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { mapToInternalOAuthClient, type OAuthClient } from "../OAuthClient.ts"; +import type { OAuthClientMetadataResponseData } from "@/client"; + +describe("mapToInternalOAuthClient", () => { + it("maps all fields from API response to internal model", () => { + const apiData: OAuthClientMetadataResponseData = { + client_id: "01970f22-2bf0-7000-8000-000000000010", + client_secret: "aurahistoria_oauth_client_secret_abcdefghijk_****", + client_name: "Test OAuth App", + redirect_uris: ["https://client.example/callback"], + scope: ["products:write"], + client_id_issued_at: 1748539200, + }; + + const result: OAuthClient = mapToInternalOAuthClient(apiData); + + expect(result.clientId).toBe("01970f22-2bf0-7000-8000-000000000010"); + expect(result.clientName).toBe("Test OAuth App"); + expect(result.redirectUris).toEqual(["https://client.example/callback"]); + expect(result.scopes).toEqual(["products:write"]); + }); + + it("maps multiple scopes correctly", () => { + const apiData: OAuthClientMetadataResponseData = { + client_id: "01970f22-2bf0-7000-8000-000000000010", + client_secret: "masked", + client_name: "Multi-Scope App", + redirect_uris: ["https://client.example/callback", "https://client.example/auth"], + scope: ["products:write", "shops:manage"], + client_id_issued_at: 1748539200, + }; + + const result = mapToInternalOAuthClient(apiData); + + expect(result.scopes).toEqual(["products:write", "shops:manage"]); + expect(result.redirectUris).toHaveLength(2); + }); + + it("handles empty scopes array", () => { + const apiData: OAuthClientMetadataResponseData = { + client_id: "01970f22-2bf0-7000-8000-000000000010", + client_secret: "masked", + client_name: "No Scope App", + redirect_uris: ["https://client.example/callback"], + scope: [], + client_id_issued_at: 1748539200, + }; + + const result = mapToInternalOAuthClient(apiData); + + expect(result.scopes).toEqual([]); + }); + + it("does not include client_secret or client_id_issued_at in internal model", () => { + const apiData: OAuthClientMetadataResponseData = { + client_id: "01970f22-2bf0-7000-8000-000000000010", + client_secret: "secret-value", + client_name: "App", + redirect_uris: ["https://example.com/cb"], + scope: ["products:write"], + client_id_issued_at: 1748539200, + }; + + const result = mapToInternalOAuthClient(apiData); + + expect(result).not.toHaveProperty("clientSecret"); + expect(result).not.toHaveProperty("client_secret"); + expect(result).not.toHaveProperty("clientIdIssuedAt"); + expect(result).not.toHaveProperty("client_id_issued_at"); + }); +}); diff --git a/src/hooks/oauth/useOAuthAuthorize.ts b/src/hooks/oauth/useOAuthAuthorize.ts new file mode 100644 index 00000000..e723df87 --- /dev/null +++ b/src/hooks/oauth/useOAuthAuthorize.ts @@ -0,0 +1,51 @@ +import { useMutation, type UseMutationResult } from "@tanstack/react-query"; +import { oauthAuthorize } from "@/client"; +import { useApiError } from "@/hooks/common/useApiError.ts"; +import { mapToInternalApiError } from "@/data/internal/hooks/ApiError.ts"; + +export type OAuthAuthorizeParams = { + readonly clientId: string; + readonly redirectUri: string; + readonly codeChallenge: string; + readonly scope?: string; + readonly state?: string; +}; + +export type OAuthAuthorizeResult = { + readonly redirectUrl: string; +}; + +export function useOAuthAuthorize(): UseMutationResult< + OAuthAuthorizeResult, + Error, + OAuthAuthorizeParams +> { + const { getErrorMessage } = useApiError(); + + return useMutation({ + mutationFn: async (params: OAuthAuthorizeParams) => { + const result = await oauthAuthorize({ + query: { + response_type: "code", + client_id: params.clientId, + redirect_uri: params.redirectUri, + code_challenge: params.codeChallenge, + code_challenge_method: "S256", + scope: params.scope, + state: params.state, + }, + }); + + if (result.error) { + throw new Error(getErrorMessage(mapToInternalApiError(result.error))); + } + + const locationHeader = result.response?.headers?.get("Location"); + if (locationHeader) { + return { redirectUrl: locationHeader }; + } + + throw new Error("Authorization failed: no redirect location received."); + }, + }); +} diff --git a/src/hooks/oauth/useOAuthClient.ts b/src/hooks/oauth/useOAuthClient.ts new file mode 100644 index 00000000..510a4584 --- /dev/null +++ b/src/hooks/oauth/useOAuthClient.ts @@ -0,0 +1,27 @@ +import { useQuery, type UseQueryResult } from "@tanstack/react-query"; +import { getOAuthClient } from "@/client"; +import { mapToInternalOAuthClient, type OAuthClient } from "@/data/internal/oauth/OAuthClient.ts"; +import { useApiError } from "@/hooks/common/useApiError.ts"; +import { mapToInternalApiError } from "@/data/internal/hooks/ApiError.ts"; + +export function useOAuthClient(clientId: string | undefined): UseQueryResult { + const { getErrorMessage } = useApiError(); + + return useQuery({ + queryKey: ["oauthClient", clientId], + queryFn: async () => { + const result = await getOAuthClient({ + path: { clientId: clientId! }, + }); + + if (result.error) { + throw new Error(getErrorMessage(mapToInternalApiError(result.error))); + } + + return mapToInternalOAuthClient(result.data); + }, + enabled: !!clientId, + retry: false, + staleTime: 5 * 60 * 1000, + }); +} diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 4e2f3199..b62d6a98 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1588,5 +1588,31 @@ "deleteSuccess": "Benutzer gelöscht." } } + }, + "oauth": { + "authorize": { + "title": "Anwendung autorisieren", + "description": "\"{{appName}}\" möchte auf Ihr Aura-Historia-Konto zugreifen. Bitte überprüfen Sie die angeforderten Berechtigungen.", + "scopesTitle": "Diese Anwendung fordert folgende Berechtigungen an:", + "approve": "Autorisieren", + "deny": "Ablehnen", + "approveAriaLabel": "{{appName}} den Zugriff auf Ihr Konto erlauben", + "denyAriaLabel": "Autorisierung für {{appName}} ablehnen", + "securityNote": "Sie können diesen Zugriff jederzeit in Ihren Kontoeinstellungen widerrufen. Autorisieren Sie nur Anwendungen, denen Sie vertrauen.", + "error": { + "title": "Ungültige Autorisierungsanfrage", + "description": "Die Anwendung konnte nicht identifiziert werden. Der Autorisierungslink ist möglicherweise ungültig oder abgelaufen. Bitte versuchen Sie es erneut über die Anwendung." + } + }, + "scopes": { + "products_write": { + "label": "Produkte verwalten", + "description": "Produktangebote in Ihrem Namen erstellen, aktualisieren und verwalten." + }, + "shops_manage": { + "label": "Shops verwalten", + "description": "Auf Ihre Shop-Einstellungen und -Konfiguration zugreifen und diese verwalten." + } + } } } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 03220b4b..95d78947 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -1577,5 +1577,31 @@ "deleteSuccess": "User deleted." } } + }, + "oauth": { + "authorize": { + "title": "Authorize Application", + "description": "\"{{appName}}\" is requesting access to your Aura Historia account. Please review the requested permissions below.", + "scopesTitle": "This application is requesting the following permissions:", + "approve": "Authorize", + "deny": "Decline", + "approveAriaLabel": "Authorize {{appName}} to access your account", + "denyAriaLabel": "Decline authorization for {{appName}}", + "securityNote": "You can revoke this access at any time from your account settings. Only authorize applications you trust.", + "error": { + "title": "Invalid Authorization Request", + "description": "The application could not be identified. The authorization link may be invalid or expired. Please try again from the application." + } + }, + "scopes": { + "products_write": { + "label": "Manage Products", + "description": "Create, update, and manage product listings on your behalf." + }, + "shops_manage": { + "label": "Manage Shops", + "description": "Access and manage your shop settings and configuration." + } + } } } diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 24f46c59..210da8f2 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -1577,5 +1577,31 @@ }, "matchReason": "Razón AI", "feedbackError": "No se pudo guardar el comentario." + }, + "oauth": { + "authorize": { + "title": "Autorizar aplicación", + "description": "\"{{appName}}\" solicita acceso a su cuenta de Aura Historia. Revise los permisos solicitados a continuación.", + "scopesTitle": "Esta aplicación solicita los siguientes permisos:", + "approve": "Autorizar", + "deny": "Rechazar", + "approveAriaLabel": "Autorizar a {{appName}} para acceder a su cuenta", + "denyAriaLabel": "Rechazar la autorización para {{appName}}", + "securityNote": "Puede revocar este acceso en cualquier momento desde la configuración de su cuenta. Solo autorice aplicaciones de confianza.", + "error": { + "title": "Solicitud de autorización inválida", + "description": "No se pudo identificar la aplicación. El enlace de autorización puede ser inválido o haber expirado. Inténtelo de nuevo desde la aplicación." + } + }, + "scopes": { + "products_write": { + "label": "Gestionar productos", + "description": "Crear, actualizar y gestionar listados de productos en su nombre." + }, + "shops_manage": { + "label": "Gestionar tiendas", + "description": "Acceder y gestionar la configuración de su tienda." + } + } } } diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json index d50dd795..60bf707d 100644 --- a/src/i18n/locales/fr/translation.json +++ b/src/i18n/locales/fr/translation.json @@ -1577,5 +1577,31 @@ }, "matchReason": "Raison IA", "feedbackError": "Échec de l'enregistrement des commentaires." + }, + "oauth": { + "authorize": { + "title": "Autoriser l'application", + "description": "\"{{appName}}\" demande l'accès à votre compte Aura Historia. Veuillez vérifier les autorisations demandées ci-dessous.", + "scopesTitle": "Cette application demande les autorisations suivantes :", + "approve": "Autoriser", + "deny": "Refuser", + "approveAriaLabel": "Autoriser {{appName}} à accéder à votre compte", + "denyAriaLabel": "Refuser l'autorisation pour {{appName}}", + "securityNote": "Vous pouvez révoquer cet accès à tout moment depuis les paramètres de votre compte. N'autorisez que les applications de confiance.", + "error": { + "title": "Demande d'autorisation invalide", + "description": "L'application n'a pas pu être identifiée. Le lien d'autorisation est peut-être invalide ou expiré. Veuillez réessayer depuis l'application." + } + }, + "scopes": { + "products_write": { + "label": "Gérer les produits", + "description": "Créer, mettre à jour et gérer les annonces de produits en votre nom." + }, + "shops_manage": { + "label": "Gérer les boutiques", + "description": "Accéder à vos paramètres de boutique et les gérer." + } + } } } diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index e9e0b9c3..e5b3da37 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -1577,5 +1577,31 @@ }, "matchReason": "Motivo AI", "feedbackError": "Impossibile salvare il feedback." + }, + "oauth": { + "authorize": { + "title": "Autorizza applicazione", + "description": "\"{{appName}}\" richiede l'accesso al tuo account Aura Historia. Controlla le autorizzazioni richieste di seguito.", + "scopesTitle": "Questa applicazione richiede le seguenti autorizzazioni:", + "approve": "Autorizza", + "deny": "Rifiuta", + "approveAriaLabel": "Autorizza {{appName}} ad accedere al tuo account", + "denyAriaLabel": "Rifiuta l'autorizzazione per {{appName}}", + "securityNote": "Puoi revocare questo accesso in qualsiasi momento dalle impostazioni del tuo account. Autorizza solo applicazioni di cui ti fidi.", + "error": { + "title": "Richiesta di autorizzazione non valida", + "description": "L'applicazione non è stata identificata. Il link di autorizzazione potrebbe essere non valido o scaduto. Riprova dall'applicazione." + } + }, + "scopes": { + "products_write": { + "label": "Gestisci prodotti", + "description": "Creare, aggiornare e gestire gli annunci di prodotti per tuo conto." + }, + "shops_manage": { + "label": "Gestisci negozi", + "description": "Accedere e gestire le impostazioni del tuo negozio." + } + } } } diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 06c22fdd..b0f8b3a6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -25,6 +25,7 @@ import { Route as AuthAdminRouteImport } from './routes/_auth.admin' import { Route as ShopsShopSlugIdIndexRouteImport } from './routes/shops.$shopSlugId.index' import { Route as AuthAdminIndexRouteImport } from './routes/_auth.admin.index' import { Route as ProductShopIdShopsProductIdRouteImport } from './routes/product.$shopId.$shopsProductId' +import { Route as AuthOauthAuthorizeRouteImport } from './routes/_auth.oauth.authorize' import { Route as AuthMeWatchlistRouteImport } from './routes/_auth.me.watchlist' import { Route as AuthMeSearchFiltersRouteImport } from './routes/_auth.me.search-filters' import { Route as AuthMeNotificationsRouteImport } from './routes/_auth.me.notifications' @@ -117,6 +118,11 @@ const ProductShopIdShopsProductIdRoute = path: '/product/$shopId/$shopsProductId', getParentRoute: () => rootRouteImport, } as any) +const AuthOauthAuthorizeRoute = AuthOauthAuthorizeRouteImport.update({ + id: '/oauth/authorize', + path: '/oauth/authorize', + getParentRoute: () => AuthRoute, +} as any) const AuthMeWatchlistRoute = AuthMeWatchlistRouteImport.update({ id: '/me/watchlist', path: '/me/watchlist', @@ -191,6 +197,7 @@ export interface FileRoutesByFullPath { '/me/notifications': typeof AuthMeNotificationsRoute '/me/search-filters': typeof AuthMeSearchFiltersRoute '/me/watchlist': typeof AuthMeWatchlistRoute + '/oauth/authorize': typeof AuthOauthAuthorizeRoute '/product/$shopId/$shopsProductId': typeof ProductShopIdShopsProductIdRoute '/admin/': typeof AuthAdminIndexRoute '/shops/$shopSlugId/': typeof ShopsShopSlugIdIndexRoute @@ -216,6 +223,7 @@ export interface FileRoutesByTo { '/me/notifications': typeof AuthMeNotificationsRoute '/me/search-filters': typeof AuthMeSearchFiltersRoute '/me/watchlist': typeof AuthMeWatchlistRoute + '/oauth/authorize': typeof AuthOauthAuthorizeRoute '/product/$shopId/$shopsProductId': typeof ProductShopIdShopsProductIdRoute '/admin': typeof AuthAdminIndexRoute '/shops/$shopSlugId': typeof ShopsShopSlugIdIndexRoute @@ -245,6 +253,7 @@ export interface FileRoutesById { '/_auth/me/notifications': typeof AuthMeNotificationsRoute '/_auth/me/search-filters': typeof AuthMeSearchFiltersRoute '/_auth/me/watchlist': typeof AuthMeWatchlistRoute + '/_auth/oauth/authorize': typeof AuthOauthAuthorizeRoute '/product/$shopId/$shopsProductId': typeof ProductShopIdShopsProductIdRoute '/_auth/admin/': typeof AuthAdminIndexRoute '/shops/$shopSlugId/': typeof ShopsShopSlugIdIndexRoute @@ -274,6 +283,7 @@ export interface FileRouteTypes { | '/me/notifications' | '/me/search-filters' | '/me/watchlist' + | '/oauth/authorize' | '/product/$shopId/$shopsProductId' | '/admin/' | '/shops/$shopSlugId/' @@ -299,6 +309,7 @@ export interface FileRouteTypes { | '/me/notifications' | '/me/search-filters' | '/me/watchlist' + | '/oauth/authorize' | '/product/$shopId/$shopsProductId' | '/admin' | '/shops/$shopSlugId' @@ -327,6 +338,7 @@ export interface FileRouteTypes { | '/_auth/me/notifications' | '/_auth/me/search-filters' | '/_auth/me/watchlist' + | '/_auth/oauth/authorize' | '/product/$shopId/$shopsProductId' | '/_auth/admin/' | '/shops/$shopSlugId/' @@ -465,6 +477,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProductShopIdShopsProductIdRouteImport parentRoute: typeof rootRouteImport } + '/_auth/oauth/authorize': { + id: '/_auth/oauth/authorize' + path: '/oauth/authorize' + fullPath: '/oauth/authorize' + preLoaderRoute: typeof AuthOauthAuthorizeRouteImport + parentRoute: typeof AuthRoute + } '/_auth/me/watchlist': { id: '/_auth/me/watchlist' path: '/me/watchlist' @@ -562,6 +581,7 @@ interface AuthRouteChildren { AuthMeNotificationsRoute: typeof AuthMeNotificationsRoute AuthMeSearchFiltersRoute: typeof AuthMeSearchFiltersRoute AuthMeWatchlistRoute: typeof AuthMeWatchlistRoute + AuthOauthAuthorizeRoute: typeof AuthOauthAuthorizeRoute AuthMeBillingManageRoute: typeof AuthMeBillingManageRoute AuthMeSearchFilterFilterIdRoute: typeof AuthMeSearchFilterFilterIdRoute } @@ -572,6 +592,7 @@ const AuthRouteChildren: AuthRouteChildren = { AuthMeNotificationsRoute: AuthMeNotificationsRoute, AuthMeSearchFiltersRoute: AuthMeSearchFiltersRoute, AuthMeWatchlistRoute: AuthMeWatchlistRoute, + AuthOauthAuthorizeRoute: AuthOauthAuthorizeRoute, AuthMeBillingManageRoute: AuthMeBillingManageRoute, AuthMeSearchFilterFilterIdRoute: AuthMeSearchFilterFilterIdRoute, } diff --git a/src/routes/_auth.oauth.authorize.tsx b/src/routes/_auth.oauth.authorize.tsx new file mode 100644 index 00000000..92382c6d --- /dev/null +++ b/src/routes/_auth.oauth.authorize.tsx @@ -0,0 +1,27 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { OAuthAuthorizePage } from "@/components/oauth/OAuthAuthorizePage.tsx"; +import { z } from "zod"; + +const oauthAuthorizeSearchSchema = z.object({ + response_type: z.string().default("code"), + client_id: z.string(), + redirect_uri: z.string(), + scope: z.string().optional(), + state: z.string().optional(), + code_challenge: z.string(), + code_challenge_method: z.string().default("S256"), +}); + +export const Route = createFileRoute("/_auth/oauth/authorize")({ + ssr: false, + head: () => ({ + meta: [{ name: "robots", content: "noindex, nofollow" }], + }), + validateSearch: (search: Record) => oauthAuthorizeSearchSchema.parse(search), + component: OAuthAuthorizeRouteComponent, +}); + +function OAuthAuthorizeRouteComponent() { + const searchParams = Route.useSearch(); + return ; +} From ca236333a1dd7686bba60773e909f0b60f25e383 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 11:43:24 +0000 Subject: [PATCH 3/3] feat: enrich oauth authorization consent details --- src/client/types.gen.ts | 52 ++++++++- src/components/oauth/OAuthAuthorizePage.tsx | 103 +++++++++++++----- .../__tests__/OAuthAuthorizePage.test.tsx | 45 ++++++-- src/data/internal/oauth/OAuthClient.ts | 8 ++ .../oauth/__tests__/OAuthClient.test.ts | 20 ++++ src/i18n/locales/de/translation.json | 8 +- src/i18n/locales/en/translation.json | 8 +- src/i18n/locales/es/translation.json | 8 +- src/i18n/locales/fr/translation.json | 8 +- src/i18n/locales/it/translation.json | 8 +- 10 files changed, 219 insertions(+), 49 deletions(-) diff --git a/src/client/types.gen.ts b/src/client/types.gen.ts index 6933923c..e8983fbf 100644 --- a/src/client/types.gen.ts +++ b/src/client/types.gen.ts @@ -1708,6 +1708,22 @@ export type OAuthClientMetadataRequestData = { * Display name of the OAuth client. */ client_name: string; + /** + * HTTPS URL of the OAuth client's terms-of-service document. + */ + tos_uri: string; + /** + * HTTPS URL of the OAuth client's privacy policy document. + */ + policy_uri: string; + /** + * HTTPS URL of the OAuth client's homepage or product site. + */ + client_uri: string; + /** + * HTTPS URL of the OAuth client's logo image. + */ + logo_uri: string; /** * Registered HTTPS redirect URIs for the OAuth client. */ @@ -1728,6 +1744,22 @@ export type OAuthClientMetadataPatchData = { * Optional replacement display name for the OAuth client. */ client_name?: string | null; + /** + * Optional replacement terms-of-service document URL for the OAuth client. + */ + tos_uri?: string | null; + /** + * Optional replacement privacy-policy document URL for the OAuth client. + */ + policy_uri?: string | null; + /** + * Optional replacement homepage or product-site URL for the OAuth client. + */ + client_uri?: string | null; + /** + * Optional replacement logo image URL for the OAuth client. + */ + logo_uri?: string | null; /** * Optional replacement set of registered HTTPS redirect URIs. */ @@ -1759,6 +1791,22 @@ export type OAuthClientMetadataResponseData = { * Display name of the OAuth client. */ client_name: string; + /** + * Terms-of-service document URL registered for the OAuth client. + */ + tos_uri: string; + /** + * Privacy-policy document URL registered for the OAuth client. + */ + policy_uri: string; + /** + * Homepage or product-site URL registered for the OAuth client. + */ + client_uri: string; + /** + * Logo image URL registered for the OAuth client. + */ + logo_uri: string; /** * Registered redirect URIs for the OAuth client. */ @@ -6819,7 +6867,7 @@ export type OauthAuthorizeData = { export type OauthAuthorizeErrors = { /** - * Bad request — a required query parameter is missing or has an unsupported value. + * Bad request — a required query parameter is missing, malformed, or has an unsupported value. */ 400: ApiError; /** @@ -6871,7 +6919,7 @@ export type OauthTokenData = { export type OauthTokenErrors = { /** - * Bad request — a required form field is missing, `client_id` is not a valid UUID, or an authorization code error occurred (code not found, expired, mismatched client or redirect URI, or invalid PKCE verifier). + * Bad request — a required form field is missing, `client_id` is not a valid UUID, `redirect_uri` is malformed, or an authorization code error occurred (code not found, expired, mismatched client or redirect URI, or invalid PKCE verifier). */ 400: ApiError; /** diff --git a/src/components/oauth/OAuthAuthorizePage.tsx b/src/components/oauth/OAuthAuthorizePage.tsx index f26f2afe..b3c84c24 100644 --- a/src/components/oauth/OAuthAuthorizePage.tsx +++ b/src/components/oauth/OAuthAuthorizePage.tsx @@ -5,8 +5,8 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/componen import { Button } from "@/components/ui/button.tsx"; import { H1 } from "@/components/typography/H1.tsx"; import { Spinner } from "@/components/ui/spinner.tsx"; -import { ShieldCheck, ShieldAlert, AlertTriangle } from "lucide-react"; -import type { OAuthScope } from "@/data/internal/oauth/OAuthClient.ts"; +import { Badge } from "@/components/ui/badge.tsx"; +import { ShieldCheck, ShieldAlert, AlertTriangle, ExternalLink } from "lucide-react"; type OAuthAuthorizeSearchParams = { readonly response_type: string; @@ -28,6 +28,23 @@ export function OAuthAuthorizePage({ searchParams }: OAuthAuthorizePageProps) { const authorize = useOAuthAuthorize(); const requestedScopes = searchParams.scope?.split(" ").filter(Boolean) ?? []; + const clientLogoUri = getSafeHttpsUrl(client?.logoUri); + const clientLinks = client + ? [ + { + href: getSafeHttpsUrl(client.clientUri), + label: t("oauth.authorize.clientInfoLink"), + }, + { + href: getSafeHttpsUrl(client.policyUri), + label: t("oauth.authorize.privacyLink"), + }, + { + href: getSafeHttpsUrl(client.tosUri), + label: t("oauth.authorize.termsLink"), + }, + ].filter((link): link is { href: string; label: string } => !!link.href) + : []; const handleApprove = () => { authorize.mutate( @@ -94,14 +111,42 @@ export function OAuthAuthorizePage({ searchParams }: OAuthAuthorizePageProps) {

    {t("oauth.authorize.title")}

    - - - + +
    + {clientLogoUri ? ( + {t("oauth.authorize.logoAlt", + ) : ( +
    +
    + {client.clientName} + {clientLinks.length > 0 && ( +
    + {clientLinks.map((link) => ( + + {link.label} + + ))} +
    + )} +
    @@ -121,7 +166,7 @@ export function OAuthAuthorizePage({ searchParams }: OAuthAuthorizePageProps) { aria-label={t("oauth.authorize.scopesTitle")} > {requestedScopes.map((scope) => ( - + ))} @@ -184,28 +229,36 @@ function PageContainer({ children }: { readonly children: React.ReactNode }) { ); } -const SCOPE_ICONS: Record = { - "products:write": "📦", - "shops:manage": "🏪", -}; - -function ScopeItem({ scope }: { readonly scope: OAuthScope }) { +function ScopeItem({ scope }: { readonly scope: string }) { const { t } = useTranslation(); const scopeKey = scope.replace(":", "_"); - const label = t(`oauth.scopes.${scopeKey}.label`); const description = t(`oauth.scopes.${scopeKey}.description`); - const icon = SCOPE_ICONS[scope] ?? "🔑"; return ( -
  • - -
    - {label} +
  • +
    + + {scope} + {description}
  • ); } + +function getSafeHttpsUrl(url: string | undefined): string | undefined { + if (!url) { + return undefined; + } + + try { + const parsedUrl = new URL(url); + return parsedUrl.protocol === "https:" ? url : undefined; + } catch { + return undefined; + } +} diff --git a/src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx b/src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx index ca3a7309..39cfa8cd 100644 --- a/src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx +++ b/src/components/oauth/__tests__/OAuthAuthorizePage.test.tsx @@ -9,6 +9,10 @@ const mockMutate = vi.hoisted(() => vi.fn()); const mockClientData = vi.hoisted(() => ({ clientId: "01970f22-2bf0-7000-8000-000000000010", clientName: "Test Partner App", + tosUri: "https://client.example/terms", + policyUri: "https://client.example/privacy", + clientUri: "https://client.example", + logoUri: "https://client.example/logo.png", redirectUris: ["https://client.example/callback"], scopes: ["products:write" as const, "shops:manage" as const], })); @@ -73,23 +77,40 @@ describe("OAuthAuthorizePage", () => { expect(screen.getByText("Test Partner App")).toBeInTheDocument(); }); - it("displays requested scopes with labels and descriptions", async () => { + it("displays app logo and client metadata links", async () => { await act(async () => renderWithRouter(), ); - expect(screen.getByText("Produkte verwalten")).toBeInTheDocument(); + expect(screen.getByAltText("Logo von Test Partner App")).toBeInTheDocument(); expect( - screen.getByText( - "Produktangebote in Ihrem Namen erstellen, aktualisieren und verwalten.", - ), - ).toBeInTheDocument(); - expect(screen.getByText("Shops verwalten")).toBeInTheDocument(); + screen.getByRole("link", { + name: "Mehr über diese App", + }), + ).toHaveAttribute("href", "https://client.example"); + expect( + screen.getByRole("link", { + name: "Datenschutz", + }), + ).toHaveAttribute("href", "https://client.example/privacy"); + expect( + screen.getByRole("link", { + name: "Nutzungsbedingungen", + }), + ).toHaveAttribute("href", "https://client.example/terms"); + }); + + it("displays requested scope tags with short descriptions", async () => { + await act(async () => + renderWithRouter(), + ); + + expect(screen.getByText("products:write")).toBeInTheDocument(); expect( - screen.getByText( - "Auf Ihre Shop-Einstellungen und -Konfiguration zugreifen und diese verwalten.", - ), + screen.getByText("Produkte in Ihrem Namen erstellen oder aktualisieren."), ).toBeInTheDocument(); + expect(screen.getByText("shops:manage")).toBeInTheDocument(); + expect(screen.getByText("Ihre Shop-Einstellungen verwalten.")).toBeInTheDocument(); }); it("displays the authorization description with app name", async () => { @@ -310,7 +331,7 @@ describe("OAuthAuthorizePage", () => { renderWithRouter(), ); - expect(screen.getByText("Produkte verwalten")).toBeInTheDocument(); - expect(screen.queryByText("Shops verwalten")).not.toBeInTheDocument(); + expect(screen.getByText("products:write")).toBeInTheDocument(); + expect(screen.queryByText("shops:manage")).not.toBeInTheDocument(); }); }); diff --git a/src/data/internal/oauth/OAuthClient.ts b/src/data/internal/oauth/OAuthClient.ts index 3176f01c..3c26cee4 100644 --- a/src/data/internal/oauth/OAuthClient.ts +++ b/src/data/internal/oauth/OAuthClient.ts @@ -5,6 +5,10 @@ export type OAuthScope = "shops:manage" | "products:write"; export type OAuthClient = { readonly clientId: string; readonly clientName: string; + readonly tosUri: string; + readonly policyUri: string; + readonly clientUri: string; + readonly logoUri: string; readonly redirectUris: readonly string[]; readonly scopes: readonly OAuthScope[]; }; @@ -17,6 +21,10 @@ export function mapToInternalOAuthClient(data: OAuthClientMetadataResponseData): return { clientId: data.client_id, clientName: data.client_name, + tosUri: data.tos_uri, + policyUri: data.policy_uri, + clientUri: data.client_uri, + logoUri: data.logo_uri, redirectUris: data.redirect_uris, scopes: data.scope.map(mapScope), }; diff --git a/src/data/internal/oauth/__tests__/OAuthClient.test.ts b/src/data/internal/oauth/__tests__/OAuthClient.test.ts index 709ae6a2..c81b887e 100644 --- a/src/data/internal/oauth/__tests__/OAuthClient.test.ts +++ b/src/data/internal/oauth/__tests__/OAuthClient.test.ts @@ -8,6 +8,10 @@ describe("mapToInternalOAuthClient", () => { client_id: "01970f22-2bf0-7000-8000-000000000010", client_secret: "aurahistoria_oauth_client_secret_abcdefghijk_****", client_name: "Test OAuth App", + tos_uri: "https://client.example/tos", + policy_uri: "https://client.example/privacy", + client_uri: "https://client.example", + logo_uri: "https://client.example/logo.png", redirect_uris: ["https://client.example/callback"], scope: ["products:write"], client_id_issued_at: 1748539200, @@ -17,6 +21,10 @@ describe("mapToInternalOAuthClient", () => { expect(result.clientId).toBe("01970f22-2bf0-7000-8000-000000000010"); expect(result.clientName).toBe("Test OAuth App"); + expect(result.tosUri).toBe("https://client.example/tos"); + expect(result.policyUri).toBe("https://client.example/privacy"); + expect(result.clientUri).toBe("https://client.example"); + expect(result.logoUri).toBe("https://client.example/logo.png"); expect(result.redirectUris).toEqual(["https://client.example/callback"]); expect(result.scopes).toEqual(["products:write"]); }); @@ -26,6 +34,10 @@ describe("mapToInternalOAuthClient", () => { client_id: "01970f22-2bf0-7000-8000-000000000010", client_secret: "masked", client_name: "Multi-Scope App", + tos_uri: "https://client.example/tos", + policy_uri: "https://client.example/privacy", + client_uri: "https://client.example", + logo_uri: "https://client.example/logo.png", redirect_uris: ["https://client.example/callback", "https://client.example/auth"], scope: ["products:write", "shops:manage"], client_id_issued_at: 1748539200, @@ -42,6 +54,10 @@ describe("mapToInternalOAuthClient", () => { client_id: "01970f22-2bf0-7000-8000-000000000010", client_secret: "masked", client_name: "No Scope App", + tos_uri: "https://client.example/tos", + policy_uri: "https://client.example/privacy", + client_uri: "https://client.example", + logo_uri: "https://client.example/logo.png", redirect_uris: ["https://client.example/callback"], scope: [], client_id_issued_at: 1748539200, @@ -57,6 +73,10 @@ describe("mapToInternalOAuthClient", () => { client_id: "01970f22-2bf0-7000-8000-000000000010", client_secret: "secret-value", client_name: "App", + tos_uri: "https://client.example/tos", + policy_uri: "https://client.example/privacy", + client_uri: "https://client.example", + logo_uri: "https://client.example/logo.png", redirect_uris: ["https://example.com/cb"], scope: ["products:write"], client_id_issued_at: 1748539200, diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index b62d6a98..df7d501a 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1594,6 +1594,10 @@ "title": "Anwendung autorisieren", "description": "\"{{appName}}\" möchte auf Ihr Aura-Historia-Konto zugreifen. Bitte überprüfen Sie die angeforderten Berechtigungen.", "scopesTitle": "Diese Anwendung fordert folgende Berechtigungen an:", + "logoAlt": "Logo von {{appName}}", + "clientInfoLink": "Mehr über diese App", + "privacyLink": "Datenschutz", + "termsLink": "Nutzungsbedingungen", "approve": "Autorisieren", "deny": "Ablehnen", "approveAriaLabel": "{{appName}} den Zugriff auf Ihr Konto erlauben", @@ -1607,11 +1611,11 @@ "scopes": { "products_write": { "label": "Produkte verwalten", - "description": "Produktangebote in Ihrem Namen erstellen, aktualisieren und verwalten." + "description": "Produkte in Ihrem Namen erstellen oder aktualisieren." }, "shops_manage": { "label": "Shops verwalten", - "description": "Auf Ihre Shop-Einstellungen und -Konfiguration zugreifen und diese verwalten." + "description": "Ihre Shop-Einstellungen verwalten." } } } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 95d78947..bc938c8a 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -1583,6 +1583,10 @@ "title": "Authorize Application", "description": "\"{{appName}}\" is requesting access to your Aura Historia account. Please review the requested permissions below.", "scopesTitle": "This application is requesting the following permissions:", + "logoAlt": "{{appName}} logo", + "clientInfoLink": "About this app", + "privacyLink": "Privacy", + "termsLink": "Terms", "approve": "Authorize", "deny": "Decline", "approveAriaLabel": "Authorize {{appName}} to access your account", @@ -1596,11 +1600,11 @@ "scopes": { "products_write": { "label": "Manage Products", - "description": "Create, update, and manage product listings on your behalf." + "description": "Create or update your product listings." }, "shops_manage": { "label": "Manage Shops", - "description": "Access and manage your shop settings and configuration." + "description": "Manage your shop settings." } } } diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 210da8f2..005479ab 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -1583,6 +1583,10 @@ "title": "Autorizar aplicación", "description": "\"{{appName}}\" solicita acceso a su cuenta de Aura Historia. Revise los permisos solicitados a continuación.", "scopesTitle": "Esta aplicación solicita los siguientes permisos:", + "logoAlt": "Logo de {{appName}}", + "clientInfoLink": "Más sobre esta app", + "privacyLink": "Privacidad", + "termsLink": "Condiciones de uso", "approve": "Autorizar", "deny": "Rechazar", "approveAriaLabel": "Autorizar a {{appName}} para acceder a su cuenta", @@ -1596,11 +1600,11 @@ "scopes": { "products_write": { "label": "Gestionar productos", - "description": "Crear, actualizar y gestionar listados de productos en su nombre." + "description": "Crear o actualizar sus anuncios de productos." }, "shops_manage": { "label": "Gestionar tiendas", - "description": "Acceder y gestionar la configuración de su tienda." + "description": "Gestionar la configuración de su tienda." } } } diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json index 60bf707d..7f7a99fa 100644 --- a/src/i18n/locales/fr/translation.json +++ b/src/i18n/locales/fr/translation.json @@ -1583,6 +1583,10 @@ "title": "Autoriser l'application", "description": "\"{{appName}}\" demande l'accès à votre compte Aura Historia. Veuillez vérifier les autorisations demandées ci-dessous.", "scopesTitle": "Cette application demande les autorisations suivantes :", + "logoAlt": "Logo de {{appName}}", + "clientInfoLink": "À propos de cette application", + "privacyLink": "Confidentialité", + "termsLink": "Conditions d'utilisation", "approve": "Autoriser", "deny": "Refuser", "approveAriaLabel": "Autoriser {{appName}} à accéder à votre compte", @@ -1596,11 +1600,11 @@ "scopes": { "products_write": { "label": "Gérer les produits", - "description": "Créer, mettre à jour et gérer les annonces de produits en votre nom." + "description": "Créer ou mettre à jour vos annonces produits." }, "shops_manage": { "label": "Gérer les boutiques", - "description": "Accéder à vos paramètres de boutique et les gérer." + "description": "Gérer les paramètres de votre boutique." } } } diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index e5b3da37..fde3db42 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -1583,6 +1583,10 @@ "title": "Autorizza applicazione", "description": "\"{{appName}}\" richiede l'accesso al tuo account Aura Historia. Controlla le autorizzazioni richieste di seguito.", "scopesTitle": "Questa applicazione richiede le seguenti autorizzazioni:", + "logoAlt": "Logo di {{appName}}", + "clientInfoLink": "Maggiori informazioni", + "privacyLink": "Privacy", + "termsLink": "Termini di servizio", "approve": "Autorizza", "deny": "Rifiuta", "approveAriaLabel": "Autorizza {{appName}} ad accedere al tuo account", @@ -1596,11 +1600,11 @@ "scopes": { "products_write": { "label": "Gestisci prodotti", - "description": "Creare, aggiornare e gestire gli annunci di prodotti per tuo conto." + "description": "Creare o aggiornare i tuoi annunci di prodotto." }, "shops_manage": { "label": "Gestisci negozi", - "description": "Accedere e gestire le impostazioni del tuo negozio." + "description": "Gestire le impostazioni del tuo negozio." } } }