@sp-api-sdk/auth
Advanced tools
@@ -5,3 +5,10 @@ "use strict"; | ||
| const axios_1 = require("axios"); | ||
| /** | ||
| * Error thrown when an LWA token request fails. | ||
| * | ||
| * Wraps the underlying Axios error with a human-readable message that includes | ||
| * the HTTP status code (or "No response" for network errors). | ||
| */ | ||
| class SellingPartnerApiAuthError extends axios_1.AxiosError { | ||
| /** The original error message from the failed HTTP request. */ | ||
| innerMessage; | ||
@@ -8,0 +15,0 @@ constructor(error) { |
+53
-30
@@ -12,3 +12,7 @@ "use strict"; | ||
| /** | ||
| * Class for simplify auth with Selling Partner API | ||
| * Handles Login with Amazon (LWA) OAuth token management for the Selling Partner API. | ||
| * | ||
| * Supports both refresh-token and grantless (scope-based) authentication flows. | ||
| * Tokens are cached and automatically refreshed when expired. Concurrent calls | ||
| * to {@link getAccessToken} are deduplicated into a single request. | ||
| */ | ||
@@ -22,2 +26,3 @@ class SellingPartnerApiAuth { | ||
| #accessTokenExpiration; | ||
| #pendingTokenRequest; | ||
| constructor(parameters) { | ||
@@ -42,38 +47,56 @@ const clientId = parameters.clientId ?? node_process_1.default.env.LWA_CLIENT_ID; | ||
| /** | ||
| * Get access token | ||
| * Returns a valid LWA access token, refreshing it if expired. | ||
| * | ||
| * Concurrent calls while a refresh is in progress share the same request. | ||
| * | ||
| * @returns The access token string. | ||
| */ | ||
| async getAccessToken() { | ||
| if (!this.#accessToken || | ||
| (this.#accessTokenExpiration && Date.now() >= this.#accessTokenExpiration.getTime())) { | ||
| const body = { | ||
| client_id: this.clientId, | ||
| client_secret: this.clientSecret, | ||
| ...(this.refreshToken | ||
| ? { | ||
| grant_type: 'refresh_token', | ||
| refresh_token: this.refreshToken, | ||
| } | ||
| : { | ||
| grant_type: 'client_credentials', | ||
| scope: this.scopes.join(' '), | ||
| }), | ||
| }; | ||
| try { | ||
| const expiration = new Date(); | ||
| const { data } = await axios_2.axios.post('/o2/token', body); | ||
| expiration.setSeconds(expiration.getSeconds() + data.expires_in); | ||
| this.#accessToken = data.access_token; | ||
| this.#accessTokenExpiration = expiration; | ||
| } | ||
| catch (error) { | ||
| if (error instanceof axios_1.AxiosError) { | ||
| throw new error_1.SellingPartnerApiAuthError(error); | ||
| if (this.#accessToken && | ||
| (!this.#accessTokenExpiration || Date.now() < this.#accessTokenExpiration.getTime())) { | ||
| return this.#accessToken; | ||
| } | ||
| // Deduplicate concurrent calls: share the same in-flight request | ||
| if (this.#pendingTokenRequest) { | ||
| return this.#pendingTokenRequest; | ||
| } | ||
| this.#pendingTokenRequest = this.#refreshAccessToken(); | ||
| try { | ||
| return await this.#pendingTokenRequest; | ||
| } | ||
| finally { | ||
| this.#pendingTokenRequest = undefined; | ||
| } | ||
| } | ||
| async #refreshAccessToken() { | ||
| const body = { | ||
| client_id: this.clientId, | ||
| client_secret: this.clientSecret, | ||
| ...(this.refreshToken | ||
| ? { | ||
| grant_type: 'refresh_token', | ||
| refresh_token: this.refreshToken, | ||
| } | ||
| throw error; | ||
| : { | ||
| grant_type: 'client_credentials', | ||
| scope: this.scopes.join(' '), | ||
| }), | ||
| }; | ||
| try { | ||
| const expiration = new Date(); | ||
| const { data } = await axios_2.axios.post('/o2/token', body); | ||
| expiration.setSeconds(expiration.getSeconds() + data.expires_in); | ||
| this.#accessToken = data.access_token; | ||
| this.#accessTokenExpiration = expiration; | ||
| return data.access_token; | ||
| } | ||
| catch (error) { | ||
| if (error instanceof axios_1.AxiosError) { | ||
| throw new error_1.SellingPartnerApiAuthError(error); | ||
| } | ||
| throw error; | ||
| } | ||
| return this.#accessToken; | ||
| } | ||
| /** | ||
| * Access token expiration date | ||
| * Expiration date of the currently cached access token, or `undefined` if no token has been fetched yet. | ||
| */ | ||
@@ -80,0 +103,0 @@ get accessTokenExpiration() { |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.AuthorizationScope = void 0; | ||
| /** Authorization scopes for grantless Selling Partner API operations. */ | ||
| var AuthorizationScope; | ||
| (function (AuthorizationScope) { | ||
| /** Scope for the Notifications API. */ | ||
| AuthorizationScope["NOTIFICATIONS"] = "sellingpartnerapi::notifications"; | ||
| /** Scope for rotating application client credentials. */ | ||
| AuthorizationScope["CLIENT_CREDENTIAL_ROTATION"] = "sellingpartnerapi::client_credential:rotation"; | ||
| })(AuthorizationScope || (exports.AuthorizationScope = AuthorizationScope = {})); |
@@ -11,2 +11,3 @@ "use strict"; | ||
| baseURL: 'https://api.amazon.com/auth/', | ||
| timeout: 30_000, | ||
| headers: { | ||
@@ -13,0 +14,0 @@ 'user-agent': `${package_1.packageJson.name}/${package_1.packageJson.version}`, |
+7
-0
| import { AxiosError } from 'axios'; | ||
| /** | ||
| * Error thrown when an LWA token request fails. | ||
| * | ||
| * Wraps the underlying Axios error with a human-readable message that includes | ||
| * the HTTP status code (or "No response" for network errors). | ||
| */ | ||
| export class SellingPartnerApiAuthError extends AxiosError { | ||
| /** The original error message from the failed HTTP request. */ | ||
| innerMessage; | ||
@@ -4,0 +11,0 @@ constructor(error) { |
+53
-30
@@ -6,3 +6,7 @@ import process from 'node:process'; | ||
| /** | ||
| * Class for simplify auth with Selling Partner API | ||
| * Handles Login with Amazon (LWA) OAuth token management for the Selling Partner API. | ||
| * | ||
| * Supports both refresh-token and grantless (scope-based) authentication flows. | ||
| * Tokens are cached and automatically refreshed when expired. Concurrent calls | ||
| * to {@link getAccessToken} are deduplicated into a single request. | ||
| */ | ||
@@ -16,2 +20,3 @@ export class SellingPartnerApiAuth { | ||
| #accessTokenExpiration; | ||
| #pendingTokenRequest; | ||
| constructor(parameters) { | ||
@@ -36,38 +41,56 @@ const clientId = parameters.clientId ?? process.env.LWA_CLIENT_ID; | ||
| /** | ||
| * Get access token | ||
| * Returns a valid LWA access token, refreshing it if expired. | ||
| * | ||
| * Concurrent calls while a refresh is in progress share the same request. | ||
| * | ||
| * @returns The access token string. | ||
| */ | ||
| async getAccessToken() { | ||
| if (!this.#accessToken || | ||
| (this.#accessTokenExpiration && Date.now() >= this.#accessTokenExpiration.getTime())) { | ||
| const body = { | ||
| client_id: this.clientId, | ||
| client_secret: this.clientSecret, | ||
| ...(this.refreshToken | ||
| ? { | ||
| grant_type: 'refresh_token', | ||
| refresh_token: this.refreshToken, | ||
| } | ||
| : { | ||
| grant_type: 'client_credentials', | ||
| scope: this.scopes.join(' '), | ||
| }), | ||
| }; | ||
| try { | ||
| const expiration = new Date(); | ||
| const { data } = await axios.post('/o2/token', body); | ||
| expiration.setSeconds(expiration.getSeconds() + data.expires_in); | ||
| this.#accessToken = data.access_token; | ||
| this.#accessTokenExpiration = expiration; | ||
| } | ||
| catch (error) { | ||
| if (error instanceof AxiosError) { | ||
| throw new SellingPartnerApiAuthError(error); | ||
| if (this.#accessToken && | ||
| (!this.#accessTokenExpiration || Date.now() < this.#accessTokenExpiration.getTime())) { | ||
| return this.#accessToken; | ||
| } | ||
| // Deduplicate concurrent calls: share the same in-flight request | ||
| if (this.#pendingTokenRequest) { | ||
| return this.#pendingTokenRequest; | ||
| } | ||
| this.#pendingTokenRequest = this.#refreshAccessToken(); | ||
| try { | ||
| return await this.#pendingTokenRequest; | ||
| } | ||
| finally { | ||
| this.#pendingTokenRequest = undefined; | ||
| } | ||
| } | ||
| async #refreshAccessToken() { | ||
| const body = { | ||
| client_id: this.clientId, | ||
| client_secret: this.clientSecret, | ||
| ...(this.refreshToken | ||
| ? { | ||
| grant_type: 'refresh_token', | ||
| refresh_token: this.refreshToken, | ||
| } | ||
| throw error; | ||
| : { | ||
| grant_type: 'client_credentials', | ||
| scope: this.scopes.join(' '), | ||
| }), | ||
| }; | ||
| try { | ||
| const expiration = new Date(); | ||
| const { data } = await axios.post('/o2/token', body); | ||
| expiration.setSeconds(expiration.getSeconds() + data.expires_in); | ||
| this.#accessToken = data.access_token; | ||
| this.#accessTokenExpiration = expiration; | ||
| return data.access_token; | ||
| } | ||
| catch (error) { | ||
| if (error instanceof AxiosError) { | ||
| throw new SellingPartnerApiAuthError(error); | ||
| } | ||
| throw error; | ||
| } | ||
| return this.#accessToken; | ||
| } | ||
| /** | ||
| * Access token expiration date | ||
| * Expiration date of the currently cached access token, or `undefined` if no token has been fetched yet. | ||
| */ | ||
@@ -74,0 +97,0 @@ get accessTokenExpiration() { |
@@ -0,5 +1,8 @@ | ||
| /** Authorization scopes for grantless Selling Partner API operations. */ | ||
| export var AuthorizationScope; | ||
| (function (AuthorizationScope) { | ||
| /** Scope for the Notifications API. */ | ||
| AuthorizationScope["NOTIFICATIONS"] = "sellingpartnerapi::notifications"; | ||
| /** Scope for rotating application client credentials. */ | ||
| AuthorizationScope["CLIENT_CREDENTIAL_ROTATION"] = "sellingpartnerapi::client_credential:rotation"; | ||
| })(AuthorizationScope || (AuthorizationScope = {})); |
@@ -5,2 +5,3 @@ import globalAxios from 'axios'; | ||
| baseURL: 'https://api.amazon.com/auth/', | ||
| timeout: 30_000, | ||
| headers: { | ||
@@ -7,0 +8,0 @@ 'user-agent': `${packageJson.name}/${packageJson.version}`, |
| import { AxiosError } from 'axios'; | ||
| import type { AccessTokenData, AccessTokenQuery } from './types/access-token'; | ||
| /** | ||
| * Error thrown when an LWA token request fails. | ||
| * | ||
| * Wraps the underlying Axios error with a human-readable message that includes | ||
| * the HTTP status code (or "No response" for network errors). | ||
| */ | ||
| export declare class SellingPartnerApiAuthError extends AxiosError<AccessTokenData, AccessTokenQuery> { | ||
| /** The original error message from the failed HTTP request. */ | ||
| readonly innerMessage: string; | ||
| constructor(error: AxiosError<AccessTokenData, AccessTokenQuery>); | ||
| } |
| import { type RequireExactlyOne } from 'type-fest'; | ||
| import { type AuthorizationScope } from './types/scope'; | ||
| /** | ||
| * Configuration parameters for Selling Partner API authentication. | ||
| * | ||
| * Both `clientId` and `clientSecret` fall back to the `LWA_CLIENT_ID` and | ||
| * `LWA_CLIENT_SECRET` environment variables when omitted. | ||
| * `refreshToken` falls back to `LWA_REFRESH_TOKEN`. | ||
| */ | ||
| export interface SellingPartnerAuthParameters { | ||
| /** LWA client identifier. Defaults to the `LWA_CLIENT_ID` environment variable. */ | ||
| clientId?: string; | ||
| /** LWA client secret. Defaults to the `LWA_CLIENT_SECRET` environment variable. */ | ||
| clientSecret?: string; | ||
| /** LWA refresh token. Defaults to the `LWA_REFRESH_TOKEN` environment variable. Mutually exclusive with `scopes`. */ | ||
| refreshToken?: string; | ||
| /** Authorization scopes for grantless operations. Mutually exclusive with `refreshToken`. */ | ||
| scopes?: AuthorizationScope[]; | ||
| } | ||
| /** | ||
| * Class for simplify auth with Selling Partner API | ||
| * Handles Login with Amazon (LWA) OAuth token management for the Selling Partner API. | ||
| * | ||
| * Supports both refresh-token and grantless (scope-based) authentication flows. | ||
| * Tokens are cached and automatically refreshed when expired. Concurrent calls | ||
| * to {@link getAccessToken} are deduplicated into a single request. | ||
| */ | ||
@@ -20,7 +35,11 @@ export declare class SellingPartnerApiAuth { | ||
| /** | ||
| * Get access token | ||
| * Returns a valid LWA access token, refreshing it if expired. | ||
| * | ||
| * Concurrent calls while a refresh is in progress share the same request. | ||
| * | ||
| * @returns The access token string. | ||
| */ | ||
| getAccessToken(): Promise<string>; | ||
| /** | ||
| * Access token expiration date | ||
| * Expiration date of the currently cached access token, or `undefined` if no token has been fetched yet. | ||
| */ | ||
@@ -27,0 +46,0 @@ protected get accessTokenExpiration(): Date | undefined; |
@@ -13,6 +13,8 @@ interface BaseAccessTokenQuery { | ||
| } & BaseAccessTokenQuery; | ||
| /** Request body for the LWA token endpoint. */ | ||
| export type AccessTokenQuery = RefreshTokenAccessTokenQuery | ClientCredentialsAccessTokenQuery; | ||
| /** Response body from the LWA token endpoint. */ | ||
| export interface AccessTokenData { | ||
| access_token: string; | ||
| refresh_token: string; | ||
| refresh_token?: string; | ||
| token_type: string; | ||
@@ -19,0 +21,0 @@ expires_in: number; |
@@ -0,4 +1,7 @@ | ||
| /** Authorization scopes for grantless Selling Partner API operations. */ | ||
| export declare enum AuthorizationScope { | ||
| /** Scope for the Notifications API. */ | ||
| NOTIFICATIONS = "sellingpartnerapi::notifications", | ||
| /** Scope for rotating application client credentials. */ | ||
| CLIENT_CREDENTIAL_ROTATION = "sellingpartnerapi::client_credential:rotation" | ||
| } |
+3
-3
@@ -5,3 +5,3 @@ { | ||
| "description": "Amazon Selling Partner API authentication package", | ||
| "version": "2.2.23", | ||
| "version": "2.2.24", | ||
| "main": "dist/cjs/index.js", | ||
@@ -22,3 +22,3 @@ "module": "dist/es/index.js", | ||
| "dependencies": { | ||
| "axios": "^1.13.5", | ||
| "axios": "^1.13.6", | ||
| "read-pkg-up": "^7.0.1" | ||
@@ -45,3 +45,3 @@ }, | ||
| ], | ||
| "gitHead": "2c1fe783fb7c2204e7e19d4f85fa2bdf822e4593" | ||
| "gitHead": "ed62de76baf24107227aacb576cd494b2ecbf0b5" | ||
| } |
+54
-12
@@ -16,2 +16,18 @@ # `@sp-api-sdk/auth` | ||
| ## Usage | ||
| The `SellingPartnerApiAuth` class handles OAuth token acquisition from Login with Amazon (LWA). You must provide exactly one of `refreshToken` or `scopes`. | ||
| ```javascript | ||
| import { SellingPartnerApiAuth } from "@sp-api-sdk/auth"; | ||
| const auth = new SellingPartnerApiAuth({ | ||
| clientId: process.env.LWA_CLIENT_ID, | ||
| clientSecret: process.env.LWA_CLIENT_SECRET, | ||
| refreshToken: "Atzr|…", | ||
| }); | ||
| const accessToken = await auth.getAccessToken(); | ||
| ``` | ||
| ## Default values from the environment | ||
@@ -21,11 +37,11 @@ | ||
| | Property Name | Environement variable | | ||
| | -------------- | --------------------- | | ||
| | `clientId` | LWA_CLIENT_ID | | ||
| | `clientSecret` | LWA_CLIENT_SECRET | | ||
| | `refreshToken` | LWA_REFRESH_TOKEN | | ||
| | Property Name | Environment variable | | ||
| | -------------- | -------------------- | | ||
| | `clientId` | `LWA_CLIENT_ID` | | ||
| | `clientSecret` | `LWA_CLIENT_SECRET` | | ||
| | `refreshToken` | `LWA_REFRESH_TOKEN` | | ||
| ## Grantless APIs support | ||
| Some APIs require grantless authentication, which is done by passing scopes, instead of a refresh token. | ||
| Some APIs (e.g. Notifications API) require grantless authentication, which is done by passing scopes instead of a refresh token. | ||
| The available scopes are exposed in the `AuthorizationScope` enum from this library. | ||
@@ -35,13 +51,21 @@ | ||
| import { SellingPartnerApiAuth, AuthorizationScope } from "@sp-api-sdk/auth"; | ||
| import { AuthorizationApiClient } from "@sp-api-sdk/authorization-api-v1"; | ||
| import { NotificationsApiClient } from "@sp-api-sdk/notifications-api-v1"; | ||
| const auth = new SellingPartnerApiAuth({ | ||
| clientId: "", | ||
| clientSecret: "", | ||
| scopes: [AuthorizationScope.NOTIFICATIONS, AuthorizationScope.CLIENT_CREDENTIAL_ROTATION], // Or choose the only ones you need | ||
| clientId: process.env.LWA_CLIENT_ID, | ||
| clientSecret: process.env.LWA_CLIENT_SECRET, | ||
| scopes: [AuthorizationScope.NOTIFICATIONS], | ||
| }); | ||
| const accessToken = await auth.getAccessToken(); | ||
| const client = new NotificationsApiClient({ | ||
| auth, | ||
| region: "eu", | ||
| }); | ||
| ``` | ||
| Available scopes: | ||
| - `AuthorizationScope.NOTIFICATIONS` - For the Notifications API | ||
| - `AuthorizationScope.CLIENT_CREDENTIAL_ROTATION` - For client credential rotation | ||
| ## Credentials caching | ||
@@ -53,4 +77,6 @@ | ||
| You can subclass `SellingPartnerApiAuth` to add custom logic, for example, caching the access token in a store. | ||
| You can subclass `SellingPartnerApiAuth` to add custom logic, for example, caching the access token in an external store. | ||
| The protected `accessTokenExpiration` getter provides the current token's expiration date, which is useful for setting TTLs in your cache. | ||
| ```typescript | ||
@@ -76,2 +102,18 @@ import { SellingPartnerApiAuth } from "@sp-api-sdk/auth"; | ||
| ## Error handling | ||
| Authentication errors are thrown as `SellingPartnerApiAuthError` instances, which extend `AxiosError`. | ||
| ```javascript | ||
| import { SellingPartnerApiAuth, SellingPartnerApiAuthError } from "@sp-api-sdk/auth"; | ||
| try { | ||
| const accessToken = await auth.getAccessToken(); | ||
| } catch (error) { | ||
| if (error instanceof SellingPartnerApiAuthError) { | ||
| console.error(error.message); // e.g. "access-token error: Response code 401" | ||
| } | ||
| } | ||
| ``` | ||
| ## License | ||
@@ -78,0 +120,0 @@ |
22012
35.15%408
32.04%129
48.28%Updated