UNPKG

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.

190 lines (189 loc) 8.67 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import * as crypto from 'crypto-js'; import BaseService from '../infrastructure/base_service'; import fetch from 'node-fetch'; import uri from '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. */ export function isAuthenticRequest(querystring, shopifySecretKey) { return __awaiter(this, void 0, void 0, function* () { const hmac = querystring['hmac']; if (!hmac) { return false; } const computed = getHmacHash(shopifySecretKey, buildHashString('web', querystring)); return computed === hmac.toUpperCase(); }); } /** * 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. */ export function isAuthenticProxyRequest(querystring, shopifySecretKey) { return __awaiter(this, void 0, void 0, function* () { const signature = querystring['signature']; if (!signature) { return false; } const computed = getHmacHash(shopifySecretKey, buildHashString('proxy', querystring)); return computed === signature.toUpperCase(); }); } /** * 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. */ export function isAuthenticWebhook(headers, requestBody, shopifySecretKey) { return __awaiter(this, void 0, void 0, function* () { 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(); }); } /** * 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. */ export function isValidShopifyDomain(shopifyDomain) { return __awaiter(this, void 0, void 0, function* () { const url = new uri(shopifyDomain); url.protocol('https'); url.path('/admin'); const response = yield fetch(url.toString(), { method: 'HEAD', headers: BaseService.buildDefaultHeaders(), }); return response.headers.has('X-ShopId'); }); } /** * 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. */ export function buildAuthorizationUrl(scopes, shopifyDomain, shopifyApiKey, redirectUrl, state, grants) { return __awaiter(this, void 0, void 0, function* () { const url = new uri(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(); }); } /** * 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. */ export function authorize(code, shopDomain, shopifyApiKey, shopifySecretKey) { return __awaiter(this, void 0, void 0, function* () { const response = yield new AuthorizeService(shopDomain).authorize(shopifyApiKey, shopifySecretKey, code); return response; }); } /** * A private, unexported service for authorizing app installation. */ class AuthorizeService extends BaseService { 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, }); } }