@agnostack/next-shopify 4.0.0-canary.8 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/dist/esm/lib/handlers/api/billing.js +3 -2
- package/dist/esm/lib/handlers/api/callback.js +4 -3
- package/dist/esm/lib/handlers/api/uninstall.js +3 -2
- package/dist/esm/lib/handlers/api/verifiable/appInstallation.js +3 -2
- package/dist/esm/lib/handlers/api/verifiable/graph.js +3 -2
- package/dist/esm/lib/handlers/api/verifiable/proxy.js +3 -2
- package/dist/esm/lib/handlers/api/webhook.js +3 -2
- package/dist/esm/lib/handlers/ssr/protect.js +3 -2
- package/dist/esm/lib/index.js +0 -1
- package/dist/esm/lib/utils/session.js +18 -22
- package/dist/esm/shared/index.js +2 -0
- package/dist/esm/shared/storage.js +35 -0
- package/dist/esm/shared/types.js +3 -0
- package/dist/lib/handlers/api/billing.d.ts +1 -1
- package/dist/lib/handlers/api/billing.js +3 -2
- package/dist/lib/handlers/api/callback.d.ts +1 -1
- package/dist/lib/handlers/api/callback.js +4 -3
- package/dist/lib/handlers/api/uninstall.d.ts +1 -1
- package/dist/lib/handlers/api/uninstall.js +3 -2
- package/dist/lib/handlers/api/verifiable/appInstallation.d.ts +1 -1
- package/dist/lib/handlers/api/verifiable/appInstallation.js +3 -2
- package/dist/lib/handlers/api/verifiable/graph.d.ts +1 -1
- package/dist/lib/handlers/api/verifiable/graph.js +3 -2
- package/dist/lib/handlers/api/verifiable/proxy.d.ts +1 -1
- package/dist/lib/handlers/api/verifiable/proxy.js +3 -2
- package/dist/lib/handlers/api/webhook.d.ts +1 -1
- package/dist/lib/handlers/api/webhook.js +3 -2
- package/dist/lib/handlers/ssr/protect.d.ts +1 -1
- package/dist/lib/handlers/ssr/protect.js +3 -2
- package/dist/lib/index.d.ts +0 -1
- package/dist/lib/index.js +0 -1
- package/dist/lib/utils/session.d.ts +2 -4
- package/dist/lib/utils/session.js +18 -22
- package/dist/lib/utils/types.d.ts +20 -16
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/storage.d.ts +2 -0
- package/dist/shared/storage.js +39 -0
- package/dist/shared/types.d.ts +72 -1
- package/dist/shared/types.js +2 -0
- package/package.json +4 -31
- package/dist/esm/storage/firestore/index.js +0 -77
- package/dist/esm/storage/index.js +0 -54
- package/dist/storage/firestore/index.d.ts +0 -4
- package/dist/storage/firestore/index.js +0 -81
- package/dist/storage/index.d.ts +0 -61
- package/dist/storage/index.js +0 -58
|
@@ -32,8 +32,9 @@ const processRedirect = (url, params, permanent = false) => ({
|
|
|
32
32
|
/**
|
|
33
33
|
* @param {any} [serverRuntimeConfig]
|
|
34
34
|
* @param {any} [_props]
|
|
35
|
+
* @param {import('../../../shared/index.js').StorageOptions} [options]
|
|
35
36
|
*/
|
|
36
|
-
const protectRoute = (serverRuntimeConfig, _props) => {
|
|
37
|
+
const protectRoute = (serverRuntimeConfig, _props, options) => {
|
|
37
38
|
const { PAGE_PATHS, API_PATHS, APP_CONFIG, PAYMENT_CONFIG } = serverRuntimeConfig;
|
|
38
39
|
const { isSessionTokenEnabled } = APP_CONFIG;
|
|
39
40
|
const exitPath = PAGE_PATHS[index_js_2.PAGE_ROUTE_NAMES.EXIT];
|
|
@@ -53,7 +54,7 @@ const protectRoute = (serverRuntimeConfig, _props) => {
|
|
|
53
54
|
console.info(`${chalk_1.default.yellowBright('next-shopify')} - [${chalk_1.default.bold('protectRoute')}]: ${`Bypassing local protect - should ONLY happen when running in next to test non-shopify functionality!`}`);
|
|
54
55
|
return { props };
|
|
55
56
|
}
|
|
56
|
-
const helpers = await (0, index_js_1.getShopifyHelpers)(serverRuntimeConfig, { shop });
|
|
57
|
+
const helpers = await (0, index_js_1.getShopifyHelpers)(serverRuntimeConfig, { shop }, options);
|
|
57
58
|
// NOTE: unreachable — allowLocalBypass returned above, and getInitializedShopify makes the same call
|
|
58
59
|
if (!helpers.shopify) {
|
|
59
60
|
throw new Error('Shopify API is not initialized');
|
package/dist/lib/index.d.ts
CHANGED
package/dist/lib/index.js
CHANGED
|
@@ -17,7 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.prepareHeaders = exports.getShopifyHelpers = void 0;
|
|
18
18
|
__exportStar(require("../shared/index.js"), exports);
|
|
19
19
|
__exportStar(require("./handlers/index.js"), exports);
|
|
20
|
-
__exportStar(require("../storage/index.js"), exports);
|
|
21
20
|
var index_js_1 = require("./utils/index.js");
|
|
22
21
|
Object.defineProperty(exports, "getShopifyHelpers", { enumerable: true, get: function () { return index_js_1.getShopifyHelpers; } });
|
|
23
22
|
Object.defineProperty(exports, "prepareHeaders", { enumerable: true, get: function () { return index_js_1.prepareHeaders; } });
|
|
@@ -13,7 +13,5 @@ export function normalizeShopifyId(shopifyData?: any): {
|
|
|
13
13
|
entity_type?: string | undefined;
|
|
14
14
|
id?: string | undefined;
|
|
15
15
|
};
|
|
16
|
-
export function getShopifyHelpers(serverRuntimeConfig?: any, config?: any, { storage: _storage }?:
|
|
17
|
-
|
|
18
|
-
}): Promise<import("./types.js").ShopifyHelpers>;
|
|
19
|
-
export function prepareHeaders(serverRuntimeConfig?: any): ({ generateReplacements, appData, shop, appId, headers: _headers, }?: any) => Promise<any>;
|
|
16
|
+
export function getShopifyHelpers(serverRuntimeConfig?: any, config?: any, { storage: _storage }?: import("../../shared/index.js").StorageOptions): Promise<import("./types.js").ShopifyHelpers>;
|
|
17
|
+
export function prepareHeaders(serverRuntimeConfig?: any, options?: import("../../shared/index.js").StorageOptions): ({ generateReplacements, appData, shop, appId, headers: _headers, }?: any) => Promise<any>;
|
|
@@ -15,7 +15,7 @@ exports.prepareHeaders = exports.getShopifyHelpers = exports.normalizeShopifyId
|
|
|
15
15
|
const js_base64_1 = require("js-base64");
|
|
16
16
|
const shopify_api_1 = require("@shopify/shopify-api");
|
|
17
17
|
const index_js_1 = require("../../shared/index.js");
|
|
18
|
-
const index_js_2 = require("../../
|
|
18
|
+
const index_js_2 = require("../../shared/index.js");
|
|
19
19
|
const billing_js_1 = require("./billing.js");
|
|
20
20
|
const headers_js_1 = require("./headers.js");
|
|
21
21
|
const shopify_js_1 = require("./shopify.js");
|
|
@@ -78,39 +78,26 @@ const prepareSessionPayload = (session) => {
|
|
|
78
78
|
const _b = _onlineAccessInfo !== null && _onlineAccessInfo !== void 0 ? _onlineAccessInfo : {}, { associated_user } = _b, onlineAccessInfo = __rest(_b, ["associated_user"]);
|
|
79
79
|
return Object.assign({ onlineAccessInfo }, sessionData);
|
|
80
80
|
};
|
|
81
|
-
// NOTE: imported lazily so an app that supplies its own storage never loads firebase-admin,
|
|
82
|
-
// even though it is still the default. PR 4 removes this branch and the peer with it.
|
|
83
|
-
/**
|
|
84
|
-
* @param {any} firebaseConfig
|
|
85
|
-
* @param {any} options
|
|
86
|
-
*/
|
|
87
|
-
const createDefaultStorage = async (firebaseConfig, options) => {
|
|
88
|
-
const { createFirestoreStorage } = await import('../../storage/firestore/index.js');
|
|
89
|
-
return createFirestoreStorage(firebaseConfig, options);
|
|
90
|
-
};
|
|
91
81
|
// TODO: move to curry??
|
|
92
82
|
/**
|
|
93
83
|
* @param {any} [serverRuntimeConfig]
|
|
94
84
|
* @param {any} [config]
|
|
95
|
-
* @param {
|
|
96
|
-
* `storage`
|
|
97
|
-
*
|
|
85
|
+
* @param {import('../../shared/index.js').StorageOptions} options
|
|
86
|
+
* ⚠️ `storage` is REQUIRED. There is no bundled implementation to fall back to, so omitting it
|
|
87
|
+
* throws here rather than quietly writing sessions somewhere this app does not own.
|
|
98
88
|
* @returns {Promise<import('./types.js').ShopifyHelpers>}
|
|
99
89
|
*/
|
|
100
90
|
const getShopifyHelpers = async (serverRuntimeConfig, config, { storage: _storage } = {}) => {
|
|
101
91
|
var _a, _b;
|
|
102
92
|
const shopify = (0, shopify_js_1.getInitializedShopify)(serverRuntimeConfig);
|
|
103
|
-
const {
|
|
93
|
+
const { SHOPIFY_RUN_MODE, SESSION_CONFIG, PAYMENT_CONFIG, } = serverRuntimeConfig;
|
|
104
94
|
const { shop } = config !== null && config !== void 0 ? config : {};
|
|
105
95
|
const isReviewMode = (0, index_js_1.isTrue)((SHOPIFY_RUN_MODE === 'review') &&
|
|
106
96
|
(PAYMENT_CONFIG === null || PAYMENT_CONFIG === void 0 ? void 0 : PAYMENT_CONFIG.required));
|
|
107
97
|
// TODO: should this use our helper instead (and/or as a fallback)??
|
|
108
98
|
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);
|
|
109
99
|
const cryptoHandler = crypto_js_1.CryptoHandler.getInstance(SESSION_CONFIG);
|
|
110
|
-
const storage = (0, index_js_2.ensureStorage)(_storage
|
|
111
|
-
collection: FB_ROOT_COLLECTION,
|
|
112
|
-
configurationId: APP_ID,
|
|
113
|
-
}));
|
|
100
|
+
const storage = (0, index_js_2.ensureStorage)(_storage);
|
|
114
101
|
// NOTE: this should handle running in next to test non-shopify functionality
|
|
115
102
|
if (!shopify) {
|
|
116
103
|
return {
|
|
@@ -236,6 +223,9 @@ const getShopifyHelpers = async (serverRuntimeConfig, config, { storage: _storag
|
|
|
236
223
|
rawRequest: req,
|
|
237
224
|
rawResponse: res,
|
|
238
225
|
});
|
|
226
|
+
// NOTE: the core is the only thing allowed to interpret a stored session. The contract types
|
|
227
|
+
// these payloads as `unknown` so an implementation cannot read them, which makes this the one
|
|
228
|
+
// deliberate exception rather than a hole left open for everyone.
|
|
239
229
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
240
230
|
const _c = (_b = (await storage.loadSession(shopId, sessionId))) !== null && _b !== void 0 ? _b : {}, { accessToken: encryptedOnlineAccessToken } = _c, storedOnlineSession = __rest(_c, ["accessToken"]);
|
|
241
231
|
let session = storedOnlineSession;
|
|
@@ -263,6 +253,9 @@ const getShopifyHelpers = async (serverRuntimeConfig, config, { storage: _storag
|
|
|
263
253
|
const verifyOfflineSession = async () => {
|
|
264
254
|
var _a;
|
|
265
255
|
const sessionId = shopify.session.getOfflineId(shop);
|
|
256
|
+
// NOTE: the core is the only thing allowed to interpret a stored session. The contract types
|
|
257
|
+
// these payloads as `unknown` so an implementation cannot read them, which makes this the one
|
|
258
|
+
// deliberate exception rather than a hole left open for everyone.
|
|
266
259
|
const _b = (_a = (await storage.loadSession(shopId, sessionId))) !== null && _a !== void 0 ? _a : {}, { accessToken: encryptedOfflineAccessToken } = _b, offlineSession = __rest(_b, ["accessToken"]);
|
|
267
260
|
return {
|
|
268
261
|
sessionId,
|
|
@@ -448,14 +441,17 @@ const extendAppHeaders = (headers, appData) => {
|
|
|
448
441
|
[`x-shopify-app-metafield-${metafieldKey}`]: value,
|
|
449
442
|
})), headers);
|
|
450
443
|
};
|
|
451
|
-
/**
|
|
452
|
-
|
|
444
|
+
/**
|
|
445
|
+
* @param {any} [serverRuntimeConfig]
|
|
446
|
+
* @param {import('../../shared/index.js').StorageOptions} [options]
|
|
447
|
+
*/
|
|
448
|
+
const prepareHeaders = (serverRuntimeConfig, options) => async ({ generateReplacements, appData, shop, appId, headers: _headers, } = /** @type {any} */ ({})) => {
|
|
453
449
|
const headers = (0, index_js_1.cleanObject)(_headers, false, index_js_1.stringEmptyOnly);
|
|
454
450
|
if ((0, index_js_1.objectEmpty)(headers) || !generateReplacements) {
|
|
455
451
|
return headers;
|
|
456
452
|
}
|
|
457
453
|
if (!appData) {
|
|
458
|
-
const { loadAppData } = await (0, exports.getShopifyHelpers)(serverRuntimeConfig, { shop });
|
|
454
|
+
const { loadAppData } = await (0, exports.getShopifyHelpers)(serverRuntimeConfig, { shop }, options);
|
|
459
455
|
appData = await loadAppData({ appId }); // NOTE: this handles even if no appId
|
|
460
456
|
}
|
|
461
457
|
if (!appData) {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { GraphqlClient, Session, Shopify } from '@shopify/shopify-api';
|
|
2
|
+
import type { ShopId, StoredState } from '../../shared/types.js';
|
|
3
|
+
export type SessionData = {
|
|
4
|
+
id: string;
|
|
5
|
+
} & Record<string, unknown>;
|
|
2
6
|
export interface AppData {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
appInstallation: Record<string, unknown>;
|
|
8
|
+
appMetafields: Record<string, unknown>;
|
|
9
|
+
appInfo: Record<string, unknown>;
|
|
6
10
|
}
|
|
7
11
|
export interface ShopifyHelpersBypassed {
|
|
8
12
|
shopify: undefined;
|
|
@@ -40,19 +44,19 @@ export interface ShopifyHelpersActive {
|
|
|
40
44
|
session: Session;
|
|
41
45
|
};
|
|
42
46
|
}>;
|
|
43
|
-
encryptData: (value?: unknown) =>
|
|
44
|
-
decryptData: (encrypted: unknown, params?: unknown) =>
|
|
47
|
+
encryptData: (value?: unknown) => string;
|
|
48
|
+
decryptData: (encrypted: unknown, params?: unknown) => unknown;
|
|
45
49
|
loadAppData: (params?: unknown) => Promise<AppData>;
|
|
46
|
-
loadData: () => Promise<
|
|
47
|
-
loadSecureData: () => Promise<
|
|
48
|
-
loadSession: (params?: unknown) => Promise<
|
|
49
|
-
storeSession: (session?: unknown) => Promise<
|
|
50
|
-
ensureBilling: (session: unknown, params?: unknown) => Promise<
|
|
51
|
-
loadState: () => Promise<
|
|
52
|
-
storeState: (state:
|
|
53
|
-
deleteState: () => Promise<
|
|
54
|
-
updateSession: (sessionId:
|
|
55
|
-
deleteSessions: (sessionIds:
|
|
56
|
-
findSessionIds: (shop:
|
|
50
|
+
loadData: () => Promise<SessionData>;
|
|
51
|
+
loadSecureData: () => Promise<SessionData>;
|
|
52
|
+
loadSession: (params?: unknown) => Promise<SessionData>;
|
|
53
|
+
storeSession: (session?: unknown) => Promise<unknown>;
|
|
54
|
+
ensureBilling: (session: unknown, params?: unknown) => Promise<string | undefined>;
|
|
55
|
+
loadState: () => Promise<StoredState | undefined>;
|
|
56
|
+
storeState: (state: StoredState) => Promise<unknown>;
|
|
57
|
+
deleteState: () => Promise<unknown>;
|
|
58
|
+
updateSession: (sessionId: string, data?: Record<string, unknown>) => Promise<unknown>;
|
|
59
|
+
deleteSessions: (sessionIds: string[]) => Promise<unknown>;
|
|
60
|
+
findSessionIds: (shop: ShopId) => Promise<string[]>;
|
|
57
61
|
}
|
|
58
62
|
export type ShopifyHelpers = ShopifyHelpersBypassed | ShopifyHelpersActive;
|
package/dist/shared/index.d.ts
CHANGED
package/dist/shared/index.js
CHANGED
|
@@ -21,5 +21,7 @@ __exportStar(require("./errors.js"), exports);
|
|
|
21
21
|
__exportStar(require("./graph.js"), exports);
|
|
22
22
|
__exportStar(require("./match.js"), exports);
|
|
23
23
|
__exportStar(require("./shopify.js"), exports);
|
|
24
|
+
__exportStar(require("./storage.js"), exports);
|
|
25
|
+
__exportStar(require("./types.js"), exports);
|
|
24
26
|
__exportStar(require("./theme.js"), exports);
|
|
25
27
|
__exportStar(require("./uuid.js"), exports);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureStorage = exports.STORAGE_METHODS = void 0;
|
|
4
|
+
const display_js_1 = require("./display.js");
|
|
5
|
+
exports.STORAGE_METHODS = [
|
|
6
|
+
'loadState',
|
|
7
|
+
'storeState',
|
|
8
|
+
'deleteState',
|
|
9
|
+
'loadSession',
|
|
10
|
+
'updateSession',
|
|
11
|
+
'deleteSessions',
|
|
12
|
+
'findSessionIds'
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Fails at composition rather than at the first OAuth round-trip. Nothing automated covers the
|
|
16
|
+
* Shopify auth flow, so a missing method would otherwise surface as a runtime TypeError in a
|
|
17
|
+
* callback that needs a real dev store to reach.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ Takes `unknown` on purpose. Whether this is a SessionStorage is what the function decides,
|
|
20
|
+
* so the argument cannot be typed as one, and `any` would let the body use it before checking.
|
|
21
|
+
*
|
|
22
|
+
* @param {unknown} storage
|
|
23
|
+
* @returns {import('./types.js').SessionStorage}
|
|
24
|
+
*/
|
|
25
|
+
const ensureStorage = (storage) => {
|
|
26
|
+
const candidate = /** @type {Record<string, unknown> | null | undefined} */ (storage);
|
|
27
|
+
// ⚠️ the upgrade path, called out separately: this package used to bundle a firestore store and
|
|
28
|
+
// fall back to it. Listing all seven methods as missing would be true and would not explain why.
|
|
29
|
+
if (!candidate) {
|
|
30
|
+
throw new Error('No session storage supplied. Pass one as `storage` - this package no longer bundles a default.');
|
|
31
|
+
}
|
|
32
|
+
const missing = (0, display_js_1.ensureArray)(exports.STORAGE_METHODS).filter((method) => (typeof (candidate === null || candidate === void 0 ? void 0 : candidate[method]) !== 'function'));
|
|
33
|
+
if (missing.length) {
|
|
34
|
+
throw new Error(`Invalid session storage, missing: ${missing.join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
// the one place the assertion is earned: every method is present, so it is a SessionStorage
|
|
37
|
+
return /** @type {import('./types.js').SessionStorage} */ (storage);
|
|
38
|
+
};
|
|
39
|
+
exports.ensureStorage = ensureStorage;
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -1 +1,72 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shop and session ids are nullable: `sanitizeShop` returns null for anything it does not
|
|
3
|
+
* recognise, and the core forwards that rather than guarding.
|
|
4
|
+
*/
|
|
5
|
+
export type ShopId = string | null | undefined;
|
|
6
|
+
/** A session id, as shopify's own `getCurrentId` and `getOfflineId` produce it. */
|
|
7
|
+
export type SessionId = string | null | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* A stored session as it comes back out. Every value is opaque: it is whatever was persisted,
|
|
10
|
+
* still encrypted, and only the core knows how to read it.
|
|
11
|
+
*/
|
|
12
|
+
export type StoredSession = Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* A session going in. Addressed by `id`, which is the only field an implementation may read;
|
|
15
|
+
* everything else is opaque payload.
|
|
16
|
+
*/
|
|
17
|
+
export type SessionRecord = {
|
|
18
|
+
id: string;
|
|
19
|
+
} & Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Consumer-owned state, stored verbatim.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ Opaque to an IMPLEMENTATION, which must persist and return it untouched, but not opaque to
|
|
24
|
+
* the consumer, who wrote it and reads its fields back. That is why this is a record of unknowns
|
|
25
|
+
* rather than `unknown`: the property access is theirs to make, the interpretation is not ours.
|
|
26
|
+
*
|
|
27
|
+
* A record rather than anything at all because every store here writes it as a document. A string
|
|
28
|
+
* or a number would compile and then fail at the driver.
|
|
29
|
+
*/
|
|
30
|
+
export type StoredState = Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* What the session layer needs from a store. Two entities, sessions and state, both scoped to a
|
|
33
|
+
* shop. This package ships no implementation: an app owns where its sessions live and passes
|
|
34
|
+
* one in.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ Values arrive ALREADY ENCRYPTED and already stripped of anything that must not be persisted.
|
|
37
|
+
* An implementation stores and returns what it is handed - it must not inspect, reshape or
|
|
38
|
+
* re-encrypt, or the core's crypto and DPA guarantees stop holding. That is why the payloads are
|
|
39
|
+
* `unknown` rather than `any`: `unknown` is the type that refuses to be read without a cast, so
|
|
40
|
+
* the contract is the annotation rather than a comment beside it.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ The four writes return `unknown` because no caller in the core reads them. Returning the
|
|
43
|
+
* stored document is welcome, but nothing may depend on it. The two reads are typed, because
|
|
44
|
+
* their results are the point of calling them.
|
|
45
|
+
*/
|
|
46
|
+
export interface SessionStorage {
|
|
47
|
+
/** `undefined` for a session that is not there, which is ordinary rather than exceptional. */
|
|
48
|
+
loadSession: (shopId: ShopId, sessionId: SessionId) => Promise<StoredSession | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* MERGES into the stored session. The core writes partial sessions, so a replacing write would
|
|
51
|
+
* silently drop fields it did not send.
|
|
52
|
+
*/
|
|
53
|
+
updateSession: (shopId: ShopId, session: SessionRecord) => Promise<unknown>;
|
|
54
|
+
deleteSessions: (shopId: ShopId, sessionIds: string[]) => Promise<unknown>;
|
|
55
|
+
/** `[]` rather than an error for an unknown shop - that is what an initial install looks like. */
|
|
56
|
+
findSessionIds: (shopId: ShopId) => Promise<string[]>;
|
|
57
|
+
/** `undefined` for a shop with no state, which is ordinary rather than exceptional. */
|
|
58
|
+
loadState: (shop: ShopId) => Promise<StoredState | undefined>;
|
|
59
|
+
storeState: (shop: ShopId, state: StoredState) => Promise<unknown>;
|
|
60
|
+
deleteState: (shop: ShopId) => Promise<unknown>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The trailing options every handler factory takes. `storage` defaults to the bundled Firestore
|
|
64
|
+
* implementation, so an app that passes nothing keeps today's behaviour.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ This cannot travel through `serverRuntimeConfig`. Next serialises that bag to
|
|
67
|
+
* `run-config.json` at build time and the Netlify runtime re-stringifies it into
|
|
68
|
+
* `__NEXT_PRIVATE_STANDALONE_CONFIG`, so a function on it is dropped without an error.
|
|
69
|
+
*/
|
|
70
|
+
export interface StorageOptions {
|
|
71
|
+
storage?: SessionStorage;
|
|
72
|
+
}
|
package/dist/shared/types.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// The session-storage contract. Types rather than JSDoc typedefs because that is nearly all this
|
|
3
|
+
// is, and because tsc preserves JSDoc into the emitted JavaScript, where none of it belongs.
|
|
2
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agnostack/next-shopify",
|
|
3
|
-
"version": "4.0.0
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"author": "agnoStack Dev <developers@agnostack.com> (https://agnostack.com)",
|
|
5
5
|
"owner": "agnoStack",
|
|
6
6
|
"description": "Please contact agnoStack via info@agnostack.com for any questions",
|
|
@@ -39,28 +39,6 @@
|
|
|
39
39
|
"import": "./dist/esm/lib/index.js",
|
|
40
40
|
"default": "./dist/esm/lib/index.js"
|
|
41
41
|
},
|
|
42
|
-
"./storage": {
|
|
43
|
-
"types": "./dist/storage/index.d.ts",
|
|
44
|
-
"require": "./dist/storage/index.js",
|
|
45
|
-
"import": "./dist/storage/index.js",
|
|
46
|
-
"default": "./dist/storage/index.js"
|
|
47
|
-
},
|
|
48
|
-
"./storage/esm": {
|
|
49
|
-
"types": "./dist/storage/index.d.ts",
|
|
50
|
-
"import": "./dist/esm/storage/index.js",
|
|
51
|
-
"default": "./dist/esm/storage/index.js"
|
|
52
|
-
},
|
|
53
|
-
"./storage/firestore": {
|
|
54
|
-
"types": "./dist/storage/firestore/index.d.ts",
|
|
55
|
-
"require": "./dist/storage/firestore/index.js",
|
|
56
|
-
"import": "./dist/storage/firestore/index.js",
|
|
57
|
-
"default": "./dist/storage/firestore/index.js"
|
|
58
|
-
},
|
|
59
|
-
"./storage/firestore/esm": {
|
|
60
|
-
"types": "./dist/storage/firestore/index.d.ts",
|
|
61
|
-
"import": "./dist/esm/storage/firestore/index.js",
|
|
62
|
-
"default": "./dist/esm/storage/firestore/index.js"
|
|
63
|
-
},
|
|
64
42
|
"./runtime": {
|
|
65
43
|
"types": "./dist/runtime/index.d.ts",
|
|
66
44
|
"require": "./dist/runtime/index.js",
|
|
@@ -134,9 +112,9 @@
|
|
|
134
112
|
"uuid": "11.1.1"
|
|
135
113
|
},
|
|
136
114
|
"devDependencies": {
|
|
137
|
-
"@agnostack/env": "
|
|
138
|
-
"@agnostack/next-plugins": "
|
|
139
|
-
"@agnostack/verifyd": "
|
|
115
|
+
"@agnostack/env": "latest",
|
|
116
|
+
"@agnostack/next-plugins": "latest",
|
|
117
|
+
"@agnostack/verifyd": "latest",
|
|
140
118
|
"@commitlint/cli": "21.x",
|
|
141
119
|
"@commitlint/config-conventional": "21.x",
|
|
142
120
|
"@eslint/js": "9.x",
|
|
@@ -152,7 +130,6 @@
|
|
|
152
130
|
"@typescript-eslint/parser": "8.x",
|
|
153
131
|
"eslint": "9.x",
|
|
154
132
|
"eslint-plugin-import": "2.x",
|
|
155
|
-
"firebase-admin": "14.x",
|
|
156
133
|
"globals": "16.x",
|
|
157
134
|
"http-proxy": "1.18.x",
|
|
158
135
|
"husky": "9.x",
|
|
@@ -166,7 +143,6 @@
|
|
|
166
143
|
"@agnostack/env": "*",
|
|
167
144
|
"@agnostack/next-plugins": "*",
|
|
168
145
|
"@agnostack/verifyd": "*",
|
|
169
|
-
"firebase-admin": ">=10.0.0",
|
|
170
146
|
"http-proxy": ">=1.18.1",
|
|
171
147
|
"next": ">=15.0.0",
|
|
172
148
|
"react": ">=18.0.0",
|
|
@@ -175,9 +151,6 @@
|
|
|
175
151
|
"prompts": ">=2.4.0"
|
|
176
152
|
},
|
|
177
153
|
"peerDependenciesMeta": {
|
|
178
|
-
"firebase-admin": {
|
|
179
|
-
"optional": true
|
|
180
|
-
},
|
|
181
154
|
"next": {
|
|
182
155
|
"optional": true
|
|
183
156
|
},
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { cert, getApp, getApps, initializeApp } from 'firebase-admin/app';
|
|
2
|
-
import { getFirestore } from 'firebase-admin/firestore';
|
|
3
|
-
import { ensureArray, stringEmpty, objectNotEmpty, COLLECTION_ROOT, STATE_PREFIX, } from '../../shared/index.js';
|
|
4
|
-
// firebase-admin allows one app per process, so an existing one is reused rather than replaced.
|
|
5
|
-
// NOTE: the retired handler also passed `databaseURL`, which configures the Realtime Database.
|
|
6
|
-
// Nothing here has ever used it - no `getDatabase`, no `firebase-admin/database` import.
|
|
7
|
-
/** @param {any} firebaseConfig */
|
|
8
|
-
const initializeFirebaseApp = (firebaseConfig) => (getApps().length
|
|
9
|
-
? getApp()
|
|
10
|
-
: initializeApp({ credential: cert(firebaseConfig) }));
|
|
11
|
-
/**
|
|
12
|
-
* The Firestore implementation of the session-storage contract, kept here so the core never
|
|
13
|
-
* imports firebase-admin. Sessions and state share one collection per shop; state is a single
|
|
14
|
-
* document per shop, prefixed so it cannot collide with a session id.
|
|
15
|
-
*
|
|
16
|
-
* @param {any} firebaseConfig a firebase service-account config
|
|
17
|
-
* @param {{ collection?: string, configurationId: string }} options
|
|
18
|
-
* @returns {import('../index.js').SessionStorage}
|
|
19
|
-
*/
|
|
20
|
-
export const createFirestoreStorage = (firebaseConfig, { collection = COLLECTION_ROOT, configurationId }) => {
|
|
21
|
-
if (!objectNotEmpty(firebaseConfig)) {
|
|
22
|
-
throw new Error('createFirestoreStorage requires a firebase service-account config');
|
|
23
|
-
}
|
|
24
|
-
const firestore = getFirestore(initializeFirebaseApp(firebaseConfig));
|
|
25
|
-
/** @param {any} shop */
|
|
26
|
-
const shopCollection = (shop) => (firestore
|
|
27
|
-
.collection(collection)
|
|
28
|
-
.doc(shop)
|
|
29
|
-
.collection(configurationId));
|
|
30
|
-
/** @param {any} shop */
|
|
31
|
-
const stateRef = (shop) => shopCollection(shop).doc(`${STATE_PREFIX}${shop}`);
|
|
32
|
-
/** @param {any} docRef */
|
|
33
|
-
const readDocument = async (docRef) => {
|
|
34
|
-
const document = await docRef.get();
|
|
35
|
-
return document.exists ? document.data() : undefined;
|
|
36
|
-
};
|
|
37
|
-
/** @type {import('../index.js').SessionStorage} */
|
|
38
|
-
const storage = {
|
|
39
|
-
loadSession: async (shopId, sessionId) => (stringEmpty(sessionId)
|
|
40
|
-
? undefined
|
|
41
|
-
: readDocument(shopCollection(shopId).doc(`${sessionId}`))),
|
|
42
|
-
// NOTE: merge rather than replace. The core writes partial sessions - a billing status
|
|
43
|
-
// update carries no access token - and a replacing write would drop the rest.
|
|
44
|
-
updateSession: async (shopId, session) => {
|
|
45
|
-
const docRef = shopCollection(shopId).doc(session.id);
|
|
46
|
-
await docRef.set(session, { merge: true });
|
|
47
|
-
return readDocument(docRef);
|
|
48
|
-
},
|
|
49
|
-
deleteSessions: async (shopId, sessionIds) => {
|
|
50
|
-
const collectionRef = shopCollection(shopId);
|
|
51
|
-
return Promise.all(ensureArray(sessionIds).map((sessionId) => collectionRef.doc(sessionId).delete()));
|
|
52
|
-
},
|
|
53
|
-
findSessionIds: async (shopId) => {
|
|
54
|
-
// NOTE: empty rather than an error - this is the state on an initial install
|
|
55
|
-
if (stringEmpty(shopId)) {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
const documents = await shopCollection(shopId).get();
|
|
59
|
-
return documents.docs.map((/** @type {any} */ { id }) => id);
|
|
60
|
-
},
|
|
61
|
-
loadState: async (shop) => (stringEmpty(shop)
|
|
62
|
-
? undefined
|
|
63
|
-
: readDocument(stateRef(shop))),
|
|
64
|
-
storeState: async (shop, state) => {
|
|
65
|
-
if (stringEmpty(shop)) {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
const docRef = stateRef(shop);
|
|
69
|
-
await docRef.set(state);
|
|
70
|
-
return readDocument(docRef);
|
|
71
|
-
},
|
|
72
|
-
deleteState: async (shop) => (stringEmpty(shop)
|
|
73
|
-
? undefined
|
|
74
|
-
: stateRef(shop).delete()),
|
|
75
|
-
};
|
|
76
|
-
return storage;
|
|
77
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { ensureArray } from '../shared/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Shop and session ids are nullable: `sanitizeShop` returns null for anything it does not
|
|
4
|
-
* recognise, and the core forwards that rather than guarding.
|
|
5
|
-
*
|
|
6
|
-
* @typedef {string | null | undefined} ShopId
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* What the session layer needs from a store. Two entities, sessions and state, both scoped to a
|
|
10
|
-
* shop. Nothing here is Firestore-specific: `@agnostack/next-shopify/firestore` is one
|
|
11
|
-
* implementation, and an app can supply its own.
|
|
12
|
-
*
|
|
13
|
-
* ⚠️ Values arrive ALREADY ENCRYPTED and already stripped of anything that must not be persisted.
|
|
14
|
-
* An implementation stores and returns what it is handed - it must not inspect, reshape or
|
|
15
|
-
* re-encrypt, or the core's crypto and DPA guarantees stop holding.
|
|
16
|
-
*
|
|
17
|
-
* ⚠️ `findSessionIds` returns `[]` rather than erroring for an unknown shop - that is what an
|
|
18
|
-
* initial install looks like.
|
|
19
|
-
*
|
|
20
|
-
* @typedef {object} SessionStorage
|
|
21
|
-
* @property {(shopId: ShopId, sessionId: ShopId) => Promise<any>} loadSession
|
|
22
|
-
* @property {(shopId: ShopId, session: any) => Promise<any>} updateSession
|
|
23
|
-
* MERGES into the stored session. The core writes partial sessions, so a replacing write
|
|
24
|
-
* would silently drop fields it did not send.
|
|
25
|
-
* @property {(shopId: ShopId, sessionIds: any) => Promise<any>} deleteSessions
|
|
26
|
-
* @property {(shopId: ShopId) => Promise<string[]>} findSessionIds
|
|
27
|
-
* @property {(shop: ShopId) => Promise<any>} loadState
|
|
28
|
-
* @property {(shop: ShopId, state: any) => Promise<any>} storeState
|
|
29
|
-
* @property {(shop: ShopId) => Promise<any>} deleteState
|
|
30
|
-
*/
|
|
31
|
-
export const STORAGE_METHODS = [
|
|
32
|
-
'loadState',
|
|
33
|
-
'storeState',
|
|
34
|
-
'deleteState',
|
|
35
|
-
'loadSession',
|
|
36
|
-
'updateSession',
|
|
37
|
-
'deleteSessions',
|
|
38
|
-
'findSessionIds'
|
|
39
|
-
];
|
|
40
|
-
/**
|
|
41
|
-
* Fails at composition rather than at the first OAuth round-trip. Nothing automated covers the
|
|
42
|
-
* Shopify auth flow, so a missing method would otherwise surface as a runtime TypeError in a
|
|
43
|
-
* callback that needs a real dev store to reach.
|
|
44
|
-
*
|
|
45
|
-
* @param {any} storage
|
|
46
|
-
* @returns {SessionStorage}
|
|
47
|
-
*/
|
|
48
|
-
export const ensureStorage = (storage) => {
|
|
49
|
-
const missing = ensureArray(STORAGE_METHODS).filter((method) => (typeof (storage === null || storage === void 0 ? void 0 : storage[method]) !== 'function'));
|
|
50
|
-
if (missing.length) {
|
|
51
|
-
throw new Error(`Invalid session storage, missing: ${missing.join(', ')}`);
|
|
52
|
-
}
|
|
53
|
-
return storage;
|
|
54
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createFirestoreStorage = void 0;
|
|
4
|
-
const app_1 = require("firebase-admin/app");
|
|
5
|
-
const firestore_1 = require("firebase-admin/firestore");
|
|
6
|
-
const index_js_1 = require("../../shared/index.js");
|
|
7
|
-
// firebase-admin allows one app per process, so an existing one is reused rather than replaced.
|
|
8
|
-
// NOTE: the retired handler also passed `databaseURL`, which configures the Realtime Database.
|
|
9
|
-
// Nothing here has ever used it - no `getDatabase`, no `firebase-admin/database` import.
|
|
10
|
-
/** @param {any} firebaseConfig */
|
|
11
|
-
const initializeFirebaseApp = (firebaseConfig) => ((0, app_1.getApps)().length
|
|
12
|
-
? (0, app_1.getApp)()
|
|
13
|
-
: (0, app_1.initializeApp)({ credential: (0, app_1.cert)(firebaseConfig) }));
|
|
14
|
-
/**
|
|
15
|
-
* The Firestore implementation of the session-storage contract, kept here so the core never
|
|
16
|
-
* imports firebase-admin. Sessions and state share one collection per shop; state is a single
|
|
17
|
-
* document per shop, prefixed so it cannot collide with a session id.
|
|
18
|
-
*
|
|
19
|
-
* @param {any} firebaseConfig a firebase service-account config
|
|
20
|
-
* @param {{ collection?: string, configurationId: string }} options
|
|
21
|
-
* @returns {import('../index.js').SessionStorage}
|
|
22
|
-
*/
|
|
23
|
-
const createFirestoreStorage = (firebaseConfig, { collection = index_js_1.COLLECTION_ROOT, configurationId }) => {
|
|
24
|
-
if (!(0, index_js_1.objectNotEmpty)(firebaseConfig)) {
|
|
25
|
-
throw new Error('createFirestoreStorage requires a firebase service-account config');
|
|
26
|
-
}
|
|
27
|
-
const firestore = (0, firestore_1.getFirestore)(initializeFirebaseApp(firebaseConfig));
|
|
28
|
-
/** @param {any} shop */
|
|
29
|
-
const shopCollection = (shop) => (firestore
|
|
30
|
-
.collection(collection)
|
|
31
|
-
.doc(shop)
|
|
32
|
-
.collection(configurationId));
|
|
33
|
-
/** @param {any} shop */
|
|
34
|
-
const stateRef = (shop) => shopCollection(shop).doc(`${index_js_1.STATE_PREFIX}${shop}`);
|
|
35
|
-
/** @param {any} docRef */
|
|
36
|
-
const readDocument = async (docRef) => {
|
|
37
|
-
const document = await docRef.get();
|
|
38
|
-
return document.exists ? document.data() : undefined;
|
|
39
|
-
};
|
|
40
|
-
/** @type {import('../index.js').SessionStorage} */
|
|
41
|
-
const storage = {
|
|
42
|
-
loadSession: async (shopId, sessionId) => ((0, index_js_1.stringEmpty)(sessionId)
|
|
43
|
-
? undefined
|
|
44
|
-
: readDocument(shopCollection(shopId).doc(`${sessionId}`))),
|
|
45
|
-
// NOTE: merge rather than replace. The core writes partial sessions - a billing status
|
|
46
|
-
// update carries no access token - and a replacing write would drop the rest.
|
|
47
|
-
updateSession: async (shopId, session) => {
|
|
48
|
-
const docRef = shopCollection(shopId).doc(session.id);
|
|
49
|
-
await docRef.set(session, { merge: true });
|
|
50
|
-
return readDocument(docRef);
|
|
51
|
-
},
|
|
52
|
-
deleteSessions: async (shopId, sessionIds) => {
|
|
53
|
-
const collectionRef = shopCollection(shopId);
|
|
54
|
-
return Promise.all((0, index_js_1.ensureArray)(sessionIds).map((sessionId) => collectionRef.doc(sessionId).delete()));
|
|
55
|
-
},
|
|
56
|
-
findSessionIds: async (shopId) => {
|
|
57
|
-
// NOTE: empty rather than an error - this is the state on an initial install
|
|
58
|
-
if ((0, index_js_1.stringEmpty)(shopId)) {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
const documents = await shopCollection(shopId).get();
|
|
62
|
-
return documents.docs.map((/** @type {any} */ { id }) => id);
|
|
63
|
-
},
|
|
64
|
-
loadState: async (shop) => ((0, index_js_1.stringEmpty)(shop)
|
|
65
|
-
? undefined
|
|
66
|
-
: readDocument(stateRef(shop))),
|
|
67
|
-
storeState: async (shop, state) => {
|
|
68
|
-
if ((0, index_js_1.stringEmpty)(shop)) {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
const docRef = stateRef(shop);
|
|
72
|
-
await docRef.set(state);
|
|
73
|
-
return readDocument(docRef);
|
|
74
|
-
},
|
|
75
|
-
deleteState: async (shop) => ((0, index_js_1.stringEmpty)(shop)
|
|
76
|
-
? undefined
|
|
77
|
-
: stateRef(shop).delete()),
|
|
78
|
-
};
|
|
79
|
-
return storage;
|
|
80
|
-
};
|
|
81
|
-
exports.createFirestoreStorage = createFirestoreStorage;
|