UNPKG

@agnostack/next-shopify

Version:

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

199 lines • 12.9 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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.handleProxy = void 0; /* eslint-disable @typescript-eslint/no-unused-vars */ const nextjs_cors_1 = __importDefault(require("nextjs-cors")); const http_proxy_1 = __importDefault(require("http-proxy")); const node_http_proxy_json_1 = __importDefault(require("node-http-proxy-json")); const verifyd_1 = require("@agnostack/verifyd"); const utils_1 = require("../../../utils"); const shared_1 = require("../../../../shared"); const PROXY_TIMEOUT = 6000; // NOTE: 6 seconds const verifyAppProxySignature = (signature, verifiable, hmacSecret) => { const hmac = (0, utils_1.generateHMAC)((0, shared_1.objectToSortedString)(verifiable), hmacSecret); return (0, utils_1.compareHashes)(signature, hmac); }; const handleProxy = (serverRuntimeConfig, data) => { const { generateReplacements, returnResponse = true } = data !== null && data !== void 0 ? data : {}; const { API_BASE_PATH, API_URLS, API_PATHS, APP_CONFIG, APP_BASE_URL, TARGET_SAFELIST, ORIGIN_SAFELIST, } = serverRuntimeConfig; const _prepareHeaders = (0, utils_1.prepareHeaders)(serverRuntimeConfig); const _getVerificationHelpers = (0, verifyd_1.getVerificationHelpers)(serverRuntimeConfig); const _getVerifiedSessionData = (0, utils_1.getVerifiedSessionData)(serverRuntimeConfig); return (req, res, params) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d; const _e = (_a = req.headers) !== null && _a !== void 0 ? _a : {}, { origin, 'x-forwarded-for': _forwardFor, 'x-forwarded-proto': _forwardProto, 'x-forwarded-host': forwardHost, 'x-proxy-origin': proxyOrigin } = _e, _headers = __rest(_e, ["origin", 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-proxy-origin']); const { 'x-extension-session': sessionToken, 'x-extension-point': extensionPoint, 'x-shop-domain': shopDomainHeader, 'x-shopify-shop-domain': shopifyShopDomainHeader, 'x-shopify-shop': shopHeader = shopifyShopDomainHeader !== null && shopifyShopDomainHeader !== void 0 ? shopifyShopDomainHeader : shopDomainHeader, 'x-shopify-app-id': shopifyAppId, 'x-app-id': appId = shopifyAppId, // 'x-shopify-app-installation-id': shopifyAppInstallationId, // 'x-app-installation-id': appInstallationId = shopifyAppInstallationId, // TODO: pass along installationId?? // TODO: pass along providerSetId??? } = _headers; const handleResponse = (body, statusCode = 200) => { if (returnResponse) { return res.status(statusCode).json(body); } return { statusCode, body }; }; let targetPath; try { // NOTE: GET requests are not always sending along origin if ((0, shared_1.stringEmpty)(origin)) { req.headers = Object.assign(Object.assign({}, req.headers), { origin: proxyOrigin || forwardHost }); } const _f = (_b = req.query) !== null && _b !== void 0 ? _b : {}, { signature, proxyEncoded } = _f, verifiable = __rest(_f, ["signature", "proxyEncoded"]); const { shop: _shop, host, timestamp, path_prefix } = verifiable; const shop = _shop || shopHeader; yield (0, nextjs_cors_1.default)(req, res, { methods: ['PUT', 'PATCH', 'POST', 'DELETE'], origin: (origin, callback) => { const isSafelisted = (0, shared_1.matchHostname)(origin, [ ...ORIGIN_SAFELIST, 'cdn.shopify.com' // NOTE: this is the domain for checkout UI extensions ]); if (isSafelisted) { return callback(null, true); } return callback(new Error('Invalid CORS origin')); }, optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 }); const { target: targetParam } = params !== null && params !== void 0 ? params : {}; // eslint-disable-next-line max-len const _targetUrl = ((0, shared_1.stringEmpty)(targetParam) || targetParam.startsWith('http')) ? targetParam : `${APP_BASE_URL}/${(0, shared_1.removeLeadingSlash)(targetParam)}`; const { origin: target, hostname, pathname, search } = (0, shared_1.stringNotEmpty)(_targetUrl) ? new URL(_targetUrl) : {}; targetPath = [pathname, search].filter(shared_1.stringNotEmpty).join(''); const safelistedTarget = (0, shared_1.matchHostname)(hostname, Object.assign({ [new URL(APP_BASE_URL).hostname]: { // HMMM: can we generate our own session token (or use the current one) so as to not have to tell handleGraph/Rest to behave as offline? // headers: { // Authorization: 'Bearer {{setting.zendesk.token}}', // }, params: { shop, host, }, } }, TARGET_SAFELIST)); if ((0, shared_1.stringEmpty)(targetPath) || (safelistedTarget == undefined)) { return handleResponse({ message: 'Unproxyable target' }, 407); } if (!shop || !host || !extensionPoint || !signature || !proxyEncoded) { return handleResponse({ message: 'Invalid or missing headers', extensionPoint }, 400); } const isShopifyOriginOrRelative = (0, shared_1.stringNotEmpty)((0, shared_1.ensureString)(path_prefix).startsWith('http') ? (0, shared_1.sanitizeShop)(new URL(path_prefix).hostname) : path_prefix); // TODO: once Shopify fixes CORS issue when origin is cdn.shopify.com, update this! if (!verifyAppProxySignature(signature, verifiable, isShopifyOriginOrRelative ? APP_CONFIG.apiSecretKey : timestamp)) { return handleResponse({ message: 'Invalid signature' }, 403); } const isTokenRequest = ((targetParam === null || targetParam === void 0 ? void 0 : targetParam.startsWith(API_PATHS[shared_1.API_ROUTE_NAMES.SESSION_TOKEN])) || (targetParam === null || targetParam === void 0 ? void 0 : targetParam.startsWith(API_URLS[shared_1.API_ROUTE_NAMES.SESSION_TOKEN]))); if (!isTokenRequest) { const isVerifiedSession = yield _getVerifiedSessionData(sessionToken, { shop, host }); if (!isVerifiedSession) { // NOTE: this should not get here/above should throw ShopifySessionInvalidError return handleResponse({ message: 'Unverified signature' }, 403); } } const { headers: targetHeaders, params: targetParams } = (0, shared_1.ensureObject)(safelistedTarget); if ((0, shared_1.objectNotEmpty)(targetParams)) { targetPath = (0, shared_1.appendQuery)(targetPath, new URLSearchParams(targetParams).toString()); } let requestBody; let processResponse; try { ({ requestBody, processResponse } = yield _getVerificationHelpers(req, { uri: targetPath, disableRecryption: ((targetParam === null || targetParam === void 0 ? void 0 : targetParam.startsWith(API_BASE_PATH)) || (targetParam === null || targetParam === void 0 ? void 0 : targetParam.startsWith(`${APP_BASE_URL}${API_BASE_PATH}`))), })); } catch (error) { if (error instanceof verifyd_1.VerificationError) { return handleResponse({ message: error.message }, error.code); } return handleResponse({ message: 'Verification error' }, 403); } const headers = yield _prepareHeaders({ generateReplacements, shop, appId, headers: Object.assign(Object.assign({}, _headers), targetHeaders), }); const proxy = http_proxy_1.default.createProxy(); return new Promise((resolve, reject) => { req.url = targetPath; if (headers) { req.headers = headers; } proxy.on('proxyReq', (proxyReq, proxyReqRequest, proxyReqResponse, options) => { // NOTE: removes caching headers that could trigger a 304 response proxyReq.removeHeader('If-Modified-Since'); proxyReq.removeHeader('If-None-Match'); proxyReq.removeHeader('Cache-Control'); proxyReq.removeHeader('Expires'); proxyReq.removeHeader('Pragma'); proxyReq.removeHeader('ETag'); // NOTE: adds headers to prevent 304 responses and force fresh content proxyReq.setHeader('Cache-Control', 'no-store'); proxyReq.setHeader('Pragma', 'no-cache'); if (requestBody) { const bodyData = JSON.stringify(requestBody); proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); proxyReq.write(bodyData); } }); proxy.on('proxyRes', function (proxyRes, proxyResRequest, proxyResResponse) { (0, node_http_proxy_json_1.default)(proxyResResponse, proxyRes, function (proxyResBody) { return __awaiter(this, void 0, void 0, function* () { // NOTE: Shopify app proxy will stringify request cookies and response set-cookie headers return processResponse(proxyResBody); }); }); }); proxy.once('error', (error) => { console.error('Proxy error', error); return reject(error); }); proxy.web(req, res, { target, timeout: PROXY_TIMEOUT, proxyTimeout: PROXY_TIMEOUT, followRedirects: false, changeOrigin: true, secure: true, }); }); } catch (error) { // eslint-disable-next-line max-len console.error(`Error handling proxy request: ${targetPath}`, (0, shared_1.cleanObject)(Object.assign({ type: error === null || error === void 0 ? void 0 : error.type, code: error === null || error === void 0 ? void 0 : error.code, name: error === null || error === void 0 ? void 0 : error.name, message: error === null || error === void 0 ? void 0 : error.message }, error === null || error === void 0 ? void 0 : error.data), false, shared_1.stringEmptyOnly)); if (error instanceof shared_1.ShopifySessionInvalidError) { return handleResponse({ message: error.message, type: (_c = error === null || error === void 0 ? void 0 : error.type) !== null && _c !== void 0 ? _c : 'ShopifySessionInvalidError' }, (_d = error === null || error === void 0 ? void 0 : error.code) !== null && _d !== void 0 ? _d : 407); } return handleResponse({ message: (error === null || error === void 0 ? void 0 : error.message) || 'Error handling proxy request', targetPath }, 400); } }); }; exports.handleProxy = handleProxy; //# sourceMappingURL=proxy.js.map