@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/dist/esm/lib/handlers/api/billing.js +3 -2
  3. package/dist/esm/lib/handlers/api/callback.js +4 -3
  4. package/dist/esm/lib/handlers/api/uninstall.js +3 -2
  5. package/dist/esm/lib/handlers/api/verifiable/appInstallation.js +3 -2
  6. package/dist/esm/lib/handlers/api/verifiable/graph.js +3 -2
  7. package/dist/esm/lib/handlers/api/verifiable/proxy.js +3 -2
  8. package/dist/esm/lib/handlers/api/webhook.js +3 -2
  9. package/dist/esm/lib/handlers/ssr/protect.js +3 -2
  10. package/dist/esm/lib/index.js +0 -1
  11. package/dist/esm/lib/utils/session.js +18 -22
  12. package/dist/esm/shared/index.js +2 -0
  13. package/dist/esm/shared/storage.js +35 -0
  14. package/dist/esm/shared/types.js +3 -0
  15. package/dist/lib/handlers/api/billing.d.ts +1 -1
  16. package/dist/lib/handlers/api/billing.js +3 -2
  17. package/dist/lib/handlers/api/callback.d.ts +1 -1
  18. package/dist/lib/handlers/api/callback.js +4 -3
  19. package/dist/lib/handlers/api/uninstall.d.ts +1 -1
  20. package/dist/lib/handlers/api/uninstall.js +3 -2
  21. package/dist/lib/handlers/api/verifiable/appInstallation.d.ts +1 -1
  22. package/dist/lib/handlers/api/verifiable/appInstallation.js +3 -2
  23. package/dist/lib/handlers/api/verifiable/graph.d.ts +1 -1
  24. package/dist/lib/handlers/api/verifiable/graph.js +3 -2
  25. package/dist/lib/handlers/api/verifiable/proxy.d.ts +1 -1
  26. package/dist/lib/handlers/api/verifiable/proxy.js +3 -2
  27. package/dist/lib/handlers/api/webhook.d.ts +1 -1
  28. package/dist/lib/handlers/api/webhook.js +3 -2
  29. package/dist/lib/handlers/ssr/protect.d.ts +1 -1
  30. package/dist/lib/handlers/ssr/protect.js +3 -2
  31. package/dist/lib/index.d.ts +0 -1
  32. package/dist/lib/index.js +0 -1
  33. package/dist/lib/utils/session.d.ts +2 -4
  34. package/dist/lib/utils/session.js +18 -22
  35. package/dist/lib/utils/types.d.ts +20 -16
  36. package/dist/shared/index.d.ts +2 -0
  37. package/dist/shared/index.js +2 -0
  38. package/dist/shared/storage.d.ts +2 -0
  39. package/dist/shared/storage.js +39 -0
  40. package/dist/shared/types.d.ts +72 -1
  41. package/dist/shared/types.js +2 -0
  42. package/package.json +4 -31
  43. package/dist/esm/storage/firestore/index.js +0 -77
  44. package/dist/esm/storage/index.js +0 -54
  45. package/dist/storage/firestore/index.d.ts +0 -4
  46. package/dist/storage/firestore/index.js +0 -81
  47. package/dist/storage/index.d.ts +0 -61
  48. 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');
@@ -1,4 +1,3 @@
1
1
  export * from '../shared/index.js';
2
2
  export * from './handlers/index.js';
3
- export * from '../storage/index.js';
4
3
  export { getShopifyHelpers, prepareHeaders } from './utils/index.js';
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
- storage?: import("../../storage/index.js").SessionStorage;
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("../../storage/index.js");
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 {{ storage?: import('../../storage/index.js').SessionStorage }} [options]
96
- * `storage` defaults to the Firestore implementation, which is the only reason this package
97
- * still declares firebase-admin.
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 { APP_ID, FB_CONFIG, FB_ROOT_COLLECTION, SHOPIFY_RUN_MODE, SESSION_CONFIG, PAYMENT_CONFIG, } = serverRuntimeConfig;
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 !== null && _storage !== void 0 ? _storage : await createDefaultStorage(FB_CONFIG, {
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
- /** @param {any} [serverRuntimeConfig] */
452
- const prepareHeaders = (serverRuntimeConfig) => async ({ generateReplacements, appData, shop, appId, headers: _headers, } = /** @type {any} */ ({})) => {
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
- appMetafields: any;
4
- appInfo: any;
5
- appInstallation: any;
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) => any;
44
- decryptData: (encrypted: unknown, params?: unknown) => any;
47
+ encryptData: (value?: unknown) => string;
48
+ decryptData: (encrypted: unknown, params?: unknown) => unknown;
45
49
  loadAppData: (params?: unknown) => Promise<AppData>;
46
- loadData: () => Promise<any>;
47
- loadSecureData: () => Promise<any>;
48
- loadSession: (params?: unknown) => Promise<any>;
49
- storeSession: (session?: unknown) => Promise<any>;
50
- ensureBilling: (session: unknown, params?: unknown) => Promise<any>;
51
- loadState: () => Promise<any>;
52
- storeState: (state: unknown) => Promise<any>;
53
- deleteState: () => Promise<any>;
54
- updateSession: (sessionId: unknown, data: unknown) => Promise<any>;
55
- deleteSessions: (sessionIds: unknown) => Promise<any[]>;
56
- findSessionIds: (shop: unknown) => Promise<string[]>;
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;
@@ -5,5 +5,7 @@ export * from './errors.js';
5
5
  export * from './graph.js';
6
6
  export * from './match.js';
7
7
  export * from './shopify.js';
8
+ export * from './storage.js';
9
+ export * from './types.js';
8
10
  export * from './theme.js';
9
11
  export * from './uuid.js';
@@ -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,2 @@
1
+ export const STORAGE_METHODS: string[];
2
+ export function ensureStorage(storage: unknown): import("./types.js").SessionStorage;
@@ -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;
@@ -1 +1,72 @@
1
- export {};
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
+ }
@@ -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-canary.8",
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": "canary",
138
- "@agnostack/next-plugins": "canary",
139
- "@agnostack/verifyd": "canary",
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,4 +0,0 @@
1
- export function createFirestoreStorage(firebaseConfig: any, { collection, configurationId }: {
2
- collection?: string;
3
- configurationId: string;
4
- }): import("../index.js").SessionStorage;
@@ -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;