@doswiftly/storefront-sdk 17.0.0 → 18.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 (83) hide show
  1. package/CHANGELOG.md +919 -0
  2. package/README.md +16 -4
  3. package/dist/core/auth/auth-client.d.ts +39 -3
  4. package/dist/core/auth/auth-client.d.ts.map +1 -1
  5. package/dist/core/auth/auth-client.js +51 -3
  6. package/dist/core/auth/cookie-config.d.ts +52 -3
  7. package/dist/core/auth/cookie-config.d.ts.map +1 -1
  8. package/dist/core/auth/cookie-config.js +60 -6
  9. package/dist/core/auth/handlers.d.ts +46 -0
  10. package/dist/core/auth/handlers.d.ts.map +1 -1
  11. package/dist/core/auth/handlers.js +9 -2
  12. package/dist/core/auth/session-events.d.ts +38 -0
  13. package/dist/core/auth/session-events.d.ts.map +1 -0
  14. package/dist/core/auth/session-events.js +35 -0
  15. package/dist/core/cart/cart-recovery.d.ts +23 -0
  16. package/dist/core/cart/cart-recovery.d.ts.map +1 -1
  17. package/dist/core/cart/cart-recovery.js +20 -3
  18. package/dist/core/cart/types.d.ts +2 -1
  19. package/dist/core/cart/types.d.ts.map +1 -1
  20. package/dist/core/cart/types.js +7 -1
  21. package/dist/core/client/create-client.d.ts.map +1 -1
  22. package/dist/core/client/create-client.js +7 -3
  23. package/dist/core/client/execute.d.ts +29 -3
  24. package/dist/core/client/execute.d.ts.map +1 -1
  25. package/dist/core/client/execute.js +174 -3
  26. package/dist/core/client/types.d.ts +50 -2
  27. package/dist/core/client/types.d.ts.map +1 -1
  28. package/dist/core/errors.d.ts +6 -0
  29. package/dist/core/errors.d.ts.map +1 -1
  30. package/dist/core/errors.js +6 -0
  31. package/dist/core/generated/operation-types.d.ts +838 -221
  32. package/dist/core/generated/operation-types.d.ts.map +1 -1
  33. package/dist/core/generated/operation-types.js +560 -1
  34. package/dist/core/index.d.ts +6 -3
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +12 -2
  37. package/dist/core/middleware/session-retry.d.ts +47 -0
  38. package/dist/core/middleware/session-retry.d.ts.map +1 -0
  39. package/dist/core/middleware/session-retry.js +71 -0
  40. package/dist/core/operations/auth.d.ts.map +1 -1
  41. package/dist/core/operations/auth.js +1 -0
  42. package/dist/core/operations/cart.d.ts.map +1 -1
  43. package/dist/core/operations/cart.js +15 -11
  44. package/dist/react/components/PaymentInstrumentSection.d.ts.map +1 -1
  45. package/dist/react/components/PaymentInstrumentSection.js +4 -4
  46. package/dist/react/components/PaymentInstrumentTile.d.ts +7 -7
  47. package/dist/react/components/PaymentInstrumentTile.d.ts.map +1 -1
  48. package/dist/react/components/PaymentInstrumentTile.js +4 -3
  49. package/dist/react/hooks/use-cart-manager.d.ts +104 -13
  50. package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
  51. package/dist/react/hooks/use-cart-manager.js +144 -12
  52. package/dist/react/hooks/use-login.d.ts.map +1 -1
  53. package/dist/react/hooks/use-login.js +3 -3
  54. package/dist/react/hooks/use-refresh-token.d.ts.map +1 -1
  55. package/dist/react/hooks/use-refresh-token.js +6 -4
  56. package/dist/react/hooks/use-session-expired.d.ts +16 -0
  57. package/dist/react/hooks/use-session-expired.d.ts.map +1 -0
  58. package/dist/react/hooks/use-session-expired.js +26 -0
  59. package/dist/react/hooks/use-session-refresh.d.ts +32 -0
  60. package/dist/react/hooks/use-session-refresh.d.ts.map +1 -0
  61. package/dist/react/hooks/use-session-refresh.js +147 -0
  62. package/dist/react/index.d.ts +3 -0
  63. package/dist/react/index.d.ts.map +1 -1
  64. package/dist/react/index.js +2 -0
  65. package/dist/react/providers/storefront-client-provider.d.ts +10 -1
  66. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
  67. package/dist/react/providers/storefront-client-provider.js +38 -3
  68. package/dist/react/providers/storefront-provider.d.ts +51 -3
  69. package/dist/react/providers/storefront-provider.d.ts.map +1 -1
  70. package/dist/react/providers/storefront-provider.js +22 -5
  71. package/dist/react/server/create-storefront-auth-route.d.ts +63 -0
  72. package/dist/react/server/create-storefront-auth-route.d.ts.map +1 -0
  73. package/dist/react/server/create-storefront-auth-route.js +239 -0
  74. package/dist/react/server/get-initial-auth.d.ts +57 -0
  75. package/dist/react/server/get-initial-auth.d.ts.map +1 -0
  76. package/dist/react/server/get-initial-auth.js +55 -0
  77. package/dist/react/server/index.d.ts +3 -0
  78. package/dist/react/server/index.d.ts.map +1 -1
  79. package/dist/react/server/index.js +6 -0
  80. package/dist/react/stores/auth.store.d.ts +46 -2
  81. package/dist/react/stores/auth.store.d.ts.map +1 -1
  82. package/dist/react/stores/auth.store.js +19 -7
  83. package/package.json +4 -2
