@doswiftly/storefront-sdk 19.1.0 → 20.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 (55) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/dist/core/auth/session-events.d.ts +6 -9
  3. package/dist/core/auth/session-events.d.ts.map +1 -1
  4. package/dist/core/auth/session-events.js +6 -9
  5. package/dist/core/cart/cart-client.d.ts +39 -2
  6. package/dist/core/cart/cart-client.d.ts.map +1 -1
  7. package/dist/core/cart/cart-client.js +53 -3
  8. package/dist/core/cart/cart-recovery.d.ts +41 -71
  9. package/dist/core/cart/cart-recovery.d.ts.map +1 -1
  10. package/dist/core/cart/cart-recovery.js +37 -95
  11. package/dist/core/cart/cookie-config.d.ts +29 -3
  12. package/dist/core/cart/cookie-config.d.ts.map +1 -1
  13. package/dist/core/cart/cookie-config.js +35 -3
  14. package/dist/core/generated/operation-types.d.ts +268 -4
  15. package/dist/core/generated/operation-types.d.ts.map +1 -1
  16. package/dist/core/generated/operation-types.js +8 -0
  17. package/dist/core/index.d.ts +3 -2
  18. package/dist/core/index.d.ts.map +1 -1
  19. package/dist/core/index.js +3 -2
  20. package/dist/core/middleware/cart-secret.d.ts +43 -0
  21. package/dist/core/middleware/cart-secret.d.ts.map +1 -0
  22. package/dist/core/middleware/cart-secret.js +55 -0
  23. package/dist/core/middleware/session-retry.d.ts +2 -2
  24. package/dist/core/middleware/session-retry.js +2 -2
  25. package/dist/core/operations/cart.d.ts +23 -0
  26. package/dist/core/operations/cart.d.ts.map +1 -1
  27. package/dist/core/operations/cart.js +71 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/react/cookies.d.ts +6 -0
  31. package/dist/react/cookies.d.ts.map +1 -1
  32. package/dist/react/cookies.js +23 -4
  33. package/dist/react/hooks/use-cart-manager.d.ts +20 -13
  34. package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
  35. package/dist/react/hooks/use-cart-manager.js +14 -12
  36. package/dist/react/hooks/use-logout.d.ts +7 -5
  37. package/dist/react/hooks/use-logout.d.ts.map +1 -1
  38. package/dist/react/hooks/use-logout.js +37 -7
  39. package/dist/react/providers/cart-manager-provider.d.ts +3 -0
  40. package/dist/react/providers/cart-manager-provider.d.ts.map +1 -1
  41. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
  42. package/dist/react/providers/storefront-client-provider.js +6 -0
  43. package/dist/react/server/cookie-readers.d.ts +20 -0
  44. package/dist/react/server/cookie-readers.d.ts.map +1 -1
  45. package/dist/react/server/cookie-readers.js +22 -1
  46. package/dist/react/server/index.d.ts +2 -1
  47. package/dist/react/server/index.d.ts.map +1 -1
  48. package/dist/react/server/index.js +4 -1
  49. package/dist/react/stores/cart.context.d.ts +5 -0
  50. package/dist/react/stores/cart.context.d.ts.map +1 -1
  51. package/dist/react/stores/cart.context.js +5 -0
  52. package/dist/react/stores/cart.store.d.ts +6 -0
  53. package/dist/react/stores/cart.store.d.ts.map +1 -1
  54. package/dist/react/stores/cart.store.js +6 -0
  55. package/package.json +1 -1
@@ -32,14 +32,16 @@
32
32
  * provides browser / Next.js server / AsyncStorage / in-memory implementation).
33
33
  */
34
34
  import { StorefrontError } from '../errors';
35
+ import { parseCartCookieValue } from './cookie-config';
35
36
  // ---------------------------------------------------------------------------
36
37
  // Error codes — must mirror backend CartErrorCode enum (guarded by drift test)
37
38
  // ---------------------------------------------------------------------------
