shopify-admin-api
Version:
Shopify Admin API is a NodeJS library built to help developers easily authenticate and make calls against the Shopify API. It was inspired by and borrows heavily from ShopifySharp.
200 lines (199 loc) • 9.09 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authorize = exports.buildAuthorizationUrl = exports.isValidShopifyDomain = exports.isAuthenticWebhook = exports.isAuthenticProxyRequest = exports.isAuthenticRequest = void 0;
const crypto = __importStar(require("crypto-js"));
const base_service_1 = __importDefault(require("../infrastructure/base_service"));
const node_fetch_1 = __importDefault(require("node-fetch"));
const jsuri_1 = __importDefault(require("jsuri"));
/**
* Replaces special querystring characters when calculating an authenticity signature in @isAuthenticRequest and @isAuthenticProxyRequest.
*/
function replaceChars(s, isKey) {
if (!s) {
return '';
}
let output = s.replace(/%/gi, '%25').replace(/&/gi, '%26');
if (isKey) {
output = output.replace(/=/gi, '%3D');
}
return output;
}
function buildHashString(type, querystring) {
// To calculate signature:
// 1. Cast querystring to KVP pairs.
// 2. Remove `signature` and `hmac` keys.
// 3. Replace & with %26, % with %25 in keys and values.
// 4. Replace = with %3D in keys only.
// 5. Join each key and value with = (key=value).
// 6. Sorty kvps alphabetically.
// 7. Join kvps together with & in a web request (key=value&key=value&key=value) and null in a proxy request (key=valuekey=value).
// 8. Compute the kvps with an HMAC-SHA256 using the secret key.
// 9. Request is authentic if the computed string equals the `hmac` (web) or 'signature' (proxy) in querystring.
// Reference: https://docs.shopify.com/api/guides/authentication/oauth#making-authenticated-requests
const kvps = Object.getOwnPropertyNames(querystring)
.filter((key) => key !== 'signature' && key !== 'hmac')
.sort()
.map((key) => `${replaceChars(key, true)}=${replaceChars(querystring[key], false)}`)
.join(type === 'web' ? '&' : '');
return kvps;
}
/**
* Computes an hmac-sha256 hash from the given key and string.
* @param secretKey The application's secret Shopify key.
* @param hashString The string being hashed.
* @param convertToBase64 Whether the resulting hash should be converted to base64 or left as hex. Should be true for validating webhook requests.
*/
function getHmacHash(secretKey, hashString, convertToBase64 = false) {
let hash = crypto.HmacSHA256(hashString, secretKey);
if (convertToBase64) {
hash = crypto.enc.Base64.stringify(hash);
}
return hash.toString().toUpperCase();
}
/**
* Determines if an incoming page request is authentic.
* @param querystring The collection of querystring parameters from the request.
* @param shopifySecretKey Your app's secret key.
* @returns a boolean indicating whether the request is authentic or not.
*/
async function isAuthenticRequest(querystring, shopifySecretKey) {
const hmac = querystring['hmac'];
if (!hmac) {
return false;
}
const computed = getHmacHash(shopifySecretKey, buildHashString('web', querystring));
return computed === hmac.toUpperCase();
}
exports.isAuthenticRequest = isAuthenticRequest;
/**
* Determines if an incoming proxy page request is authentic.
* @param querystring The collection of querystring parameters from the request.
* @param shopifySecretKey Your app's secret key.
* @returns a boolean indicating whether the request is authentic or not.
*/
async function isAuthenticProxyRequest(querystring, shopifySecretKey) {
const signature = querystring['signature'];
if (!signature) {
return false;
}
const computed = getHmacHash(shopifySecretKey, buildHashString('proxy', querystring));
return computed === signature.toUpperCase();
}
exports.isAuthenticProxyRequest = isAuthenticProxyRequest;
/**
* Determines if an incoming webhook requeset is authentic.
* @param headers Either an object containing the request's headers, or the X-Shopify-Hmac-SHA256 header string itself.
* @param requestBody The entire request body as a string.
* @param shopifySecretKey Your app's secret key.
* @returns a boolean indicating whether the request is authentic or not.
*/
async function isAuthenticWebhook(headers, requestBody, shopifySecretKey) {
let hmac;
if (typeof headers === 'string') {
hmac = headers;
}
else {
const headerName = 'X-Shopify-Hmac-SHA256';
hmac = headers[headerName] || headers[headerName.toLowerCase()];
}
if (!hmac) {
return false;
}
const computed = getHmacHash(shopifySecretKey, requestBody, true);
return computed === hmac.toUpperCase();
}
exports.isAuthenticWebhook = isAuthenticWebhook;
/**
* A convenience function that tries to ensure that a given URL is a valid Shopify store by checking the response headers for X-ShopId. This is an undocumented feature, use at your own risk.
*/
async function isValidShopifyDomain(shopifyDomain) {
const url = new jsuri_1.default(shopifyDomain);
url.protocol('https');
url.path('/admin');
const response = await (0, node_fetch_1.default)(url.toString(), {
method: 'HEAD',
headers: base_service_1.default.buildDefaultHeaders(),
});
return response.headers.has('X-ShopId');
}
exports.isValidShopifyDomain = isValidShopifyDomain;
/**
* Builds an authorization URL for Shopify OAuth integration. Send your user to this URL where they'll be asked to accept installation of your Shopify app.
* @param scopes An array of scope permissions that your app will need from the user.
* @param shopifyDomain The user's Shopify URL.
* @param shopifyApiKey Your app's API key. This is NOT your secret key.
* @param redirectUrl An optional URL that the user will be sent to after integration. Override's the Shopify app's default redirect URL.
* @param state An optional, random string value provided by your application which is unique for each authorization request. During the OAuth callback phase, your application should check that this value matches the one you provided to this method.
* @param grants An optional array of token grant types.
*/
async function buildAuthorizationUrl(scopes, shopifyDomain, shopifyApiKey, redirectUrl, state, grants) {
const url = new jsuri_1.default(shopifyDomain);
url.protocol('https');
url.path('admin/oauth/authorize');
url.addQueryParam('client_id', shopifyApiKey);
url.addQueryParam('scope', scopes.join(','));
if (redirectUrl) {
url.addQueryParam('redirect_uri', redirectUrl);
}
if (state) {
url.addQueryParam('state', state);
}
if (grants && Array.isArray(grants)) {
grants.forEach((grant) => url.addQueryParam('grant_options[]', grant));
}
else if (grants && typeof grants === 'string') {
url.addQueryParam('grant_options[]', grants);
}
return url.toString();
}
exports.buildAuthorizationUrl = buildAuthorizationUrl;
/**
* Finalizes app installation, generating a permanent access token for the user's store.
* @param code The authorization code generated by Shopify, which should be a parameter named 'code' on the request querystring.
* @param shopifyDomain The store's Shopify domain, which should be a parameter named 'shop' on the request querystring.
* @param shopifyApiKey Your app's public API key.
* @param shopifySecretKey Your app's secret key.
* @returns The access token.
*/
async function authorize(code, shopDomain, shopifyApiKey, shopifySecretKey) {
const response = await new AuthorizeService(shopDomain).authorize(shopifyApiKey, shopifySecretKey, code);
return response;
}
exports.authorize = authorize;
/**
* A private, unexported service for authorizing app installation.
*/
class AuthorizeService extends base_service_1.default {
constructor(shopDomain) {
super(shopDomain, '', 'oauth');
}
authorize(shopifyApiKey, shopifySecretKey, code) {
return this.createRequest('POST', 'access_token', 'access_token', {
client_id: shopifyApiKey,
client_secret: shopifySecretKey,
code: code,
});
}
}