@agnostack/next-shopify
Version:
Please contact agnoStack via info@agnostack.com for any questions
199 lines • 12.9 kB
JavaScript
;
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