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