@lorenstuff/amazon-selling-partner-api
Version:
A package for interacting with the Amazon Selling Partner API.
182 lines • 7.06 kB
JavaScript
//
// 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