UNPKG

@lorenstuff/amazon-selling-partner-api

Version:

A package for interacting with the Amazon Selling Partner API.

182 lines 7.06 kB
// // Imports // import crypto from "node:crypto"; import { formatDate } from "../utilities/format-date.js"; /** The core client that handles actually connecting to the Selling Partner API. */ export class AmazonSellingPartnerAPIClient { /** The refresh token from an Amazon Seller Central app. */ refreshToken; /** The client identifier from an Amazon Seller Central app. */ clientIdentifier; /** The client secret from an Amazon Seller Central app. */ clientSecret; /** The access key for the IAM user associated with the Amazon Seller Central app. */ iamUserAccessKey; /** The secret access key for the IAM user associated with the Amazon Seller Central app. */ iamUserSecretAccessKey; /** The Amazon Seller Partner API Endpoint to use. */ apiEndpoint; /** The AWS region to use. */ awsRegion; /** The current access tokens this client has. */ accessTokens; /** Constructs a new client. */ constructor(options) { this.refreshToken = options.refreshToken; this.clientIdentifier = options.clientIdentifier; this.clientSecret = options.clientSecret; this.iamUserAccessKey = options.iamUserAccessKey; this.iamUserSecretAccessKey = options.iamUserSecretAccessKey; this.apiEndpoint = options.apiEndpoint; this.awsRegion = options.awsRegion; this.accessTokens = []; } /** * Adds an access token to the client. * * This is intended to be used to add a restricted data token. */ addAccessToken(accessToken) { this.accessTokens.unshift(accessToken); return accessToken; } /** Removes an access token from the client, if it is still present. */ removeAccessToken(accessToken) { const index = this.accessTokens.indexOf(accessToken); if (index === -1) { return; } this.accessTokens.splice(index, 1); } /** * Fetches a fresh access token. * * @returns A promise that resolves to the access token. */ async getCurrentAccessToken() { // // Try Existing Tokens // while (this.accessTokens[0] != null) { const accessToken = this.accessTokens[0]; if (accessToken.expiresTimestamp > (Date.now() / 1000)) { return accessToken; } this.accessTokens.shift(); } // // Request New Token // const headers = new Headers(); headers.set("Content-Type", "application/x-www-form-urlencoded"); const body = new URLSearchParams(); body.set("grant_type", "refresh_token"); body.set("refresh_token", this.refreshToken); body.set("client_id", this.clientIdentifier); body.set("client_secret", this.clientSecret); const rawAccessTokenResponse = await fetch("https://api.amazon.com/auth/o2/token", { method: "POST", headers, body, }); const accessTokenResponse = await rawAccessTokenResponse.json(); if (accessTokenResponse.access_token == null) { throw new Error("Failed to fetch access token."); } // // Add & Return Access Token // return this.addAccessToken({ accessToken: accessTokenResponse.access_token, expiresTimestamp: (Date.now() / 1000) + accessTokenResponse.expires_in, }); } /** Performs a request to the Selling Partner API. */ async request(options) { const headers = await this.createSignedRequestHeaders(options); let uri = this.apiEndpoint + options.path; if (options.searchParams != null) { uri += "?" + options.searchParams.toString(); } return await fetch(uri, { method: options.method, headers, body: options.body ?? null, }); } /** Creates signed request headers for an AWS request. */ async createSignedRequestHeaders(options) { const dateTime = formatDate(new Date()); const date = dateTime.substring(0, 8); const accessToken = await this.getCurrentAccessToken(); const headers = { "host": new URL(this.apiEndpoint).hostname, "user-agent": "Amazon SP API Node.js Client", "x-amz-access-token": accessToken.accessToken, // Note: This is case-sensitive for some goddamn reason "x-amz-date": dateTime, // Note: This one ISN'T (???) but I am lower casing it for consistency }; if (options.body != null) { headers["content-type"] = "application/json"; } const canonicalHeaders = Object.entries(headers) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => `${k.toLowerCase()}:${v.trim()}\n`) .join(""); const signedHeaderNames = Object.keys(headers) .map((k) => k.toLowerCase()) .sort() .join(";"); const hashedPayload = crypto .createHash("sha256") .update(options.body ?? "") .digest("hex"); const canonicalQueryParams = Array.from(options.searchParams ?? new URLSearchParams()) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join("&"); const canonicalRequest = [ options.method, options.path, canonicalQueryParams, canonicalHeaders, signedHeaderNames, hashedPayload, ].join("\n"); const credentialScope = [ date, this.awsRegion, "execute-api", "aws4_request", ].join("/"); const stringToSign = [ "AWS4-HMAC-SHA256", dateTime, credentialScope, crypto.createHash("sha256").update(canonicalRequest).digest("hex"), ].join("\n"); const signingKey = this.getSigningKey(date); const signature = crypto .createHmac("sha256", signingKey) .update(stringToSign) .digest("hex"); headers["Authorization"] = `AWS4-HMAC-SHA256 Credential=${this.iamUserAccessKey}/${credentialScope},` + `SignedHeaders=${signedHeaderNames},Signature=${signature}`; return headers; } /** Gets the signing key for a given date. */ getSigningKey(date) { const kDate = crypto .createHmac("sha256", "AWS4" + this.iamUserSecretAccessKey) .update(date) .digest(); const kRegion = crypto.createHmac("sha256", kDate).update(this.awsRegion).digest(); const kService = crypto.createHmac("sha256", kRegion).update("execute-api").digest(); return crypto .createHmac("sha256", kService) .update("aws4_request") .digest(); } } //# sourceMappingURL=AmazonSellingPartnerAPIClient.js.map