package/README.md CHANGED
@@ -151,10 +151,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
151
151
  // `shop.botProtection` — whatever StorefrontProvider needs.
152
152
  const { shop } = await serverClient.query(SHOP_BASICS_QUERY);
153
153
 
154
- // Read the httpOnly auth cookie server-side so we can hydrate the auth store
155
- // without a flash of "Sign In".
154
+ // Read the raw JWT from the httpOnly auth cookie server-side and seed it into
155
+ // the auth store. This eliminates the round-trip to `/api/auth/whoami` on the
156
+ // first mount — `authMiddleware` adds `Authorization: Bearer ...` from the
157
+ // very first request. Token stays in memory only (never written to
158
+ // localStorage — `persist.partialize` excludes `accessToken`, XSS hardening).
156
159
  const cookieStore = await cookies();
157
- const initialIsAuthenticated = Boolean(cookieStore.get(AUTH_COOKIE_NAME)?.value);
160
+ const initialAccessToken = cookieStore.get(AUTH_COOKIE_NAME)?.value ?? null;
158
161
 
159
162
  return (
160
163
  <html lang="pl">
@@ -165,7 +168,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
165
168
  shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
166
169
  }}
167
170
  shopData={shop}
168
- initialIsAuthenticated={initialIsAuthenticated}
171
+ initialAccessToken={initialAccessToken}
169
172
  >
170
173
  {children}
171
174
  </StorefrontProvider>
@@ -175,6 +178,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
175
178
  }
176
179
  ```
177
180
 
181
+ > **Tip — `initialIsAuthenticated` vs `initialAccessToken`:** if you only know
182
+ > *whether* the user is signed in (cookie present, value not readable server-side),
183
+ > pass `initialIsAuthenticated={Boolean(...)}` — UI starts in the right gating
184
+ > state, and the SDK fetches the token via `createWhoamiHandler` on first mount.
185
+ > If you have the raw JWT server-side (cookie value, SSO redirect param,
186
+ > dev-seed env var), prefer `initialAccessToken={token}` — no round-trip needed.
187
+ > When `initialAccessToken` is truthy, `isAuthenticated` defaults to `true`
188
+ > automatically; pass `initialIsAuthenticated={false}` to override.
189
+
178
190
  **2. `app/api/auth/set-token/route.ts`** — BFF route that sets the httpOnly
179
191
  cookie. The SDK ships factories so each file is two lines:
180
192
 
@@ -17,9 +17,26 @@
17
17
  */
18
18
  import type { StorefrontClient } from '../client/types';
19
19
  import type { AuthResult, Customer, CustomerCreateInput, MailingAddress } from './types';
20
+ /** Result of a same-origin BFF session refresh — the new access token + its absolute expiry. */
21
+ export interface SessionRefreshResult {
22
+ accessToken: string;
23
+ expiresAt: string;
24
+ }
25
+ /**
26
+ * Options configuring the same-origin BFF transport used by {@link AuthClient.refreshSession}.
27
+ * GraphQL auth operations (login/register/getCustomer) keep using the injected
28
+ * `StorefrontClient`; only `refreshSession` talks to the same-origin route.
29
+ */
30
+ export interface AuthClientOptions {
31
+ /** Base path of the SDK-BFF auth route handlers (default `/api/auth`). */
32
+ authBasePath?: string;
33
+ /** Custom fetch (tests, non-standard runtimes). Defaults to `globalThis.fetch`. */
34
+ fetch?: typeof globalThis.fetch;
35
+ }
20
36
  export declare class AuthClient {
21
37
  private readonly client;
22
- constructor(client: StorefrontClient);
38
+ private readonly options;
39
+ constructor(client: StorefrontClient, options?: AuthClientOptions);
23
40
  /**
24
41
  * Login with email and password.
25
42
  * Returns access token + expiry.
@@ -33,8 +50,27 @@ export declare class AuthClient {
33
50
  */
34
51
  logout(): Promise<void>;
35
52
  /**
36
- * Refresh access token extends expiry. Auth context czytany przez backend
37
- * z cookie / Authorization Bearer (jak inne authenticated mutations).
53
+ * Refresh the session via the same-origin BFF route (`POST {authBasePath}/refresh`).
54
+ *
55
+ * This is the browser refresh path: the route handler reads the httpOnly
56
+ * refresh cookie server-side, rotates it against the backend server-to-server,
57
+ * sets the new first-party cookies, and returns only the new access token +
58
+ * absolute expiry. The refresh token is never read by JS — so an EXPIRED access
59
+ * token still refreshes (the GraphQL `customerRefreshToken` mutation could not,
60
+ * as it required a valid access token).
61
+ *
62
+ * Throws a {@link StorefrontError} with code `SESSION_EXPIRED` when the route
63
+ * responds non-OK (reused/expired/missing refresh token) — the scheduler and
64
+ * the reactive-401 middleware translate that into a `session-expired` signal.
65
+ */
66
+ refreshSession(): Promise<SessionRefreshResult>;
67
+ /**
68
+ * Refresh the access token via the GraphQL `customerRefreshToken` mutation.
69
+ *
70
+ * @deprecated The browser refresh now goes through {@link refreshSession}
71
+ * (same-origin BFF). This GraphQL mutation requires a still-valid access token
72
+ * and is retained for backward compatibility until it is removed in a future
73
+ * major release. Prefer `refreshSession()`.
38
74
  */
39
75
  refreshToken(): Promise<AuthResult>;
40
76
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../../src/core/auth/auth-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAWzF,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IAErD;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAiBjE;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAa7B;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC;IAiBzC;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB/D;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAQ7C;;;;;;;;;;OAUG;IACG,YAAY,IAAI,OAAO,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC;CAMvD"}
1
+ {"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../../src/core/auth/auth-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAYzF,gGAAgG;AAChG,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,qBAAa,UAAU;IAEnB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,iBAAsB;IAGlD;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAiBjE;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAa7B;;;;;;;;;;;;;OAaG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAgCrD;;;;;;;OAOG;IACG,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC;IAiBzC;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB/D;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAQ7C;;;;;;;;;;OAUG;IACG,YAAY,IAAI,OAAO,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC;CAMvD"}
@@ -16,11 +16,14 @@
16
16
  * ```