38
39
  /**
39
40
  * Cart error codes that require recreation of the cart resource.
40
41
  *
41
- * - `CART_NOT_FOUND` — cart expired, missing, or owner mismatch (backend masks
42
- * `UNAUTHORIZED` as `CART_NOT_FOUND` to avoid leaking ownership info).
42
+ * - `CART_NOT_FOUND` — cart expired, missing, or the access secret was absent
43
+ * or did not match. The backend returns one uniform code so it never leaks
44
+ * whether a given cart exists.
43
45
  * - `ALREADY_COMPLETED` — cart status not in `{ACTIVE, RECOVERED}` (CONVERTED
44
46
  * after checkout, EXPIRED via TTL, or ABANDONED by purge worker).
45
47
  *
@@ -65,53 +67,6 @@ export function isCartRecoverableError(err) {
65
67
  return false;
66
68
  return err.userErrors.some((ue) => typeof ue.code === 'string' && RECOVERABLE_CODE_SET.has(ue.code));
67
69
  }
68
- // ---------------------------------------------------------------------------
69
- // Session error codes — auth-recoverable, distinct from cart-resource recovery
70
- // ---------------------------------------------------------------------------
71
- /**
72
- * Cart error codes that signal a missing or invalid auth context on a
73
- * customer-owned cart — distinct from cart-resource recovery
74
- * (`CART_RECOVERABLE_ERROR_CODES`).
75
- *
76
- * - `CART_UNAUTHENTICATED` — anonymous request OR present-but-invalid token
77
- * (expired JWT, malformed). The cart resource is intact and reachable
78
- * again after re-auth.
79
- *
80
- * Storefronts SHOULD preserve the `cart-id` cookie when handling these
81
- * codes and prompt sign-in — the same cart resumes after a successful login.
82
- */
83
- export const CART_SESSION_ERROR_CODES = ['CART_UNAUTHENTICATED'];
84
- const SESSION_CODE_SET = new Set(CART_SESSION_ERROR_CODES);
85
- /**
86
- * Type-safe predicate for "this error means the session is gone, but the
87
- * cart resource is intact". Distinct from `isCartRecoverableError`
88
- * (cart-resource recovery) — `isCartSessionError` matches auth-recoverable
89
- * codes that should trigger re-auth flow, NOT cart cookie cleanup.
90
- *
91
- * Inspects `err.userErrors[].code` (structured field) — never matches against
92
- * `err.message` (locale-dependent, non-stable).
93
- */
94
- export function isCartSessionError(err) {
95
- if (!(err instanceof StorefrontError))
96
- return false;
97
- if (err.userErrors.length === 0)
98
- return false;
99
- return err.userErrors.some((ue) => typeof ue.code === 'string' && SESSION_CODE_SET.has(ue.code));
100
- }
101
- /**
102
- * Thrown when a cart operation fails with a session error
103
- * (`CART_UNAUTHENTICATED`). Distinct from `CartRecoveryNotPossibleError`
104
- * (cart-resource issue) — storefronts should redirect to a sign-in flow
105
- * and retry the operation after re-auth.
106
- */
107
- export class CartSessionRequiredError extends Error {
108
- cause;
109
- constructor(cause, message) {
110
- super(message ?? 'Session required — please sign in to continue', { cause });
111
- this.name = 'CartSessionRequiredError';
112
- this.cause = cause;
113
- }
114
- }
115
70
  /**
116
71
  * Thrown when an operation hits a recoverable cart error but recovery is not
117
72
  * possible (no `recreateAndRun`, or recovery itself failed). UI should clear
@@ -150,13 +105,32 @@ async function acquireCartId(coord, factory) {
150
105
  coord.inFlight = p;
151
106
  return p;
152
107
  }
108
+ /**
109
+ * Promote a server-known seed into the cookie store and return its cart id.
110
+ *
111
+ * The seed may be a bare cart id or the composite `<cartId>.<secret>` value.
112
+ * When it carries a secret the composite is written so later requests send the
113
+ * access secret and reach the seeded cart; a bare id is written without one and
114
+ * degrades to a fresh cart on the next write. Returns `null` when the seed does
115
+ * not parse to a usable cart id.
116
+ */
117
+ function seedCookieFromInitialCartId(cookieStore, initialCartId, cookieMaxAge) {
118
+ const parsed = parseCartCookieValue(initialCartId);
119
+ if (!parsed)
120
+ return null;
121
+ cookieStore.set(parsed.cartId, {
122
+ secret: parsed.cartSecret,
123
+ ...(cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : {}),
124
+ });
125
+ return parsed.cartId;
126
+ }
153
127
  /**
154
128
  * Runs an operation against the current cart, recovering once on
155
129
  * `CART_NOT_FOUND` / `ALREADY_COMPLETED`. Pure async function — non-React
156
130
  * consumers can wire this directly without the runner factory.
157
131
  */
