UNPKG

@agnostack/next-shopify

Version:

Please contact agnoStack via info@agnostack.com for any questions

384 lines • 23.7 kB
"use strict"; 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()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.prepareHeaders = exports.getShopifyHelpers = exports.normalizeShopifyId = void 0; const js_base64_1 = require("js-base64"); const shopify_api_1 = require("@shopify/shopify-api"); const shared_1 = require("../../shared"); const headers_1 = require("./headers"); const billing_1 = require("./billing"); const shopify_1 = require("./shopify"); const firebase_1 = require("./firebase"); const crypto_1 = require("./crypto"); // TODO!!!: keep in sync between next-shopify and lib-core const normalizeShopifyId = (shopifyData) => { var _a, _b; // TODO move CART__DRAFT const into lib-core if (shopifyData === 'cart_draft') { return { id: shopifyData, }; } const _isString = (0, shared_1.isString)(shopifyData); if (_isString && !shopifyData.startsWith('gid:')) { if (!(0, shared_1.isNumericOnly)(shopifyData)) { return {}; } return { id: `${shopifyData}`, }; } const { id: _entityId, legacyResourceId } = _isString ? { id: shopifyData } : (0, shared_1.ensureObject)(shopifyData); if ((0, shared_1.stringEmpty)(_entityId) && (0, shared_1.stringEmpty)(legacyResourceId)) { return {}; } const entity_id = (0, shared_1.ensureString)(_entityId); const { entity_type, parsedId } = (_b = (_a = entity_id.match(/^gid:\/\/shopify\/(?<entity_type>.*)\/(?<parsedId>[0-9]+).*$/)) === null || _a === void 0 ? void 0 : _a.groups) !== null && _b !== void 0 ? _b : {}; const id = (0, shared_1.ensureString)(parsedId || legacyResourceId); const hasId = (0, shared_1.stringNotEmpty)(id); const hasEntityId = (0, shared_1.stringNotEmpty)(entity_id); const hasEntityType = (0, shared_1.stringNotEmpty)(entity_type); return Object.assign(Object.assign(Object.assign({}, (0, shared_1.stringNotEmpty)(id) && { id }), hasEntityType && { entity_type }), hasEntityId ? { entity_id, gid: entity_id, } : (hasEntityType && hasId) && { gid: `gid://shopify/${entity_type}/${id}`, }); }; exports.normalizeShopifyId = normalizeShopifyId; // TODO: move to curry?? const getShopifyHelpers = (serverRuntimeConfig, config) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b; const shopify = (0, shopify_1.getInitializedShopify)(serverRuntimeConfig); const { APP_ID, FB_CONFIG, FB_ROOT_COLLECTION, SHOPIFY_RUN_MODE, SESSION_CONFIG, PAYMENT_CONFIG, } = serverRuntimeConfig; const { shop } = config !== null && config !== void 0 ? config : {}; const isReviewMode = (0, shared_1.isTrue)((SHOPIFY_RUN_MODE === 'review') && (PAYMENT_CONFIG === null || PAYMENT_CONFIG === void 0 ? void 0 : PAYMENT_CONFIG.required)); // TODO: should this use our helper instead (and/or as a fallback)?? const shopId = (_b = (_a = shopify === null || shopify === void 0 ? void 0 : shopify.utils) === null || _a === void 0 ? void 0 : _a.sanitizeShop) === null || _b === void 0 ? void 0 : _b.call(_a, shop); const cryptoHandler = crypto_1.CryptoHandler.getInstance(SESSION_CONFIG); const sessionHandler = firebase_1.FirebaseSessionHandler.getInstance(FB_CONFIG, { collection: FB_ROOT_COLLECTION, configurationId: APP_ID, }); const buildEmbeddedAppUrl = (_appPath) => __awaiter(void 0, void 0, void 0, function* () { if ((0, shared_1.stringEmpty)(shop)) { return undefined; } const embeddedAppBaseUrl = yield shopify.auth.buildEmbeddedAppUrl(js_base64_1.Base64.encode(`${shop}/admin`)); const appPath = (0, shared_1.removeLeadingSlash)(_appPath); return `${embeddedAppBaseUrl}${(0, shared_1.stringNotEmpty)(appPath) ? `/${appPath}` : ''}`; }); const decryptData = (encrypted, { publicKey, sharedSecret } = {}) => { if (!cryptoHandler.verifyPublicKey(publicKey, sharedSecret)) { throw new Error('Invalid or missing public key'); } return cryptoHandler.decrypt(encrypted); }; const encryptData = (value) => (cryptoHandler.encrypt(value)); const ensureEncryptedObject = (encryptable) => (Object.entries((0, shared_1.ensureObject)(encryptable)).reduce((_encrypted, [key, value]) => (Object.assign(Object.assign({}, _encrypted), (value != undefined) && { [key]: encryptData(value), })), {})); // #region internal shopifyHelper implementations const validateSessionToken = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* ({ idToken: idTokens } = {}) { const [idToken] = (0, shared_1.ensureArray)(idTokens); if ((0, shared_1.stringEmpty)(idToken)) { console.warn('Missing ID token (validateSessionToken)'); return { isValid: false }; } try { const decodedSessionToken = yield shopify.session.decodeSessionToken(idToken); if (!(decodedSessionToken === null || decodedSessionToken === void 0 ? void 0 : decodedSessionToken.dest) || (new URL(decodedSessionToken.dest).hostname !== shop)) { return { isValid: false }; } return { isValid: true }; } catch (error) { console.error('Error decoding/validating session token', error); return { isValid: false }; } }); const exchangeSessionToken = (...args_2) => __awaiter(void 0, [...args_2], void 0, function* ({ idToken: idTokens, isOnline } = {}) { const [idToken] = (0, shared_1.ensureArray)(idTokens); if ((0, shared_1.stringEmpty)(idToken)) { console.warn('Missing ID token (exchangeSessionToken)'); return { isValid: false }; } const tokenPayload = { shop, sessionToken: idToken, requestedTokenType: shopify_api_1.RequestedTokenType[isOnline ? 'OnlineAccessToken' : 'OfflineAccessToken'], }; try { const accessToken = yield shopify.auth.tokenExchange(tokenPayload); return { accessToken, isValid: true }; } catch (error) { console.error('Error exchanging session token', { error, payload: tokenPayload }); return { isValid: false }; } }); const ensureEncryptedSession = (session) => { const { accessToken, secure } = session; return Object.assign(Object.assign(Object.assign({}, session), ensureEncryptedObject({ accessToken })), (secure !== null) ? ensureEncryptedObject({ secure }) : { secure }); }; // TODO: handle encrypting other keys const encryptUpdateSession = (session) => __awaiter(void 0, void 0, void 0, function* () { if ((0, shared_1.stringNotEmpty)(session === null || session === void 0 ? void 0 : session.id)) { return (0, firebase_1.updateCallback)(sessionHandler, shopId, ensureEncryptedSession(session)); } throw new shared_1.ShopifySessionInvalidError('Invalid session'); }); const encryptStoreSession = (session) => __awaiter(void 0, void 0, void 0, function* () { return (encryptUpdateSession(session)); }); const verifyOnlineSession = (_c) => __awaiter(void 0, [_c], void 0, function* ({ req, res }) { var _d, _e; const authHeader = req.headers.authorization; const idToken = (authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer ')) ? authHeader.substring(7) : (_d = req === null || req === void 0 ? void 0 : req.query) === null || _d === void 0 ? void 0 : _d[shared_1.SESSION_TOKEN_PARAM]; if ((0, shared_1.stringEmpty)(idToken)) { throw new shared_1.ShopifySessionMissingError('Missing session token'); } const { isValid: isValidSession } = yield validateSessionToken({ idToken, isOnline: true }); if (!isValidSession) { console.warn('Invalid Session'); throw new shared_1.ShopifySessionAuthenticationError('Invalid session'); } const sessionId = yield shopify.session.getCurrentId({ isOnline: true, rawRequest: req, rawResponse: res, }); // eslint-disable-next-line @typescript-eslint/no-unused-vars const _f = (_e = (yield (0, firebase_1.loadCallback)(sessionHandler, shopId, sessionId))) !== null && _e !== void 0 ? _e : {}, { accessToken: encryptedOnlineAccessToken } = _f, storedOnlineSession = __rest(_f, ["accessToken"]); let session = storedOnlineSession; let accessToken = encryptedOnlineAccessToken; let isEncrypted = true; // TODO: explore removing (isEncryptyed) if ((0, shared_1.isTokenExpired)(storedOnlineSession)) { const { isValid: isValidExchange, accessToken: { session: newOnlineSession, } = {}, } = yield exchangeSessionToken({ idToken, isOnline: true }); if (!isValidExchange) { console.warn('Invalid Session'); throw new shared_1.ShopifySessionAuthenticationError('Invalid session exchange'); } // TODO: explore if we want to send back the output/encrypted here?? yield encryptStoreSession(newOnlineSession); session = newOnlineSession; accessToken = newOnlineSession === null || newOnlineSession === void 0 ? void 0 : newOnlineSession.accessToken; isEncrypted = false; // TODO: explore removing (isEncryptyed) } return { session, sessionId, accessToken, isEncrypted, // TODO: explore removing (isEncryptyed) }; }); const verifyOfflineSession = () => __awaiter(void 0, void 0, void 0, function* () { var _g; const sessionId = shopify.session.getOfflineId(shop); const _h = (_g = (yield (0, firebase_1.loadCallback)(sessionHandler, shopId, sessionId))) !== null && _g !== void 0 ? _g : {}, { accessToken: encryptedOfflineAccessToken } = _h, offlineSession = __rest(_h, ["accessToken"]); return { sessionId, session: offlineSession, accessToken: encryptedOfflineAccessToken, isEncrypted: true, // TODO: explore removing (isEncryptyed) }; }); const verifySession = (...args_3) => __awaiter(void 0, [...args_3], void 0, function* ({ req, res, isOnline: _isOnline } = {}) { try { const isOnline = _isOnline !== null && _isOnline !== void 0 ? _isOnline : ((req != undefined) && (res != undefined)); // TODO: explore removing (isEncryptyed) const { session, sessionId, accessToken, isEncrypted } = isOnline ? yield verifyOnlineSession({ req, res }) : yield verifyOfflineSession(); if ((0, shared_1.stringNotEmpty)(session === null || session === void 0 ? void 0 : session.shop) && (session.shop !== shop)) { throw new Error('Invalid shop'); } if ((0, shared_1.stringEmpty)(accessToken)) { throw new shared_1.ShopifySessionMissingError(`Missing ${isOnline ? 'online' : 'offline'} session`, { sessionId }); } return Object.assign(Object.assign({}, session), { accessToken, isEncrypted }); // TODO: explore removing (isEncryptyed) } catch (error) { if (error instanceof shared_1.ShopifySessionAuthenticationError) { throw error; } throw new shared_1.ShopifyAuthenticationError(error === null || error === void 0 ? void 0 : error.message); } }); const decryptVerifyData = (includeSecure) => __awaiter(void 0, void 0, void 0, function* () { const { id, data, secure } = yield verifySession(); return Object.assign(Object.assign({ id }, data), (includeSecure && secure) && cryptoHandler.decrypt(secure)); }); const decryptVerifySecureData = () => __awaiter(void 0, void 0, void 0, function* () { return (decryptVerifyData(true)); }); const decryptVerifySession = (params) => __awaiter(void 0, void 0, void 0, function* () { const _j = yield verifySession(params), { // eslint-disable-next-line @typescript-eslint/no-unused-vars data, // eslint-disable-next-line @typescript-eslint/no-unused-vars secure, isEncrypted, // TODO: explore removing (isEncryptyed) accessToken } = _j, session = __rest(_j, ["data", "secure", "isEncrypted", "accessToken"]); return Object.assign(Object.assign({}, session), { // TODO: explore removing (isEncryptyed) accessToken: isEncrypted ? cryptoHandler.decrypt(accessToken) : accessToken }); }); const _getLatestOfflineSession = (session) => __awaiter(void 0, void 0, void 0, function* () { if ((0, shared_1.stringNotEmpty)(session === null || session === void 0 ? void 0 : session.billingStatus)) { return session; } return yield decryptVerifySession().catch((_error) => { // NOTE: first time install, this is expected if ((_error instanceof shared_1.ShopifySessionMissingError)) { return !(session === null || session === void 0 ? void 0 : session.isOnline) ? session : undefined; } throw _error; }); }); const handleEnsureBilling = (session_1, ...args_4) => __awaiter(void 0, [session_1, ...args_4], void 0, function* (session, { reAuth, forceBilling: _forceBilling = false } = {}) { var _k; if (!isReviewMode && !(0, shared_1.isTrue)(session === null || session === void 0 ? void 0 : session.isOnline)) { yield encryptStoreSession(session); return (0, billing_1.ensureBilling)(PAYMENT_CONFIG, { session, shopify, forceBilling: _forceBilling }); } let forceBilling = _forceBilling; if (isReviewMode) { const offlineSession = yield _getLatestOfflineSession(session); // NOTE: billingStatus is a custom field we add when we're in review mode to handle reauth billing correctly const billingStatus = (_k = offlineSession === null || offlineSession === void 0 ? void 0 : offlineSession.billingStatus) !== null && _k !== void 0 ? _k : 'PENDING'; if ((0, shared_1.objectNotEmpty)(offlineSession) && (0, shared_1.stringEmpty)(offlineSession.billingStatus)) { // NOTE: offlineSession should be latest from FB or Shopify session yield encryptStoreSession(Object.assign(Object.assign({}, offlineSession), { billingStatus })); } forceBilling = (!(0, shared_1.isTrue)(reAuth) && (billingStatus !== 'ACTIVE')); } return (0, billing_1.ensureBilling)(PAYMENT_CONFIG, { session, shopify, forceBilling }); }); const getVerifiedClients = (...args_5) => __awaiter(void 0, [...args_5], void 0, function* ({ req, res, apiVersion, isOnline: _isOnline } = {}) { if ((0, shared_1.stringEmpty)(shop)) { return undefined; } try { const session = yield decryptVerifySession({ req, res, isOnline: _isOnline }); const isOnline = _isOnline !== null && _isOnline !== void 0 ? _isOnline : ((req != undefined) && (res != undefined)); const Graphql = new shopify.clients.Graphql(Object.assign({ session }, apiVersion && { apiVersion })); // NOTE this handle ensure billing in case the user leaves the billing flow early during initial install if (isOnline && (0, shared_1.isTrue)(PAYMENT_CONFIG === null || PAYMENT_CONFIG === void 0 ? void 0 : PAYMENT_CONFIG.required)) { const confirmationUrl = yield handleEnsureBilling(session); if ((0, shared_1.stringNotEmpty)(confirmationUrl)) { throw new shared_1.ShopifyAuthenticationError('Invalid payment status', { redirectUrl: confirmationUrl }); } } return { Graphql, // NOTE: calls to the REST api have been deprecated // TODO: add support for Storefront client (takes different params) // Storefront: shopify.clients.Storefront({ ...apiVersion && { apiVersion } }), }; } catch (error) { if (error instanceof shopify_api_1.HttpResponseError && error.response.code === 401) { throw new shared_1.ShopifyAuthenticationError('Not authenticated'); } throw error; } }); const loadAppData = (...args_6) => __awaiter(void 0, [...args_6], void 0, function* ({ appId: _appId } = {}) { var _l, _m, _o, _p; // TODO!!!!!: ensure this is all cached let { id: appId } = (0, exports.normalizeShopifyId)(_appId); const { Graphql } = (_l = yield getVerifiedClients()) !== null && _l !== void 0 ? _l : {}; if ((0, shared_1.stringEmpty)(appId)) { const appResponse = yield Graphql.request(shared_1.APP_QUERY.INSTALLATION_BASIC); const { id: currentAppId } = (0, exports.normalizeShopifyId)((_o = (_m = appResponse === null || appResponse === void 0 ? void 0 : appResponse.data) === null || _m === void 0 ? void 0 : _m.currentAppInstallation) === null || _o === void 0 ? void 0 : _o.app); appId = currentAppId; } // TODO: throw/return early if still no appId?? const appInstallation = yield Graphql.request(shared_1.APP_QUERY.INSTALLATION, { variables: { namespace: `${appId}-metafields` }, }); return shared_1.TRANSFORM.APP_QUERY((_p = appInstallation === null || appInstallation === void 0 ? void 0 : appInstallation.data) === null || _p === void 0 ? void 0 : _p.currentAppInstallation); }); // #endregion internal shopifyHelper implementations return Object.assign({ shopify, getAppUrl: () => undefined, getVerifiedClients: () => undefined, loadAppData: () => undefined, loadData: () => ({}), loadState: () => undefined, storeState: () => undefined, deleteState: () => undefined, loadSecureData: () => ({}), loadSession: () => ({}), storeSession: () => ({}), updateSession: () => ({}), deleteSessions: () => undefined, findSessionIds: () => ([]), ensureBilling: () => undefined, validateSessionToken: () => ({}), exchangeSessionToken: () => ({}) }, shopify && { getVerifiedClients, getAppUrl: buildEmbeddedAppUrl, validateSessionToken, exchangeSessionToken, encryptData, decryptData, loadAppData, loadData: () => decryptVerifyData(), // NOTE: ensure cannot access secure loadSecureData: decryptVerifySecureData, loadSession: decryptVerifySession, storeSession: encryptStoreSession, ensureBilling: handleEnsureBilling, loadState: () => __awaiter(void 0, void 0, void 0, function* () { return (0, firebase_1.loadState)(sessionHandler, shop); }), storeState: (state) => __awaiter(void 0, void 0, void 0, function* () { return (0, firebase_1.storeState)(sessionHandler, state); }), deleteState: () => __awaiter(void 0, void 0, void 0, function* () { return (0, firebase_1.deleteState)(sessionHandler, shop); }), updateSession: (sessionId, data) => __awaiter(void 0, void 0, void 0, function* () { return encryptUpdateSession(Object.assign(Object.assign({}, data), { id: sessionId })); }), deleteSessions: (sessionIds) => __awaiter(void 0, void 0, void 0, function* () { return (0, firebase_1.deleteSessionsCallback)(sessionHandler, shopId, sessionIds); }), findSessionIds: (_shop) => __awaiter(void 0, void 0, void 0, function* () { var _q, _r; return (0, firebase_1.findSessionIdsByShop)(sessionHandler, (_r = (_q = shopify === null || shopify === void 0 ? void 0 : shopify.utils) === null || _q === void 0 ? void 0 : _q.sanitizeShop) === null || _r === void 0 ? void 0 : _r.call(_q, _shop)); }), }); }); exports.getShopifyHelpers = getShopifyHelpers; const extendAppHeaders = (headers, appData) => { var _a, _b; const { ['x-app-id']: appIdHeader, ['x-shopify-app-id']: shopifyAppIdHeader, ['x-app-installation-id']: appInstallationIdHeader, ['x-shopify-app-installation-id']: shopifyAppInstallationIdHeader, } = headers !== null && headers !== void 0 ? headers : {}; if ((0, shared_1.stringEmpty)(appIdHeader) && (0, shared_1.stringEmpty)(shopifyAppIdHeader) && (0, shared_1.stringNotEmpty)((_a = appData === null || appData === void 0 ? void 0 : appData.appInfo) === null || _a === void 0 ? void 0 : _a.id)) { headers = Object.assign(Object.assign({}, headers), { 'x-app-id': appData.appInfo.id }); } if ((0, shared_1.stringEmpty)(appInstallationIdHeader) && (0, shared_1.stringEmpty)(shopifyAppInstallationIdHeader) && (0, shared_1.stringNotEmpty)((_b = appData === null || appData === void 0 ? void 0 : appData.appInstallation) === null || _b === void 0 ? void 0 : _b.id)) { headers = Object.assign(Object.assign({}, headers), { 'x-app-installation-id': appData.appInstallation.id }); } // TODO: pass along providerSetId?? return Object.entries((0, shared_1.ensureObject)(appData === null || appData === void 0 ? void 0 : appData.appMetafields)).reduce((_headers, [metafieldKey, { value } = {}]) => (Object.assign(Object.assign({}, _headers), !(metafieldKey === null || metafieldKey === void 0 ? void 0 : metafieldKey.includes('_key')) && { [`x-shopify-app-metafield-${metafieldKey}`]: value, })), headers); }; const prepareHeaders = (serverRuntimeConfig) => (...args_7) => __awaiter(void 0, [...args_7], void 0, function* ({ generateReplacements, appData, shop, appId, headers: _headers } = {}) { const headers = (0, shared_1.cleanObject)(_headers, false, shared_1.stringEmptyOnly); if ((0, shared_1.objectEmpty)(headers) || !generateReplacements) { return headers; } if (!appData) { const { loadAppData } = yield (0, exports.getShopifyHelpers)(serverRuntimeConfig, { shop }); appData = yield loadAppData({ appId }); // NOTE: this handles even if no appId } if (!appData) { return headers; } return (0, headers_1.replaceHeaders)({ headers: extendAppHeaders(headers, appData), replacements: yield generateReplacements(Object.assign({ shop, headers }, appData)), }); }); exports.prepareHeaders = prepareHeaders; //# sourceMappingURL=session.js.map