17
17
  */
18
18
  import { assertNoUserErrors } from '../helpers/assert-no-user-errors';
19
+ import { StorefrontError, ErrorCodes } from '../errors';
19
20
  import { CUSTOMER_LOGIN, CUSTOMER_LOGOUT, CUSTOMER_REFRESH_TOKEN, CUSTOMER_SIGNUP, CUSTOMER_QUERY, CUSTOMER_ADDRESSES_QUERY, } from '../operations/auth';
20
21
  export class AuthClient {
21
22
  client;
22
- constructor(client) {
23
+ options;
24
+ constructor(client, options = {}) {
23
25
  this.client = client;
26
+ this.options = options;
24
27
  }
25
28
  /**
26
29
  * Login with email and password.
@@ -50,8 +53,53 @@ export class AuthClient {
50
53
  }
51
54
  }
52
55
  /**
53
- * Refresh access token extends expiry. Auth context czytany przez backend
54
- * z cookie / Authorization Bearer (jak inne authenticated mutations).
56
+ * Refresh the session via the same-origin BFF route (`POST {authBasePath}/refresh`).
57
+ *
58
+ * This is the browser refresh path: the route handler reads the httpOnly
59
+ * refresh cookie server-side, rotates it against the backend server-to-server,
60
+ * sets the new first-party cookies, and returns only the new access token +
61
+ * absolute expiry. The refresh token is never read by JS — so an EXPIRED access
62
+ * token still refreshes (the GraphQL `customerRefreshToken` mutation could not,
63
+ * as it required a valid access token).
64
+ *
65
+ * Throws a {@link StorefrontError} with code `SESSION_EXPIRED` when the route
66
+ * responds non-OK (reused/expired/missing refresh token) — the scheduler and
67
+ * the reactive-401 middleware translate that into a `session-expired` signal.
68
+ */
69
+ async refreshSession() {
70
+ const base = (this.options.authBasePath ?? '/api/auth').replace(/\/$/, '');
71
+ const fetchFn = this.options.fetch ?? globalThis.fetch;
72
+ const response = await fetchFn(`${base}/refresh`, {
73
+ method: 'POST',
74
+ headers: { Accept: 'application/json' },
75
+ // Same-origin route on the storefront domain — the httpOnly refresh cookie
76
+ // is attached automatically; the token never enters JavaScript.
77
+ credentials: 'same-origin',
78
+ });
79
+ if (!response.ok) {
80
+ throw new StorefrontError({
81
+ code: ErrorCodes.SESSION_EXPIRED,
82
+ message: `Session refresh failed (HTTP ${response.status})`,
83
+ status: response.status,
84
+ });
85
+ }
86
+ const data = (await response.json());
87
+ if (!data.accessToken || !data.expiresAt) {
88
+ throw new StorefrontError({
89
+ code: ErrorCodes.SESSION_EXPIRED,
90
+ message: 'Session refresh returned an incomplete token pair',
91
+ status: response.status,
92
+ });
93
+ }
94
+ return { accessToken: data.accessToken, expiresAt: data.expiresAt };
95
+ }
96
+ /**
97
+ * Refresh the access token via the GraphQL `customerRefreshToken` mutation.
98
+ *
99
+ * @deprecated The browser refresh now goes through {@link refreshSession}
100
+ * (same-origin BFF). This GraphQL mutation requires a still-valid access token
101
+ * and is retained for backward compatibility until it is removed in a future
102
+ * major release. Prefer `refreshSession()`.
55
103
  */
56
104
  async refreshToken() {
57
105
  const data = await this.client.mutate(CUSTOMER_REFRESH_TOKEN);
@@ -1,11 +1,20 @@
1
1
  /**
2
2
  * Auth Cookie Configuration — platform contract.
3
3
  *
4
- * Backend validates cookie by name `customerAccessToken`.
5
- * Every storefront MUST use the same name. Security options
6
- * (httpOnly, secure, sameSite) are the platform standard.
4
+ * The SDK-BFF route handlers (`createStorefrontAuthRoute`) own these first-party
5
+ * cookies on the storefront domain; the backend never emits a `Set-Cookie` to a
6
+ * browser (it returns tokens in the body, server-to-server). Every storefront
7
+ * MUST use the same cookie names — they are read by the SDK auth middleware
8
+ * (access) and the proactive scheduler (session-expiry), and forwarded
9
+ * server-side by the BFF (refresh). Changing a name is a platform-wide contract
10
+ * change (backend rotation namespace + middleware + template).
7
11
  */
12
+ /** Short-lived access token (Bearer). Host-only, httpOnly, `Path=/` (SSR + every request). */
8
13
  export declare const AUTH_COOKIE_NAME = "customerAccessToken";
14
+ /** Long-lived rotation refresh token. Host-only, httpOnly, isolated to the refresh path. */
15
+ export declare const REFRESH_COOKIE_NAME = "customerRefreshToken";
16
+ /** Readable (non-httpOnly) absolute session-expiry hint for the proactive scheduler's cold start. */
17
+ export declare const SESSION_EXPIRY_COOKIE_NAME = "session-expiry";
9
18
  export declare const AUTH_COOKIE_DEFAULTS: {
10
19
  readonly name: "customerAccessToken";
11
20
  readonly path: "/";
@@ -15,4 +24,44 @@ export declare const AUTH_COOKIE_DEFAULTS: {
15
24
  readonly maxAge: number;
16
25
  };
17
26
  export type AuthCookieConfig = typeof AUTH_COOKIE_DEFAULTS;
27
+ /**
28
+ * Refresh-token cookie attributes. Scoped to the auth base path (`/api/auth`) so
29
+ * it reaches BOTH the refresh route (to rotate) and the logout route (to revoke,
30
+ * via possession-proof) while staying off GraphQL data traffic, which lives on a
31
+ * different path. The token is read only server-side and never reaches
32
+ * JavaScript. `maxAge` is the refresh-token lifetime (30 days); the backend body
33
+ * carries no refresh expiry, so the BFF route uses this constant.
34
+ *
35
+ * The `path` must equal the route's mount base so logout receives the cookie — a
36
+ * narrower `/api/auth/refresh` would NOT be sent to `/api/auth/logout` (cookies
37
+ * are not sent to sibling paths), leaving the family un-revoked on logout.
38
+ */
39
+ export declare const REFRESH_COOKIE_DEFAULTS: {
40
+ readonly name: "customerRefreshToken";
41
+ readonly path: "/api/auth";
42
+ readonly sameSite: "lax";
43
+ readonly httpOnly: true;
44
+ readonly secure: boolean;
45
+ readonly maxAge: number;
46
+ };
47
+ export type RefreshCookieConfig = typeof REFRESH_COOKIE_DEFAULTS;
48
+ /**
49
+ * Session-expiry hint cookie attributes. Deliberately NOT httpOnly — the value
50
+ * is only an absolute ISO timestamp (no secret), readable so the proactive
51
+ * scheduler can arm on a cold start without a whoami round-trip. It lives as long
52
+ * as the refresh session (30 days), NOT as long as the short access token: a
53
+ * returning visitor whose access token has lapsed must still find this hint on a
54
+ * cold start so the app renders signed-in and recovers the session (a past
55
+ * timestamp tells the scheduler to refresh immediately) instead of flashing
56
+ * signed-out.
57
+ */
58
+ export declare const SESSION_EXPIRY_COOKIE_DEFAULTS: {
59
+ readonly name: "session-expiry";
60
+ readonly path: "/";
61
+ readonly sameSite: "lax";
62
+ readonly httpOnly: false;
63
+ readonly secure: boolean;
64
+ readonly maxAge: number;
65
+ };
66
+ export type SessionExpiryCookieConfig = typeof SESSION_EXPIRY_COOKIE_DEFAULTS;
18
67
  //# sourceMappingURL=cookie-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cookie-config.d.ts","sourceRoot":"","sources":["../../../src/core/auth/cookie-config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AAEtD,eAAO,MAAM,oBAAoB;;;;;;;CAUvB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,OAAO,oBAAoB,CAAC"}
1
+ {"version":3,"file":"cookie-config.d.ts","sourceRoot":"","sources":["../../../src/core/auth/cookie-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AAEtD,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,mBAAmB,CAAC;AAa3D,eAAO,MAAM,oBAAoB;;;;;;;CAOvB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,OAAO,oBAAoB,CAAC;AAE3D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB;;;;;;;CAO1B,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,OAAO,uBAAuB,CAAC;AAEjE;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B;;;;;;;CAOjC,CAAC;AAEX,MAAM,MAAM,yBAAyB,GAAG,OAAO,8BAA8B,CAAC"}
@@ -1,18 +1,72 @@
1
1
  /**
2
2
  * Auth Cookie Configuration — platform contract.
3
3
  *
4
- * Backend validates cookie by name `customerAccessToken`.
5
- * Every storefront MUST use the same name. Security options
6
- * (httpOnly, secure, sameSite) are the platform standard.
4
+ * The SDK-BFF route handlers (`createStorefrontAuthRoute`) own these first-party
5
+ * cookies on the storefront domain; the backend never emits a `Set-Cookie` to a
6
+ * browser (it returns tokens in the body, server-to-server). Every storefront
7
+ * MUST use the same cookie names — they are read by the SDK auth middleware
8
+ * (access) and the proactive scheduler (session-expiry), and forwarded
9
+ * server-side by the BFF (refresh). Changing a name is a platform-wide contract
10
+ * change (backend rotation namespace + middleware + template).
7
11
  */
12
+ /** Short-lived access token (Bearer). Host-only, httpOnly, `Path=/` (SSR + every request). */
8
13
  export const AUTH_COOKIE_NAME = 'customerAccessToken';
14
+ /** Long-lived rotation refresh token. Host-only, httpOnly, isolated to the refresh path. */
15
+ export const REFRESH_COOKIE_NAME = 'customerRefreshToken';
16
+ /** Readable (non-httpOnly) absolute session-expiry hint for the proactive scheduler's cold start. */
17
+ export const SESSION_EXPIRY_COOKIE_NAME = 'session-expiry';
18
+ /**
19
+ * Whether cookies must carry the `Secure` attribute. `true` over HTTPS in the
20
+ * browser and in production on the server; `false` in local HTTP development so
21
+ * the cookie is still accepted. Evaluated once at module load — the BFF route
22
+ * runs server-side, so the `process.env.NODE_ENV` branch is the live one there.
23
+ */
24
+ const SECURE = typeof window !== 'undefined'
25
+ ? window.location.protocol === 'https:'
26
+ : process.env.NODE_ENV === 'production';
9
27
  export const AUTH_COOKIE_DEFAULTS = {
10
28
  name: AUTH_COOKIE_NAME,
11
29
  path: '/',
12
30
  sameSite: 'lax',
13
31
  httpOnly: true,
14
- secure: typeof window !== 'undefined'
15
- ? window.location.protocol === 'https:'
16
- : process.env.NODE_ENV === 'production',
32
+ secure: SECURE,
33
+ maxAge: 60 * 60 * 24 * 30, // 30 days
34
+ };
35
+ /**
36
+ * Refresh-token cookie attributes. Scoped to the auth base path (`/api/auth`) so
37
+ * it reaches BOTH the refresh route (to rotate) and the logout route (to revoke,
38
+ * via possession-proof) while staying off GraphQL data traffic, which lives on a
39
+ * different path. The token is read only server-side and never reaches
40
+ * JavaScript. `maxAge` is the refresh-token lifetime (30 days); the backend body
41
+ * carries no refresh expiry, so the BFF route uses this constant.
42
+ *
43
+ * The `path` must equal the route's mount base so logout receives the cookie — a
44
+ * narrower `/api/auth/refresh` would NOT be sent to `/api/auth/logout` (cookies
45
+ * are not sent to sibling paths), leaving the family un-revoked on logout.
46
+ */
47
+ export const REFRESH_COOKIE_DEFAULTS = {
48
+ name: REFRESH_COOKIE_NAME,
49
+ path: '/api/auth',
50
+ sameSite: 'lax',
51
+ httpOnly: true,
52
+ secure: SECURE,
17
53
  maxAge: 60 * 60 * 24 * 30, // 30 days
18
54
  };
55
+ /**
56
+ * Session-expiry hint cookie attributes. Deliberately NOT httpOnly — the value
57
+ * is only an absolute ISO timestamp (no secret), readable so the proactive
58
+ * scheduler can arm on a cold start without a whoami round-trip. It lives as long
59
+ * as the refresh session (30 days), NOT as long as the short access token: a
60
+ * returning visitor whose access token has lapsed must still find this hint on a
61
+ * cold start so the app renders signed-in and recovers the session (a past
62
+ * timestamp tells the scheduler to refresh immediately) instead of flashing
63
+ * signed-out.
64
+ */
65
+ export const SESSION_EXPIRY_COOKIE_DEFAULTS = {
66
+ name: SESSION_EXPIRY_COOKIE_NAME,
67
+ path: '/',
68
+ sameSite: 'lax',
69
+ httpOnly: false,
70
+ secure: SECURE,
71
+ maxAge: 60 * 60 * 24 * 30, // 30 days — outlives the access token so cold-start recovery works
72
+ };
@@ -51,6 +51,52 @@ interface ClearTokenHandlerOptions {
51
51
  /** Optional predicate that bypasses strict Host check when truthy. */
52
52
  isTrustedOrigin?: OriginValidator | null;
53
53
  }
54
+ /**
55
+ * Serialize a single `Set-Cookie` header value. Shared by the set/clear token
56
+ * handlers and the `createStorefrontAuthRoute` factory so cookie attribute
57
+ * emission stays identical across both. The value is emitted as-is — callers
58
+ * pass cookie-safe values (JWT base64url, opaque `{familyId}.{uuid}`, ISO
59
+ * timestamps) or pre-encode.
60
+ */
61
+ export declare function serializeCookie(name: string, value: string, options: {
62
+ maxAge?: number;
63
+ path?: string;
64
+ sameSite?: string;
65
+ secure?: boolean;
66
+ httpOnly?: boolean;
67
+ }): string;
68
+ /**
69
+ * Validate the Origin header against the Host header to prevent CSRF from
70
+ * unrelated origins.
71
+ *
72
+ * Compares the *parsed* origin's host with the request's host header, not a
73
+ * substring match — `origin.includes(host)` was bypassable two ways:
74
+ * - `host = ""` (or missing) → `origin.includes('')` always true;
75
+ * - `origin = "https://attacker.example/?host=trusted"` → substring match
76
+ * passes for any trusted host name.
77
+ *
78
+ * For state-changing methods (POST/PUT/PATCH/DELETE) browsers always send
79
+ * Origin (fetch spec § 3.5.6 — "method is neither GET nor HEAD"). For safe
80
+ * methods (GET/HEAD) browsers OMIT Origin for same-origin requests; passing
81
+ * `allowMissingOrigin: true` skips the "Origin required" check while still
82
+ * validating mismatch when Origin happens to be present (defense-in-depth
83
+ * for cross-origin GET edge cases).
84
+ *
85
+ * When `isTrustedOrigin` is provided, the predicate is evaluated AFTER the
86
+ * Origin is parsed but BEFORE the strict Host comparison. If the predicate
87
+ * returns truthy, the request passes — useful when running behind a reverse
88
+ * proxy that rewrites or strips the `Host` header (Cloudflare Workers
89
+ * dispatch, Vercel, etc.) while preserving the customer-facing host in
90
+ * `X-Forwarded-Host`. The predicate is consulted instead of (not in addition
91
+ * to) the strict check.
92
+ *
93
+ * Server-to-server callers without an Origin header should hit the GraphQL
94
+ * API directly, not these BFF handlers.
95
+ */
96
+ export declare function validateOrigin(request: Request, options?: {
97
+ allowMissingOrigin?: boolean;
98
+ isTrustedOrigin?: OriginValidator | null;
99
+ }): Promise<Response | null>;
54
100
  /**
55
101
  * Pre-built {@link OriginValidator} that passes when the parsed `Origin` host
56
102
  * matches a hostname set by a trusted reverse proxy.
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/core/auth/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,sBAAsB,KACxB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,UAAU,sBAAsB;IAC9B,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED,UAAU,wBAAwB;IAChC,sEAAsE;IACtE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAqHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,6BAA6B,EAAE,eAa3C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,MAAM,EAAE,GACvB,eAAe,CAajB;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,CAAC,EAAE,sBAAsB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiEzC;AAED,UAAU,oBAAoB;IAC5B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,oBAAyB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAwEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,CAAC,EAAE,wBAAwB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAoCzC"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/core/auth/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,sBAAsB,KACxB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,UAAU,sBAAsB;IAC9B,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED,UAAU,wBAAwB;IAChC,sEAAsE;IACtE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GACA,MAAM,CAQR;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IACR,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C,GACA,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA2D1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,6BAA6B,EAAE,eAa3C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,MAAM,EAAE,GACvB,eAAe,CAajB;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,CAAC,EAAE,sBAAsB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiEzC;AAED,UAAU,oBAAoB;IAC5B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,oBAAyB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAwEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,CAAC,EAAE,wBAAwB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAoCzC"}
@@ -13,7 +13,14 @@
13
13
  * ```
14
14
  */
15
15
  import { AUTH_COOKIE_NAME, AUTH_COOKIE_DEFAULTS } from './cookie-config';
16
- function serializeCookie(name, value, options) {
16
+ /**
17
+ * Serialize a single `Set-Cookie` header value. Shared by the set/clear token
18
+ * handlers and the `createStorefrontAuthRoute` factory so cookie attribute
19
+ * emission stays identical across both. The value is emitted as-is — callers
20
+ * pass cookie-safe values (JWT base64url, opaque `{familyId}.{uuid}`, ISO
21
+ * timestamps) or pre-encode.
22
+ */
23
+ export function serializeCookie(name, value, options) {
17
24
  const parts = [`${name}=${value}`];
18
25
  if (options.maxAge != null)
19
26
  parts.push(`Max-Age=${options.maxAge}`);
@@ -55,7 +62,7 @@ function serializeCookie(name, value, options) {
55
62
  * Server-to-server callers without an Origin header should hit the GraphQL
56
63
  * API directly, not these BFF handlers.
57
64
  */
58
- async function validateOrigin(request, options) {
65
+ export async function validateOrigin(request, options) {
59
66
  const origin = request.headers.get('origin');
60
67
  // GET/HEAD same-origin: browser omits Origin per fetch spec — pass through.
61
68
  // Mutating methods (POST/etc.) MUST have Origin (browsers always send it).
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Session-expired event channel — auth-level pub/sub (0-deps core).
3
+ *
4
+ * Distinct from cart-recovery's `CartSessionExpiredEvent` (scoped to one cart
5
+ * operation): this fires when the CUSTOMER auth session can no longer be kept
6
+ * alive — the proactive scheduler's refresh failed on tab wake, or a reactive
7
+ * refresh after a 401 also failed. Storefronts subscribe once globally
8
+ * (`useSessionExpired`) and react (notify + redirect to sign-in).
9
+ *
10
+ * Mirrors the cart-recovery emitter shape (Set + emit-swallows-listener-errors
11
+ * + subscribe→unsubscribe). 0-deps (Inv-1): plain `Set` + closure, no React /
12
+ * DOM. The provider owns the instance (`useRef` + Context, Inv-3) — never a
13
+ * module-level singleton.
14
+ */
15
+ export type SessionExpiredReason =
16
+ /** Proactive scheduler tried to refresh on tab wake but the session was already gone. */
17
+ 'wake-refresh-failed'
18
+ /** A reactive refresh after a 401 failed (the retry also could not renew the session). */
19
+ | 'reactive-refresh-failed'
20
+ /** A state-changing operation hit a 401 and was intentionally not retried. */
21
+ | 'mutation-unauthorized';
22
+ export interface SessionExpiredEvent {
23
+ /** Why the session is considered expired. */
24
+ reason: SessionExpiredReason;
25
+ /** Original error that triggered the event, when one is available. */
26
+ cause?: unknown;
27
+ }
28
+ export interface SessionExpiredEmitter {
29
+ /**
30
+ * Notify every subscriber. Listener exceptions are swallowed so one bad
31
+ * listener can never break the auth flow nor the other listeners.
32
+ */
33
+ emit(event: SessionExpiredEvent): void;
34
+ /** Subscribe to session-expired events. Returns an unsubscribe function. */
35
+ subscribe(listener: (event: SessionExpiredEvent) => void): () => void;
36
+ }
37
+ export declare function createSessionExpiredEmitter(): SessionExpiredEmitter;
38
+ //# sourceMappingURL=session-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../../../src/core/auth/session-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,oBAAoB;AAC9B,yFAAyF;AACvF,qBAAqB;AACvB,0FAA0F;GACxF,yBAAyB;AAC3B,8EAA8E;GAC5E,uBAAuB,CAAC;AAE5B,MAAM,WAAW,mBAAmB;IAClC,6CAA6C;IAC7C,MAAM,EAAE,oBAAoB,CAAC;IAC7B,sEAAsE;IACtE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACvC,4EAA4E;IAC5E,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACvE;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAoBnE"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Session-expired event channel — auth-level pub/sub (0-deps core).
3
+ *
4
+ * Distinct from cart-recovery's `CartSessionExpiredEvent` (scoped to one cart
5
+ * operation): this fires when the CUSTOMER auth session can no longer be kept
6
+ * alive — the proactive scheduler's refresh failed on tab wake, or a reactive
7
+ * refresh after a 401 also failed. Storefronts subscribe once globally
8
+ * (`useSessionExpired`) and react (notify + redirect to sign-in).
9
+ *
10
+ * Mirrors the cart-recovery emitter shape (Set + emit-swallows-listener-errors
11
+ * + subscribe→unsubscribe). 0-deps (Inv-1): plain `Set` + closure, no React /
12
+ * DOM. The provider owns the instance (`useRef` + Context, Inv-3) — never a
13
+ * module-level singleton.
14
+ */
15
+ export function createSessionExpiredEmitter() {
16
+ const listeners = new Set();
17
+ return {
18
+ emit(event) {
19
+ for (const listener of listeners) {
20
+ try {
21
+ listener(event);
22
+ }
23
+ catch {
24
+ // Listeners must not break the auth flow — swallow listener exceptions.
25
+ }
26
+ }
27
+ },
28
+ subscribe(listener) {
29
+ listeners.add(listener);
30
+ return () => {
31
+ listeners.delete(listener);
32
+ };
33
+ },
34
+ };
35
+ }
@@ -193,6 +193,23 @@ export interface ExecuteWithCartRecoveryOptions<T> {
193
193
  * re-auth; the same cart resumes.
194
194
  */
195
195
  onSessionExpired?: (event: CartSessionExpiredEvent) => void;
196
+ /**
197
+ * Server-known cart-id seed used when the cookie store is empty. Cookie
198
+ * wins when present; the seed only fills the gap on first interaction.
199
+ *
200
+ * On Phase 0 the runner eagerly copies the seed into the cookie store so
201
+ * cross-tab tabs observe the same cart-id and recovery semantics match
202
+ * the standard cookie-driven path. If the seed is stale the recovery
203
+ * runner clears it via the usual Phase 2a/2b flow (bail with
204
+ * `cart-expired` for state-dependent ops, atomic recreate for replayable
205
+ * ops) — no special-case code needed at this layer.
206
+ *
207
+ * Use cases: env seed for dev sample storefronts (sample cart fixture
208
+ * without a product listing), magic-link checkout, embedded iframe
209
+ * (parent supplies cart-id via postMessage), customer-service "view this
210
+ * cart" UI, server-side recovery, multi-cart B2B selectors.
211
+ */
212
+ initialCartId?: string | null;
196
213
  }
197
214
  /**
198
215
  * Runs an operation against the current cart, recovering once on
@@ -233,6 +250,12 @@ export interface CreateCartRecoveryRunnerOptions {
233
250
  ensureCart?: () => Promise<Cart>;
234
251
  /** Cookie `maxAge` propagated to all `cookieStore.set` calls. */
235
252
  cookieMaxAge?: number;
253
+ /**
254
+ * Server-known cart-id seed — see
255
+ * `ExecuteWithCartRecoveryOptions.initialCartId`. Applied to every
256
+ * `execute` and `getCart` call on this runner instance.
257
+ */
258
+ initialCartId?: string | null;
236
259
  }
237
260
  /**
238
261
  * Build a per-shop-session runner that shares a recovery coordinator across
@@ -1 +1 @@
1
- {"version":3,"file":"cart-recovery.d.ts","sourceRoot":"","sources":["../../../src/core/cart/cart-recovery.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cart-recovery.d.ts","sourceRoot":"","sources":["../../../src/core/cart/cart-recovery.ts"],"names":[],"mappings":"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"}
@@ -156,11 +156,20 @@ async function acquireCartId(coord, factory) {
156
156
  * consumers can wire this directly without the runner factory.
157
157
  */
158
158
  export async function executeWithCartRecovery(opts) {
159
- const { cartClient, cookieStore, operation, ensureCart, cookieMaxAge, onExpired, onSessionExpired } = opts;
159
+ const { cartClient, cookieStore, operation, ensureCart, cookieMaxAge, onExpired, onSessionExpired, initialCartId, } = opts;
160
160
  const coord = opts.recoveryCoordinator ?? createCoordinator();
161
161
  const opName = operation.name ?? 'unknown';
162
162
  // Phase 0 — ensure cart exists (cookie may be empty on first interaction).
163
+ // Priority: cookie wins (cross-tab canonical state) → initialCartId seed
164
+ // (server-known via env / SSO / magic-link / iframe / customer service) →
165
+ // auto-create fallback through the coordinator. The seed is eagerly written
166
+ // to the cookie so subsequent reads see a canonical value and the recovery
167
+ // path (Phase 2a/2b) operates on a known cookie when the seed turns out stale.
163
168
  let cartId = cookieStore.get();
169
+ if (!cartId && initialCartId) {
170
+ cookieStore.set(initialCartId, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
171
+ cartId = initialCartId;
172
+ }
164
173
  if (!cartId) {
165
174
  cartId = await acquireCartId(coord, async () => {
166
175
  const created = ensureCart ? await ensureCart() : (await cartClient.create()).cart;
@@ -237,7 +246,7 @@ export async function executeWithCartRecovery(opts) {
237
246
  * concurrent operations and exposes an `onExpired` listener pattern.
238
247
  */
239
248
  export function createCartRecoveryRunner(options) {
240
- const { cartClient, cookieStore, ensureCart, cookieMaxAge } = options;
249
+ const { cartClient, cookieStore, ensureCart, cookieMaxAge, initialCartId } = options;
241
250
  const coordinator = createCoordinator();
242
251
  const expiredListeners = new Set();
243
252
  const sessionListeners = new Set();
@@ -271,6 +280,7 @@ export function createCartRecoveryRunner(options) {
271
280
  operation,
272
281
  ensureCart,
273
282
  cookieMaxAge,
283
+ initialCartId,
274
284
  recoveryCoordinator: coordinator,
275
285
  onExpired: emitExpired,
276
286
  onSessionExpired: emitSessionExpired,
@@ -278,7 +288,14 @@ export function createCartRecoveryRunner(options) {
278
288
  return executeWithCartRecovery(internalOpts);
279
289
  },
280
290
  async getCart() {
281
- const cartId = cookieStore.get();
291
+ // Symmetric with Phase 0 of `execute` — cookie wins, seed fills the gap
292
+ // and is eagerly promoted to the cookie so the next mutation picks it up
293
+ // from the canonical source without re-reading the seed.
294
+ let cartId = cookieStore.get();
295
+ if (!cartId && initialCartId) {
296
+ cookieStore.set(initialCartId, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
297
+ cartId = initialCartId;
298
+ }
282
299
  if (!cartId)
283
300
  return null;
284
301
  try {
@@ -93,7 +93,8 @@ export type PaymentSession = PaymentSessionFragment;
93
93
  export type DiscountValidationResult = CartValidateDiscountCodeQuery['cartValidateDiscountCode'];
94
94
  export type DiscountInfo = NonNullable<DiscountValidationResult['discount']>;
95
95
  export type DiscountValidationError = NonNullable<DiscountValidationResult['error']>;
96
- export type { CartCreateInput, CartLineInput, CartLineUpdateInput, CartBuyerIdentityInput, CartAddressInput, CartAttributeInput, CartCompleteInput, CartApplyGiftCardInput, CartRemoveGiftCardInput, CartSelectPaymentMethodInput, CartClearPaymentSelectionInput, CartSelectShippingMethodInput, CartSetBillingAddressInput, CartSetShippingAddressInput, CartUpdateGiftCardRecipientInput, PaymentCreateInput, ShippingAddressInput, PickupPointInput, DeliveryType, DeliveryEstimate, ShippingCarrier, FreeShippingProgress, PickupPoint, AttributeSelectionInput as CartAttributeSelectionInput, PaymentMethodType, PaymentInitiationFlow, DiscountApplicationType, DiscountErrorCode, CurrencyCode, CountryCode, LanguageCode, ProductTypeEnum, WeightUnit, CartWarningCode, AttributeType, AttributeFillingMode, AttributeBillingMode, AttributeOptionSurchargeType, StorefrontOrderStatus, OrderPaymentStatus, OrderFulfillmentStatus, } from '../generated/operation-types';
96
+ export type { CartCreateInput, CartLineInput, CartLineUpdateInput, CartBuyerIdentityInput, CartAddressInput, CartAttributeInput, CartCompleteInput, CartApplyGiftCardInput, CartRemoveGiftCardInput, CartSelectPaymentMethodInput, CartClearPaymentSelectionInput, CartSelectShippingMethodInput, CartSetBillingAddressInput, CartSetShippingAddressInput, CartUpdateGiftCardRecipientInput, PaymentCreateInput, ShippingAddressInput, PickupPointInput, DeliveryEstimate, ShippingCarrier, FreeShippingProgress, PickupPoint, AttributeSelectionInput as CartAttributeSelectionInput, } from '../generated/operation-types';
97
+ export { DeliveryType, PaymentMethodType, PaymentInitiationFlow, DiscountApplicationType, DiscountErrorCode, CurrencyCode, CountryCode, LanguageCode, ProductTypeEnum, WeightUnit, CartWarningCode, AttributeType, AttributeFillingMode, AttributeBillingMode, AttributeOptionSurchargeType, StorefrontOrderStatus, OrderPaymentStatus, OrderFulfillmentStatus, PaymentProvider, PaymentInstrumentType, PaymentInstrumentDisplayHint, PaymentMethodUnavailableReason, } from '../generated/operation-types';
97
98
  /**
98
99
  * Machine-readable error code surfaced when `createPayment` fails. The call
99
100
  * throws a `StorefrontError` on `userErrors` — read `.userErrors[0].code` off