158
132
  export async function executeWithCartRecovery(opts) {
159
- const { cartClient, cookieStore, operation, ensureCart, cookieMaxAge, onExpired, onSessionExpired, initialCartId, } = opts;
133
+ const { cartClient, cookieStore, operation, ensureCart, cookieMaxAge, onExpired, initialCartId, } = opts;
160
134
  const coord = opts.recoveryCoordinator ?? createCoordinator();
161
135
  const opName = operation.name ?? 'unknown';
162
136
  // Phase 0 — ensure cart exists (cookie may be empty on first interaction).
@@ -167,14 +141,16 @@ export async function executeWithCartRecovery(opts) {
167
141
  // path (Phase 2a/2b) operates on a known cookie when the seed turns out stale.
168
142
  let cartId = cookieStore.get();
169
143
  if (!cartId && initialCartId) {
170
- cookieStore.set(initialCartId, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
171
- cartId = initialCartId;
144
+ cartId = seedCookieFromInitialCartId(cookieStore, initialCartId, cookieMaxAge);
172
145
  }
173
146
  if (!cartId) {
174
147
  cartId = await acquireCartId(coord, async () => {
175
- const created = ensureCart ? await ensureCart() : (await cartClient.create()).cart;
176
- cookieStore.set(created.id, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
177
- return created.id;
148
+ const created = ensureCart ? await ensureCart() : await cartClient.create();
149
+ cookieStore.set(created.cart.id, {
150
+ secret: created.secret ?? null,
151
+ ...(cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : {}),
152
+ });
153
+ return created.cart.id;
178
154
  });
179
155
  }
180
156
  // Phase 1 — happy path.
@@ -182,17 +158,6 @@ export async function executeWithCartRecovery(opts) {
182
158
  return await operation.run(cartId);
183
159
  }
184
160
  catch (err) {
185
- // Session loss — cart resource intact, re-auth needed. Preserve cookie
186
- // so the same cart resumes after a successful login.
187
- if (isCartSessionError(err)) {
188
- const sessionEvent = {
189
- oldCartId: cartId,
190
- operation: opName,
191
- cause: err,
192
- };
193
- onSessionExpired?.(sessionEvent);
194
- throw new CartSessionRequiredError(err);
195
- }
196
161
  if (!isCartRecoverableError(err))
197
162
  throw err;
198
163
  const oldCartId = cartId;
@@ -220,7 +185,10 @@ export async function executeWithCartRecovery(opts) {
220
185
  try {
221
186
  cookieStore.clear();
222
187
  const recreated = await operation.recreateAndRun(cartClient);
223
- cookieStore.set(recreated.cart.id, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
188
+ cookieStore.set(recreated.cart.id, {
189
+ secret: recreated.secret ?? null,
190
+ ...(cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : {}),
191
+ });
224
192
  result = recreated.result;
225
193
  }
226
194
  catch (recreateErr) {
@@ -249,7 +217,6 @@ export function createCartRecoveryRunner(options) {
249
217
  const { cartClient, cookieStore, ensureCart, cookieMaxAge, initialCartId } = options;
250
218
  const coordinator = createCoordinator();
251
219
  const expiredListeners = new Set();
252
- const sessionListeners = new Set();
253
220
  function emitExpired(event) {
254
221
  for (const listener of expiredListeners) {
255
222
  try {
@@ -260,16 +227,6 @@ export function createCartRecoveryRunner(options) {
260
227
  }
261
228
  }
262
229
  }
263
- function emitSessionExpired(event) {
264
- for (const listener of sessionListeners) {
265
- try {
266
- listener(event);
267
- }
268
- catch {
269
- // Listeners must not break recovery flow — swallow listener exceptions.
270
- }
271
- }
272
- }
273
230
  return {
274
231
  cartClient,
275
232
  cookieStore,
@@ -283,7 +240,6 @@ export function createCartRecoveryRunner(options) {
283
240
  initialCartId,
284
241
  recoveryCoordinator: coordinator,
285
242
  onExpired: emitExpired,
286
- onSessionExpired: emitSessionExpired,
287
243
  };
288
244
  return executeWithCartRecovery(internalOpts);
289
245
  },
@@ -293,8 +249,7 @@ export function createCartRecoveryRunner(options) {
293
249
  // from the canonical source without re-reading the seed.
294
250
  let cartId = cookieStore.get();
295
251
  if (!cartId && initialCartId) {
296
- cookieStore.set(initialCartId, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
297
- cartId = initialCartId;
252
+ cartId = seedCookieFromInitialCartId(cookieStore, initialCartId, cookieMaxAge);
298
253
  }
299
254
  if (!cartId)
300
255
  return null;
@@ -302,15 +257,6 @@ export function createCartRecoveryRunner(options) {
302
257
  return await cartClient.get(cartId);
303
258
  }
304
259
  catch (err) {
305
- if (isCartSessionError(err)) {
306
- // Cart resource intact — re-auth required. Preserve cookie.
307
- emitSessionExpired({
308
- oldCartId: cartId,
309
- operation: 'getCart',
310
- cause: err,
311
- });
312
- throw new CartSessionRequiredError(err);
313
- }
314
260
  if (isCartRecoverableError(err)) {
315
261
  cookieStore.clear();
316
262
  emitExpired({
@@ -328,10 +274,6 @@ export function createCartRecoveryRunner(options) {
328
274
  expiredListeners.add(listener);
329
275
  return () => expiredListeners.delete(listener);
330
276
  },
331
- onSessionExpired(listener) {
332
- sessionListeners.add(listener);
333
- return () => sessionListeners.delete(listener);
334
- },
335
277
  };
336
278
  }
337
279
  // ---------------------------------------------------------------------------
@@ -359,7 +301,7 @@ export function createCartRecoveryRunner(options) {
359
301
  export function recreateWithInput(input) {
360
302
  return async (cartClient) => {
361
303
  const outcome = await cartClient.create(input);
362
- return { cart: outcome.cart, result: outcome };
304
+ return { cart: outcome.cart, secret: outcome.secret, result: outcome };
363
305
  };
364
306
  }
365
307
  /**
@@ -2,13 +2,39 @@
2
2
  * Cart cookie configuration — platform contract.
3
3
  *
4
4
  * Used by:
5
- * - SDK cart store (client-side cookie read/write for cartId)
5
+ * - SDK cart store (client-side cookie read/write)
6
6
  * - Server-side cart prefetching (SSR cart badge, middleware)
7
7
  * - proxy.ts (edge cart ID detection)
8
8
  *
9
- * Single cookie for cart ID persistence. Value is a plain cart ID string
10
- * (not JSON)server can read it directly via cookies().get('cart-id').
9
+ * The cookie value is composite: `<cartId>.<secret>`. The cart id is a UUID and
10
+ * the secret is base64url neither contains a dot, so the first dot separates
11
+ * the two halves. A legacy plain `<cartId>` value (written before the cart
12
+ * carried a secret) parses with `cartSecret: null`; the client then operates
13
+ * without the `x-cart-secret` header and the backend treats the cart as
14
+ * unreachable, so the SDK recreates a fresh one.
11
15
  */
12
16
  export declare const CART_COOKIE_NAME = "cart-id";
13
17
  export declare const CART_COOKIE_MAX_AGE: number;
18
+ /**
19
+ * Structured cart cookie value — the cart identifier plus its access secret.
20
+ * `cartSecret` is `null` for a legacy plain-id cookie (no secret half).
21
+ */
22
+ export interface CartCredentials {
23
+ cartId: string;
24
+ cartSecret: string | null;
25
+ }
26
+ /**
27
+ * Parse the raw `cart-id` cookie value into `{ cartId, cartSecret }`.
28
+ *
29
+ * - `"<uuid>.<secret>"` → both halves (split on the first dot).
30
+ * - `"<uuid>"` (no dot, legacy) → `{ cartId, cartSecret: null }`.
31
+ * - trailing dot (`"<uuid>."`) → id only, `cartSecret: null`.
32
+ * - empty / leading-dot / blank → `null` (no usable cart id).
33
+ */
34
+ export declare function parseCartCookieValue(raw: string | null | undefined): CartCredentials | null;
35
+ /** Build the composite `<cartId>.<secret>` cookie value. */
36
+ export declare function formatCartCookieValue(input: {
37
+ cartId: string;
38
+ cartSecret: string;
39
+ }): string;
14
40
  //# sourceMappingURL=cookie-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cookie-config.d.ts","sourceRoot":"","sources":["../../../src/core/cart/cookie-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAC1C,eAAO,MAAM,mBAAmB,QAAoB,CAAC"}
1
+ {"version":3,"file":"cookie-config.d.ts","sourceRoot":"","sources":["../../../src/core/cart/cookie-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAC1C,eAAO,MAAM,mBAAmB,QAAoB,CAAC;AAErD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAc3F;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAE3F"}
@@ -2,12 +2,44 @@
2
2
  * Cart cookie configuration — platform contract.
3
3
  *
4
4
  * Used by:
5
- * - SDK cart store (client-side cookie read/write for cartId)
5
+ * - SDK cart store (client-side cookie read/write)
6
6
  * - Server-side cart prefetching (SSR cart badge, middleware)
7
7
  * - proxy.ts (edge cart ID detection)
8
8
  *
9
- * Single cookie for cart ID persistence. Value is a plain cart ID string
10
- * (not JSON)server can read it directly via cookies().get('cart-id').
9
+ * The cookie value is composite: `<cartId>.<secret>`. The cart id is a UUID and
10
+ * the secret is base64url neither contains a dot, so the first dot separates
11
+ * the two halves. A legacy plain `<cartId>` value (written before the cart
12
+ * carried a secret) parses with `cartSecret: null`; the client then operates
13
+ * without the `x-cart-secret` header and the backend treats the cart as
14
+ * unreachable, so the SDK recreates a fresh one.
11
15
  */
12
16
  export const CART_COOKIE_NAME = 'cart-id';
13
17
  export const CART_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days
18
+ /**
19
+ * Parse the raw `cart-id` cookie value into `{ cartId, cartSecret }`.
20
+ *
21
+ * - `"<uuid>.<secret>"` → both halves (split on the first dot).
22
+ * - `"<uuid>"` (no dot, legacy) → `{ cartId, cartSecret: null }`.
23
+ * - trailing dot (`"<uuid>."`) → id only, `cartSecret: null`.
24
+ * - empty / leading-dot / blank → `null` (no usable cart id).
25
+ */
26
+ export function parseCartCookieValue(raw) {
27
+ if (!raw)
28
+ return null;
29
+ const trimmed = raw.trim();
30
+ if (!trimmed)
31
+ return null;
32
+ const dotIndex = trimmed.indexOf('.');
33
+ if (dotIndex === -1) {
34
+ return { cartId: trimmed, cartSecret: null };
35
+ }
36
+ const cartId = trimmed.slice(0, dotIndex);
37
+ const cartSecret = trimmed.slice(dotIndex + 1);
38
+ if (!cartId)
39
+ return null;
40
+ return { cartId, cartSecret: cartSecret || null };
41
+ }
42
+ /** Build the composite `<cartId>.<secret>` cookie value. */
43
+ export function formatCartCookieValue(input) {
44
+ return `${input.cartId}.${input.cartSecret}`;
45
+ }