@kotao/storefront 0.2.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -0
  3. package/dist/cart-documents-CwwRTvm8.d.ts +21 -0
  4. package/dist/cart-documents.d.ts +2 -0
  5. package/dist/cart-documents.js +3 -0
  6. package/dist/cart-documents.js.map +1 -0
  7. package/dist/chunk-2IWNUDSZ.js +104 -0
  8. package/dist/chunk-2IWNUDSZ.js.map +1 -0
  9. package/dist/chunk-3YQXJNR7.js +70 -0
  10. package/dist/chunk-3YQXJNR7.js.map +1 -0
  11. package/dist/chunk-5QOFKGKD.js +51 -0
  12. package/dist/chunk-5QOFKGKD.js.map +1 -0
  13. package/dist/chunk-GCQI6OKH.js +239 -0
  14. package/dist/chunk-GCQI6OKH.js.map +1 -0
  15. package/dist/chunk-ISAFEOR2.js +214 -0
  16. package/dist/chunk-ISAFEOR2.js.map +1 -0
  17. package/dist/chunk-MYDKIJRH.js +81 -0
  18. package/dist/chunk-MYDKIJRH.js.map +1 -0
  19. package/dist/chunk-OGWYTYYS.js +72 -0
  20. package/dist/chunk-OGWYTYYS.js.map +1 -0
  21. package/dist/chunk-PXZ737N3.js +439 -0
  22. package/dist/chunk-PXZ737N3.js.map +1 -0
  23. package/dist/chunk-QOEEUTHQ.js +110 -0
  24. package/dist/chunk-QOEEUTHQ.js.map +1 -0
  25. package/dist/chunk-U5677AEA.js +1690 -0
  26. package/dist/chunk-U5677AEA.js.map +1 -0
  27. package/dist/chunk-XMTWFJCZ.js +138 -0
  28. package/dist/chunk-XMTWFJCZ.js.map +1 -0
  29. package/dist/client-D9YYA2w6.d.ts +159 -0
  30. package/dist/customer-account-api-types.d.ts +144 -0
  31. package/dist/customer-account-api-types.js +3 -0
  32. package/dist/customer-account-api-types.js.map +1 -0
  33. package/dist/editor.d.ts +112 -0
  34. package/dist/editor.js +3 -0
  35. package/dist/editor.js.map +1 -0
  36. package/dist/image.d.ts +89 -0
  37. package/dist/image.js +3 -0
  38. package/dist/image.js.map +1 -0
  39. package/dist/index.d.ts +183 -0
  40. package/dist/index.js +208 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/pagination-CPgH3fAa.d.ts +77 -0
  43. package/dist/product.d.ts +105 -0
  44. package/dist/product.js +4 -0
  45. package/dist/product.js.map +1 -0
  46. package/dist/react-router.d.ts +29 -0
  47. package/dist/react-router.js +33 -0
  48. package/dist/react-router.js.map +1 -0
  49. package/dist/react.d.ts +299 -0
  50. package/dist/react.js +1813 -0
  51. package/dist/react.js.map +1 -0
  52. package/dist/routing-BfUp49o8.d.ts +302 -0
  53. package/dist/seo.d.ts +155 -0
  54. package/dist/seo.js +4 -0
  55. package/dist/seo.js.map +1 -0
  56. package/dist/settings.d.ts +87 -0
  57. package/dist/settings.js +3 -0
  58. package/dist/settings.js.map +1 -0
  59. package/dist/shop-DkQmro-Y.d.ts +202 -0
  60. package/dist/store-DQK1nhob.d.ts +67 -0
  61. package/dist/storefront-api-types.d.ts +312 -0
  62. package/dist/storefront-api-types.js +3 -0
  63. package/dist/storefront-api-types.js.map +1 -0
  64. package/dist/types-BDhHWZsP.d.ts +77 -0
  65. package/dist/types-Df3bGbKU.d.ts +167 -0
  66. package/package.json +82 -0
@@ -0,0 +1,1690 @@
1
+ import { CONSENT_BUCKETS, readConsentCookie, CART_FORM_ACTIONS } from './chunk-OGWYTYYS.js';
2
+ import { SHOP_HEADER, SHOP_SIGNATURE_HEADER, signShopContext } from '@kotao/storefront-contracts';
3
+
4
+ // src/cache/cache.ts
5
+ var CacheNone = () => ({ mode: "none", maxAge: 0, staleWhileRevalidate: 0 });
6
+ var CacheShort = (o) => ({
7
+ mode: "short",
8
+ maxAge: o?.maxAge ?? 1,
9
+ staleWhileRevalidate: o?.staleWhileRevalidate ?? 9
10
+ });
11
+ var CacheLong = (o) => ({
12
+ mode: "long",
13
+ maxAge: o?.maxAge ?? 3600,
14
+ staleWhileRevalidate: o?.staleWhileRevalidate ?? 82800
15
+ });
16
+ var CacheCustom = (s) => ({
17
+ mode: "custom",
18
+ ...s
19
+ });
20
+ var StorefrontDefaultCache = () => ({
21
+ mode: "custom",
22
+ maxAge: 1,
23
+ staleWhileRevalidate: 86399
24
+ });
25
+ function generateCacheControlHeader(strategy) {
26
+ if (strategy.mode === "none") return "no-store";
27
+ return `public, max-age=${String(strategy.maxAge)}, stale-while-revalidate=${String(strategy.staleWhileRevalidate)}`;
28
+ }
29
+ var requestFor = (key) => new Request(`https://kotao.cache/${encodeURIComponent(key)}`);
30
+ function createCache(opts) {
31
+ const now = opts.now ?? (() => Date.now());
32
+ const cache = opts.cache;
33
+ const read = async (key) => {
34
+ const res = await cache.match(requestFor(key));
35
+ if (!res) return null;
36
+ try {
37
+ return await res.json();
38
+ } catch {
39
+ return null;
40
+ }
41
+ };
42
+ const write = async (key, value, strategy) => {
43
+ const body = { value, storedAt: now() };
44
+ const ttl = strategy.maxAge + strategy.staleWhileRevalidate;
45
+ await cache.put(
46
+ requestFor(key),
47
+ new Response(JSON.stringify(body), {
48
+ headers: { "content-type": "application/json", "cache-control": `max-age=${String(ttl)}` }
49
+ })
50
+ );
51
+ };
52
+ return {
53
+ raw: cache,
54
+ async run(key, strategy, fn) {
55
+ if (strategy.mode === "none") return fn();
56
+ const stored = await read(key);
57
+ if (stored) {
58
+ const ageSec = (now() - stored.storedAt) / 1e3;
59
+ if (ageSec <= strategy.maxAge) return stored.value;
60
+ if (ageSec <= strategy.maxAge + strategy.staleWhileRevalidate) {
61
+ opts.waitUntil(
62
+ (async () => {
63
+ const fresh = await fn();
64
+ await write(key, fresh, strategy);
65
+ })()
66
+ );
67
+ return stored.value;
68
+ }
69
+ }
70
+ const value = await fn();
71
+ opts.waitUntil(write(key, value, strategy));
72
+ return value;
73
+ }
74
+ };
75
+ }
76
+
77
+ // src/cart/cookie.ts
78
+ var CART_ID_COOKIE = "kotao_cart";
79
+ var CART_MAX_AGE = 60 * 60 * 24 * 30;
80
+ function readCartIdCookie(cookieHeader) {
81
+ if (cookieHeader == null || cookieHeader === "") return null;
82
+ const match = cookieHeader.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${CART_ID_COOKIE}=`));
83
+ if (match === void 0) return null;
84
+ const value = decodeURIComponent(match.slice(CART_ID_COOKIE.length + 1));
85
+ return value === "" ? null : value;
86
+ }
87
+ function buildCartIdCookie(cartId) {
88
+ return `${CART_ID_COOKIE}=${encodeURIComponent(cartId)}; Path=/; Max-Age=${CART_MAX_AGE}; SameSite=Lax; Secure; HttpOnly`;
89
+ }
90
+
91
+ // src/checkout/consent.ts
92
+ var PARAM_PREFIX = "consent_";
93
+ var VALID_PARAM_VALUES = /* @__PURE__ */ new Set(["0", "1"]);
94
+ var CHECKOUT_CONSENT_PARAM_PREFIX = PARAM_PREFIX;
95
+ var CHECKOUT_CONSENT_CATEGORIES = CONSENT_BUCKETS;
96
+ function encodeCheckoutConsent(state) {
97
+ assertConsentState(state);
98
+ const params = new URLSearchParams();
99
+ for (const bucket of CHECKOUT_CONSENT_CATEGORIES) {
100
+ params.set(paramName(bucket), state[bucket] ? "1" : "0");
101
+ }
102
+ return params;
103
+ }
104
+ function decodeCheckoutConsent(input) {
105
+ const params = input instanceof URL ? input.searchParams : typeof input === "string" ? new URLSearchParams(input) : input;
106
+ const decoded = {};
107
+ for (const bucket of CHECKOUT_CONSENT_CATEGORIES) {
108
+ const key = paramName(bucket);
109
+ const value = params.get(key);
110
+ if (value === null) throw new Error(`Missing checkout consent param: ${key}`);
111
+ if (!VALID_PARAM_VALUES.has(value)) {
112
+ throw new Error(`Invalid checkout consent param ${key}: expected 0 or 1`);
113
+ }
114
+ decoded[bucket] = value === "1";
115
+ }
116
+ return decoded;
117
+ }
118
+ function paramName(bucket) {
119
+ return `${PARAM_PREFIX}${bucket}`;
120
+ }
121
+ function assertConsentState(state) {
122
+ for (const bucket of CHECKOUT_CONSENT_CATEGORIES) {
123
+ if (typeof state[bucket] !== "boolean") {
124
+ throw new Error(`Invalid checkout consent state: ${bucket} must be boolean`);
125
+ }
126
+ }
127
+ }
128
+
129
+ // src/checkout/checkout-url.ts
130
+ function resolveCheckoutUrl(options) {
131
+ const origin = resolveShopOrigin(options);
132
+ const rawCheckoutUrl = options.checkoutUrl.trim();
133
+ const url = parseCheckoutUrl(options.checkoutUrl, origin);
134
+ if (url.origin !== origin) {
135
+ if (!isExternalHttpsCheckoutUrl(rawCheckoutUrl, url)) {
136
+ throw new Error(`checkoutUrl must stay on the same origin as the shop domain (${origin})`);
137
+ }
138
+ appendConsent(url, options.consent);
139
+ assertNoZoneIdentityLeak(url);
140
+ return url.toString();
141
+ }
142
+ if (!isSameOriginCheckoutPath(url)) {
143
+ throw new Error(`checkoutUrl must use the Gateway-owned /checkout/* carve-out`);
144
+ }
145
+ appendConsent(url, options.consent);
146
+ assertNoZoneIdentityLeak(url);
147
+ return url.toString();
148
+ }
149
+ function resolveShopOrigin(options) {
150
+ if (options.shopDomain !== void 0) return originFromDomain(options.shopDomain);
151
+ if (options.requestUrl !== void 0) return originFromRequestUrl(options.requestUrl);
152
+ throw new Error("checkoutUrl resolution requires shopDomain or requestUrl");
153
+ }
154
+ function originFromDomain(shopDomain) {
155
+ const trimmed = shopDomain.trim();
156
+ if (trimmed === "") throw new Error("shopDomain must not be empty");
157
+ const url = new URL(/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`);
158
+ return url.origin;
159
+ }
160
+ function originFromRequestUrl(requestUrl) {
161
+ if (requestUrl instanceof Request) return new URL(requestUrl.url).origin;
162
+ return new URL(requestUrl.toString()).origin;
163
+ }
164
+ function parseCheckoutUrl(checkoutUrl, origin) {
165
+ if (checkoutUrl.trim() === "") throw new Error("checkoutUrl must not be empty");
166
+ if (checkoutUrl.startsWith("//")) {
167
+ throw new Error("checkoutUrl must stay on the same origin as the shop domain");
168
+ }
169
+ return new URL(checkoutUrl, origin);
170
+ }
171
+ function isSameOriginCheckoutPath(url) {
172
+ return url.pathname.startsWith("/checkout/");
173
+ }
174
+ function isExternalHttpsCheckoutUrl(rawCheckoutUrl, url) {
175
+ return /^https:\/\//i.test(rawCheckoutUrl) && (/^\/checkouts?\//u.test(url.pathname) || /^\/cart\/c\//u.test(url.pathname));
176
+ }
177
+ function appendConsent(url, consent) {
178
+ for (const [key, value] of encodeCheckoutConsent(consent)) {
179
+ url.searchParams.set(key, value);
180
+ }
181
+ }
182
+ function assertNoZoneIdentityLeak(url) {
183
+ for (const [key, value] of url.searchParams) {
184
+ const pair = `${key}=${value}`.toLowerCase();
185
+ if (pair.includes(".kotao.com") || pair.includes("domain=kotao.com")) {
186
+ throw new Error("checkout handoff URL must not carry zone-wide Kotao identity");
187
+ }
188
+ }
189
+ }
190
+
191
+ // src/checkout/design-tokens.ts
192
+ var CHECKOUT_DESIGN_TOKEN_INPUTS = [
193
+ {
194
+ type: "url",
195
+ name: "checkoutLogoUrl",
196
+ label: "Checkout logo URL"
197
+ },
198
+ {
199
+ type: "text",
200
+ name: "checkoutLogoAlt",
201
+ label: "Checkout logo alt text",
202
+ defaultValue: ""
203
+ },
204
+ {
205
+ type: "color",
206
+ name: "checkoutColorAccent",
207
+ label: "Checkout accent color",
208
+ defaultValue: "#005bd3"
209
+ },
210
+ {
211
+ type: "color",
212
+ name: "checkoutColorBackground",
213
+ label: "Checkout background color",
214
+ defaultValue: "#ffffff"
215
+ },
216
+ {
217
+ type: "color",
218
+ name: "checkoutColorForeground",
219
+ label: "Checkout foreground color",
220
+ defaultValue: "#111827"
221
+ },
222
+ {
223
+ type: "font",
224
+ name: "checkoutFontBody",
225
+ label: "Checkout body font",
226
+ defaultValue: "Inter"
227
+ },
228
+ {
229
+ type: "font",
230
+ name: "checkoutFontHeading",
231
+ label: "Checkout heading font",
232
+ defaultValue: "Inter"
233
+ }
234
+ ];
235
+ function extractCheckoutDesignTokens(snapshot) {
236
+ const global = snapshot.global;
237
+ const logoUrl = optionalString(global["checkoutLogoUrl"], "checkoutLogoUrl");
238
+ const logoAlt = optionalString(global["checkoutLogoAlt"], "checkoutLogoAlt");
239
+ return {
240
+ logo: logoUrl === null ? null : { src: logoUrl, altText: logoAlt },
241
+ colors: {
242
+ accent: requiredString(global["checkoutColorAccent"], "checkoutColorAccent"),
243
+ background: requiredString(global["checkoutColorBackground"], "checkoutColorBackground"),
244
+ foreground: requiredString(global["checkoutColorForeground"], "checkoutColorForeground")
245
+ },
246
+ fonts: {
247
+ body: requiredString(global["checkoutFontBody"], "checkoutFontBody"),
248
+ heading: requiredString(global["checkoutFontHeading"], "checkoutFontHeading")
249
+ }
250
+ };
251
+ }
252
+ function optionalString(value, key) {
253
+ if (value === void 0 || value === null || value === "") return null;
254
+ if (typeof value !== "string") throw new Error(`${key} must be a string`);
255
+ const trimmed = value.trim();
256
+ if (trimmed === "") return null;
257
+ return trimmed;
258
+ }
259
+ function requiredString(value, key) {
260
+ if (typeof value !== "string" || value.trim() === "") {
261
+ throw new Error(`${key} must be a non-empty string`);
262
+ }
263
+ return value.trim();
264
+ }
265
+
266
+ // src/cart/queries.ts
267
+ var CART_FRAGMENT = (
268
+ /* GraphQL */
269
+ `
270
+ fragment KotaoCart on Cart {
271
+ id
272
+ checkoutUrl
273
+ totalQuantity
274
+ cost {
275
+ subtotalAmount {
276
+ amount
277
+ currencyCode
278
+ }
279
+ totalAmount {
280
+ amount
281
+ currencyCode
282
+ }
283
+ totalTaxAmount {
284
+ amount
285
+ currencyCode
286
+ }
287
+ }
288
+ buyerIdentity {
289
+ email
290
+ phone
291
+ countryCode
292
+ }
293
+ note
294
+ attributes {
295
+ key
296
+ value
297
+ }
298
+ discountCodes
299
+ lines(first: 250) {
300
+ nodes {
301
+ id
302
+ quantity
303
+ attributes {
304
+ key
305
+ value
306
+ }
307
+ merchandise {
308
+ ... on ProductVariant {
309
+ id
310
+ title
311
+ availableForSale
312
+ image {
313
+ url
314
+ altText
315
+ width
316
+ height
317
+ }
318
+ price {
319
+ amount
320
+ currencyCode
321
+ }
322
+ compareAtPrice {
323
+ amount
324
+ currencyCode
325
+ }
326
+ selectedOptions {
327
+ name
328
+ value
329
+ }
330
+ product {
331
+ handle
332
+ title
333
+ }
334
+ }
335
+ }
336
+ cost {
337
+ amountPerQuantity {
338
+ amount
339
+ currencyCode
340
+ }
341
+ subtotalAmount {
342
+ amount
343
+ currencyCode
344
+ }
345
+ totalAmount {
346
+ amount
347
+ currencyCode
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ `
354
+ );
355
+ var CART_QUERY = (
356
+ /* GraphQL */
357
+ `
358
+ ${CART_FRAGMENT}
359
+ query KotaoCart($cartId: ID!) {
360
+ cart(id: $cartId) {
361
+ ...KotaoCart
362
+ }
363
+ }
364
+ `
365
+ );
366
+ var CART_CREATE_MUTATION = (
367
+ /* GraphQL */
368
+ `
369
+ ${CART_FRAGMENT}
370
+ mutation KotaoCartCreate($input: CartInput) {
371
+ cartCreate(input: $input) {
372
+ cart {
373
+ ...KotaoCart
374
+ }
375
+ userErrors {
376
+ field
377
+ message
378
+ code
379
+ }
380
+ }
381
+ }
382
+ `
383
+ );
384
+ var CART_LINES_ADD_MUTATION = (
385
+ /* GraphQL */
386
+ `
387
+ ${CART_FRAGMENT}
388
+ mutation KotaoCartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
389
+ cartLinesAdd(cartId: $cartId, lines: $lines) {
390
+ cart {
391
+ ...KotaoCart
392
+ }
393
+ userErrors {
394
+ field
395
+ message
396
+ code
397
+ }
398
+ }
399
+ }
400
+ `
401
+ );
402
+ var CART_LINES_UPDATE_MUTATION = (
403
+ /* GraphQL */
404
+ `
405
+ ${CART_FRAGMENT}
406
+ mutation KotaoCartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
407
+ cartLinesUpdate(cartId: $cartId, lines: $lines) {
408
+ cart {
409
+ ...KotaoCart
410
+ }
411
+ userErrors {
412
+ field
413
+ message
414
+ code
415
+ }
416
+ }
417
+ }
418
+ `
419
+ );
420
+ var CART_LINES_REMOVE_MUTATION = (
421
+ /* GraphQL */
422
+ `
423
+ ${CART_FRAGMENT}
424
+ mutation KotaoCartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {
425
+ cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
426
+ cart {
427
+ ...KotaoCart
428
+ }
429
+ userErrors {
430
+ field
431
+ message
432
+ code
433
+ }
434
+ }
435
+ }
436
+ `
437
+ );
438
+ var CART_BUYER_IDENTITY_UPDATE_MUTATION = (
439
+ /* GraphQL */
440
+ `
441
+ ${CART_FRAGMENT}
442
+ mutation KotaoCartBuyerIdentityUpdate($cartId: ID!, $buyerIdentity: CartBuyerIdentityInput!) {
443
+ cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
444
+ cart {
445
+ ...KotaoCart
446
+ }
447
+ userErrors {
448
+ field
449
+ message
450
+ code
451
+ }
452
+ }
453
+ }
454
+ `
455
+ );
456
+ var mutation = (operationName, fieldName, args, variables) => (
457
+ /* GraphQL */
458
+ `
459
+ ${CART_FRAGMENT}
460
+ mutation ${operationName}($cartId: ID!, ${variables}) {
461
+ ${fieldName}(cartId: $cartId, ${args}) {
462
+ cart {
463
+ ...KotaoCart
464
+ }
465
+ userErrors {
466
+ field
467
+ message
468
+ code
469
+ }
470
+ }
471
+ }
472
+ `
473
+ );
474
+ var CART_DISCOUNT_CODES_UPDATE_MUTATION = mutation(
475
+ "CartDiscountCodesUpdate",
476
+ "cartDiscountCodesUpdate",
477
+ "discountCodes: $discountCodes",
478
+ "$discountCodes: [String!]!"
479
+ );
480
+ var CART_GIFT_CARD_CODES_ADD_MUTATION = mutation(
481
+ "CartGiftCardCodesAdd",
482
+ "cartGiftCardCodesAdd",
483
+ "giftCardCodes: $giftCardCodes",
484
+ "$giftCardCodes: [String!]!"
485
+ );
486
+ var CART_GIFT_CARD_CODES_REMOVE_MUTATION = mutation(
487
+ "CartGiftCardCodesRemove",
488
+ "cartGiftCardCodesRemove",
489
+ "giftCardCodes: $giftCardCodes",
490
+ "$giftCardCodes: [String!]!"
491
+ );
492
+ var CART_NOTE_UPDATE_MUTATION = mutation(
493
+ "CartNoteUpdate",
494
+ "cartNoteUpdate",
495
+ "note: $note",
496
+ "$note: String"
497
+ );
498
+ var CART_ATTRIBUTES_UPDATE_MUTATION = mutation(
499
+ "CartAttributesUpdate",
500
+ "cartAttributesUpdate",
501
+ "attributes: $attributes",
502
+ "$attributes: [AttributeInput!]!"
503
+ );
504
+ var CART_METAFIELDS_SET_MUTATION = mutation(
505
+ "CartMetafieldsSet",
506
+ "cartMetafieldsSet",
507
+ "metafields: $metafields",
508
+ "$metafields: [CartMetafieldInput!]!"
509
+ );
510
+ var CART_METAFIELDS_DELETE_MUTATION = mutation(
511
+ "CartMetafieldsDelete",
512
+ "cartMetafieldsDelete",
513
+ "metafields: $metafields",
514
+ "$metafields: [CartMetafieldIdentifierInput!]!"
515
+ );
516
+ var CART_DELIVERY_ADDRESSES_REPLACE_MUTATION = mutation(
517
+ "CartDeliveryAddressesReplace",
518
+ "cartDeliveryAddressesReplace",
519
+ "addresses: $addresses",
520
+ "$addresses: [CartDeliveryAddressInput!]!"
521
+ );
522
+ var CART_SELECTED_DELIVERY_OPTIONS_UPDATE_MUTATION = mutation(
523
+ "CartSelectedDeliveryOptionsUpdate",
524
+ "cartSelectedDeliveryOptionsUpdate",
525
+ "selectedDeliveryOptions: $selectedDeliveryOptions",
526
+ "$selectedDeliveryOptions: [CartSelectedDeliveryOptionInput!]!"
527
+ );
528
+
529
+ // src/cart/createCartHandler.ts
530
+ function cartGetIdDefault(headers) {
531
+ return () => readCartIdCookie(headers.get("cookie"));
532
+ }
533
+ function cartSetIdDefault() {
534
+ return (cartId) => buildCartIdCookie(cartId);
535
+ }
536
+ function createCartHandler(opts) {
537
+ const request = "request" in opts ? opts.request : void 0;
538
+ let getCartId;
539
+ let setCartId;
540
+ let cartId;
541
+ if ("request" in opts) {
542
+ getCartId = () => readCartIdCookie(opts.request.headers.get("cookie"));
543
+ setCartId = (nextCartId) => buildCartIdCookie(nextCartId);
544
+ cartId = readCartIdCookie(opts.request.headers.get("cookie"));
545
+ } else {
546
+ getCartId = opts.getCartId;
547
+ setCartId = opts.setCartId;
548
+ cartId = void 0;
549
+ }
550
+ const consent = readConsentCookie(request?.headers.get("cookie"));
551
+ const currentCartId = async () => {
552
+ if (cartId !== void 0) return cartId;
553
+ cartId = await getCartId() ?? null;
554
+ return cartId;
555
+ };
556
+ const withCheckoutUrl = (cart) => {
557
+ if (cart === null) return null;
558
+ if (request === void 0) return cart;
559
+ return {
560
+ ...cart,
561
+ checkoutUrl: resolveCheckoutUrl({
562
+ checkoutUrl: cart.checkoutUrl,
563
+ requestUrl: request,
564
+ consent
565
+ })
566
+ };
567
+ };
568
+ const withCookie = async (payload) => {
569
+ const headers = new Headers();
570
+ if (payload.cart?.id) {
571
+ cartId = payload.cart.id;
572
+ appendSetCartIdHeaders(headers, await setCartId(payload.cart.id));
573
+ }
574
+ return { ...payload, cart: withCheckoutUrl(payload.cart), headers };
575
+ };
576
+ const setCartIdSync = (nextCartId) => {
577
+ cartId = nextCartId;
578
+ const headers = new Headers();
579
+ const value = setCartId(nextCartId);
580
+ if (isPromiseLike(value)) {
581
+ throw new Error("Hydrogen cart.setCartId requires a synchronous setCartId implementation");
582
+ }
583
+ appendSetCartIdHeaders(headers, value);
584
+ return headers;
585
+ };
586
+ const create = async (input = {}) => {
587
+ const data = await opts.storefront.mutate(
588
+ CART_CREATE_MUTATION,
589
+ { variables: { input } }
590
+ );
591
+ return withCookie(data.cartCreate);
592
+ };
593
+ const ensureCartId = async () => {
594
+ const id = await currentCartId();
595
+ if (id) return id;
596
+ const created = await create();
597
+ if (!created.cart) throw new Error("cartCreate returned no cart");
598
+ return created.cart.id;
599
+ };
600
+ const mutateExisting = async (document, key, variables) => {
601
+ const id = await ensureCartId();
602
+ const data = await opts.storefront.mutate(document, {
603
+ variables: { cartId: id, ...variables }
604
+ });
605
+ return withCookie(data[key]);
606
+ };
607
+ const handler = {
608
+ async get() {
609
+ const id = await currentCartId();
610
+ if (!id) return null;
611
+ const data = await opts.storefront.query(CART_QUERY, {
612
+ variables: { cartId: id }
613
+ });
614
+ return withCheckoutUrl(data.cart);
615
+ },
616
+ getCart() {
617
+ return handler.get();
618
+ },
619
+ create,
620
+ addLines: (lines) => handler.linesAdd(lines),
621
+ updateLines: (lines) => handler.linesUpdate(lines),
622
+ removeLines: (lineIds) => handler.linesRemove(lineIds),
623
+ updateBuyerIdentity: (buyerIdentity) => handler.buyerIdentityUpdate(buyerIdentity),
624
+ updateDiscountCodes: (discountCodes) => handler.discountCodesUpdate(discountCodes),
625
+ setCartId: setCartIdSync,
626
+ cartCreate: create,
627
+ async linesAdd(lines) {
628
+ if (!await currentCartId()) return create({ lines });
629
+ return mutateExisting(CART_LINES_ADD_MUTATION, "cartLinesAdd", { lines });
630
+ },
631
+ linesUpdate: (lines) => mutateExisting(CART_LINES_UPDATE_MUTATION, "cartLinesUpdate", { lines }),
632
+ linesRemove: (lineIds) => mutateExisting(CART_LINES_REMOVE_MUTATION, "cartLinesRemove", { lineIds }),
633
+ buyerIdentityUpdate: (buyerIdentity) => mutateExisting(CART_BUYER_IDENTITY_UPDATE_MUTATION, "cartBuyerIdentityUpdate", {
634
+ buyerIdentity
635
+ }),
636
+ discountCodesUpdate: (discountCodes) => mutateExisting(CART_DISCOUNT_CODES_UPDATE_MUTATION, "cartDiscountCodesUpdate", {
637
+ discountCodes
638
+ }),
639
+ giftCardCodesAdd: (giftCardCodes) => mutateExisting(CART_GIFT_CARD_CODES_ADD_MUTATION, "cartGiftCardCodesAdd", {
640
+ giftCardCodes
641
+ }),
642
+ giftCardCodesRemove: (giftCardCodes) => mutateExisting(CART_GIFT_CARD_CODES_REMOVE_MUTATION, "cartGiftCardCodesRemove", {
643
+ giftCardCodes
644
+ }),
645
+ noteUpdate: (note) => mutateExisting(CART_NOTE_UPDATE_MUTATION, "cartNoteUpdate", { note }),
646
+ attributesUpdate: (attributes) => mutateExisting(CART_ATTRIBUTES_UPDATE_MUTATION, "cartAttributesUpdate", { attributes }),
647
+ metafieldsSet: (metafields) => mutateExisting(CART_METAFIELDS_SET_MUTATION, "cartMetafieldsSet", { metafields }),
648
+ metafieldsDelete: (metafields) => mutateExisting(CART_METAFIELDS_DELETE_MUTATION, "cartMetafieldsDelete", { metafields }),
649
+ deliveryAddressesReplace: (addresses) => mutateExisting(CART_DELIVERY_ADDRESSES_REPLACE_MUTATION, "cartDeliveryAddressesReplace", {
650
+ addresses
651
+ }),
652
+ selectedDeliveryOptionsUpdate: (selectedDeliveryOptions) => mutateExisting(
653
+ CART_SELECTED_DELIVERY_OPTIONS_UPDATE_MUTATION,
654
+ "cartSelectedDeliveryOptionsUpdate",
655
+ { selectedDeliveryOptions }
656
+ ),
657
+ dispatch(action) {
658
+ switch (action.action) {
659
+ case CART_FORM_ACTIONS.LinesAdd:
660
+ return handler.linesAdd(action.inputs.lines);
661
+ case CART_FORM_ACTIONS.LinesUpdate:
662
+ return handler.linesUpdate(action.inputs.lines);
663
+ case CART_FORM_ACTIONS.LinesRemove:
664
+ return handler.linesRemove(action.inputs.lineIds);
665
+ case CART_FORM_ACTIONS.DiscountCodesUpdate:
666
+ return handler.discountCodesUpdate(action.inputs.discountCodes);
667
+ case CART_FORM_ACTIONS.GiftCardCodesAdd:
668
+ return handler.giftCardCodesAdd(action.inputs.giftCardCodes);
669
+ case CART_FORM_ACTIONS.GiftCardCodesRemove:
670
+ return handler.giftCardCodesRemove(action.inputs.giftCardCodes);
671
+ case CART_FORM_ACTIONS.BuyerIdentityUpdate:
672
+ return handler.buyerIdentityUpdate(action.inputs.buyerIdentity);
673
+ case CART_FORM_ACTIONS.NoteUpdate:
674
+ return handler.noteUpdate(action.inputs.note ?? null);
675
+ case CART_FORM_ACTIONS.AttributesUpdate:
676
+ return handler.attributesUpdate(action.inputs.attributes);
677
+ case CART_FORM_ACTIONS.MetafieldsSet:
678
+ return handler.metafieldsSet(action.inputs.metafields);
679
+ case CART_FORM_ACTIONS.MetafieldsDelete:
680
+ return handler.metafieldsDelete(action.inputs.metafields);
681
+ case CART_FORM_ACTIONS.DeliveryAddressesReplace:
682
+ return handler.deliveryAddressesReplace(action.inputs.addresses);
683
+ case CART_FORM_ACTIONS.SelectedDeliveryOptionsUpdate:
684
+ return handler.selectedDeliveryOptionsUpdate(
685
+ action.inputs.selectedDeliveryOptions
686
+ );
687
+ }
688
+ }
689
+ };
690
+ return handler;
691
+ }
692
+ function isPromiseLike(value) {
693
+ return value !== null && typeof value === "object" && typeof value.then === "function";
694
+ }
695
+ function appendSetCartIdHeaders(headers, value) {
696
+ if (typeof value === "string" && value.length > 0) {
697
+ headers.append("set-cookie", value);
698
+ return;
699
+ }
700
+ if (value instanceof Headers) {
701
+ value.forEach((headerValue, headerName) => {
702
+ headers.append(headerName, headerValue);
703
+ });
704
+ }
705
+ }
706
+
707
+ // src/customer-account/session.ts
708
+ var CUSTOMER_SESSION_COOKIE = "kotao_customer_session";
709
+ var DEFAULT_SESSION_MAX_AGE = 60 * 60 * 24 * 30;
710
+ var assertHostname = (hostname) => {
711
+ if (hostname === "" || hostname.startsWith(".") || hostname.endsWith(".")) {
712
+ throw new Error(`invalid shop hostname for the customer session cookie: '${hostname}'`);
713
+ }
714
+ };
715
+ function shopHostname(request) {
716
+ return new URL(request.url).hostname;
717
+ }
718
+ function buildCustomerSessionCookie(token, attrs) {
719
+ assertHostname(attrs.hostname);
720
+ const maxAge = attrs.maxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE;
721
+ return `${CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(token)}; Domain=${attrs.hostname}; Path=/; Max-Age=${maxAge}; HttpOnly; Secure; SameSite=Lax`;
722
+ }
723
+ function clearCustomerSessionCookie(attrs) {
724
+ assertHostname(attrs.hostname);
725
+ return `${CUSTOMER_SESSION_COOKIE}=; Domain=${attrs.hostname}; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`;
726
+ }
727
+ function readCustomerSessionToken(cookieHeader) {
728
+ if (cookieHeader === null || cookieHeader === void 0 || cookieHeader === "") return null;
729
+ const match = cookieHeader.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${CUSTOMER_SESSION_COOKIE}=`));
730
+ if (match === void 0) return null;
731
+ const raw = match.slice(CUSTOMER_SESSION_COOKIE.length + 1);
732
+ if (raw === "") return null;
733
+ try {
734
+ return decodeURIComponent(raw);
735
+ } catch {
736
+ return null;
737
+ }
738
+ }
739
+
740
+ // src/customer-account/types.ts
741
+ var CustomerAccountError = class extends Error {
742
+ code;
743
+ constructor(code, message) {
744
+ super(message);
745
+ this.name = "CustomerAccountError";
746
+ this.code = code;
747
+ }
748
+ };
749
+
750
+ // src/customer-account/client.ts
751
+ var DEFAULT_PROXY_BASE = "/api/storefront";
752
+ var DEFAULT_CUSTOMER_API_VERSION = "2026-04";
753
+ var CUSTOMER_ACCOUNT_SCOPE = "openid email customer-account-api:full";
754
+ var HYDROGEN_CUSTOMER_ACCOUNT_SESSION_KEY = "customerAccount";
755
+ var SAME_ORIGIN_PATH = /^\/(?![/\\])/;
756
+ function createCustomerAccountClient(opts) {
757
+ const proxyBase = opts.proxyBase ?? DEFAULT_PROXY_BASE;
758
+ if (!SAME_ORIGIN_PATH.test(proxyBase) || proxyBase.includes("..")) {
759
+ throw new Error(`customer-account proxyBase must be a same-origin path, got '${proxyBase}'`);
760
+ }
761
+ const doFetch = opts.fetch ?? fetch;
762
+ const cookieHeader = opts.request.headers.get("cookie");
763
+ const token = readCustomerSessionToken(cookieHeader);
764
+ const hydrogenCustomerAccountId = opts.customerAccountId?.trim();
765
+ const hydrogenShopId = opts.shopId?.trim();
766
+ const hasHydrogenCustomerAccount = hydrogenCustomerAccountId !== void 0 && hydrogenCustomerAccountId.length > 0 && hydrogenShopId !== void 0 && hydrogenShopId.length > 0;
767
+ const hydrogenSession = isHydrogenSessionLike(opts.session) ? opts.session : void 0;
768
+ const proxyGet = async (path) => {
769
+ try {
770
+ return await doFetch(`${proxyBase}${path}`, {
771
+ headers: token !== null ? { cookie: `${CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(token)}` } : {}
772
+ });
773
+ } catch (cause) {
774
+ throw new CustomerAccountError(
775
+ "unreachable",
776
+ `customer-account proxy unreachable: ${cause instanceof Error ? cause.message : String(cause)}`
777
+ );
778
+ }
779
+ };
780
+ const readJson = async (res, what) => {
781
+ if (res.status === 401) {
782
+ throw new CustomerAccountError("unauthenticated", `${what}: not logged in`);
783
+ }
784
+ if (!res.ok) {
785
+ throw new CustomerAccountError("api_error", `${what} failed: HTTP ${res.status}`);
786
+ }
787
+ try {
788
+ return await res.json();
789
+ } catch {
790
+ throw new CustomerAccountError("api_error", `${what}: malformed JSON in proxy response`);
791
+ }
792
+ };
793
+ const proxyPost = async (path, body) => {
794
+ return doFetch(`${proxyBase}${path}`, {
795
+ method: "POST",
796
+ headers: { "content-type": "application/json" },
797
+ body: JSON.stringify(body)
798
+ });
799
+ };
800
+ const proxyWrite = async (path, method, body) => {
801
+ const headers = { "content-type": "application/json" };
802
+ if (token !== null) headers.cookie = `${CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(token)}`;
803
+ return doFetch(`${proxyBase}${path}`, {
804
+ method,
805
+ headers,
806
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
807
+ });
808
+ };
809
+ const returnTo = () => {
810
+ const url = new URL(opts.request.url);
811
+ return `${url.pathname}${url.search}`;
812
+ };
813
+ const requestHttpsOrigin = () => {
814
+ const url = new URL(opts.request.url);
815
+ return url.protocol === "http:" ? url.origin.replace(/^http:/, "https:") : url.origin;
816
+ };
817
+ const redirectResponse = (location) => new Response(null, { status: 302, headers: { location } });
818
+ const commitHydrogenSession = async (headers) => {
819
+ const cookie = await hydrogenSession?.commit?.();
820
+ if (cookie !== void 0 && cookie.length > 0) headers.append("set-cookie", cookie);
821
+ };
822
+ const redirectWithHydrogenSession = async (location) => {
823
+ const headers = new Headers({ location });
824
+ await commitHydrogenSession(headers);
825
+ return new Response(null, { status: 302, headers });
826
+ };
827
+ const readHydrogenCustomerSession = () => {
828
+ const value = hydrogenSession?.get(HYDROGEN_CUSTOMER_ACCOUNT_SESSION_KEY);
829
+ if (value === null || value === void 0 || typeof value !== "object") return null;
830
+ return value;
831
+ };
832
+ const setHydrogenCustomerSession = (value) => {
833
+ hydrogenSession?.set(HYDROGEN_CUSTOMER_ACCOUNT_SESSION_KEY, value);
834
+ };
835
+ const clearHydrogenCustomerSession = () => {
836
+ if (hydrogenSession?.unset !== void 0) {
837
+ hydrogenSession.unset(HYDROGEN_CUSTOMER_ACCOUNT_SESSION_KEY);
838
+ return;
839
+ }
840
+ hydrogenSession?.set(HYDROGEN_CUSTOMER_ACCOUNT_SESSION_KEY, void 0);
841
+ };
842
+ const hydrogenTokenEndpoint = () => `https://shopify.com/authentication/${hydrogenShopId}/oauth/token`;
843
+ const setHydrogenCustomerTokens = (tokenResponse, previous) => {
844
+ const refreshToken = tokenResponse.refresh_token ?? previous?.refreshToken;
845
+ const idToken = tokenResponse.id_token ?? previous?.idToken;
846
+ const nextSession = {
847
+ accessToken: tokenResponse.access_token,
848
+ expiresAt: String(Date.now() + Math.max(tokenResponse.expires_in - 120, 0) * 1e3),
849
+ ...refreshToken !== void 0 ? { refreshToken } : {},
850
+ ...idToken !== void 0 ? { idToken } : {}
851
+ };
852
+ setHydrogenCustomerSession(nextSession);
853
+ };
854
+ const refreshHydrogenAccessToken = async (sessionValue) => {
855
+ const refreshToken = sessionValue.refreshToken;
856
+ if (refreshToken === void 0 || refreshToken.length === 0) return null;
857
+ const body = new URLSearchParams();
858
+ body.set("grant_type", "refresh_token");
859
+ body.set("client_id", hydrogenCustomerAccountId ?? "");
860
+ body.set("refresh_token", refreshToken);
861
+ try {
862
+ const res = await doFetch(hydrogenTokenEndpoint(), {
863
+ method: "POST",
864
+ headers: {
865
+ "content-type": "application/x-www-form-urlencoded",
866
+ Origin: requestHttpsOrigin()
867
+ },
868
+ body
869
+ });
870
+ if (!res.ok) {
871
+ clearHydrogenCustomerSession();
872
+ return null;
873
+ }
874
+ const tokenResponse = await res.json();
875
+ if (typeof tokenResponse.access_token !== "string" || tokenResponse.access_token.length === 0 || typeof tokenResponse.expires_in !== "number" || !Number.isFinite(tokenResponse.expires_in)) {
876
+ clearHydrogenCustomerSession();
877
+ return null;
878
+ }
879
+ setHydrogenCustomerTokens(tokenResponse, sessionValue);
880
+ return tokenResponse.access_token;
881
+ } catch {
882
+ clearHydrogenCustomerSession();
883
+ return null;
884
+ }
885
+ };
886
+ const hydrogenAccessToken = async () => {
887
+ if (!hasHydrogenCustomerAccount) return null;
888
+ const sessionValue = readHydrogenCustomerSession();
889
+ if (sessionValue === null) return null;
890
+ const accessToken = sessionValue.accessToken;
891
+ const expiresAt = Number(sessionValue.expiresAt);
892
+ if (accessToken === void 0 || accessToken.length === 0) return null;
893
+ if (!Number.isFinite(expiresAt)) return null;
894
+ if (expiresAt <= Date.now()) return refreshHydrogenAccessToken(sessionValue);
895
+ return accessToken;
896
+ };
897
+ const loginRedirect = async () => {
898
+ if (hasHydrogenCustomerAccount) {
899
+ const state = randomOAuthToken();
900
+ const nonce = randomOAuthToken();
901
+ const codeVerifier = randomOAuthToken();
902
+ const codeChallenge = await sha256Base64Url(codeVerifier);
903
+ setHydrogenCustomerSession({
904
+ ...readHydrogenCustomerSession() ?? {},
905
+ codeVerifier,
906
+ state,
907
+ nonce,
908
+ redirectPath: returnTo()
909
+ });
910
+ const loginUrl = new URL(
911
+ `https://shopify.com/authentication/${hydrogenShopId}/oauth/authorize`
912
+ );
913
+ loginUrl.searchParams.set("client_id", hydrogenCustomerAccountId);
914
+ loginUrl.searchParams.set("scope", CUSTOMER_ACCOUNT_SCOPE);
915
+ loginUrl.searchParams.set("response_type", "code");
916
+ loginUrl.searchParams.set("redirect_uri", `${requestHttpsOrigin()}/account/authorize`);
917
+ loginUrl.searchParams.set("state", state);
918
+ loginUrl.searchParams.set("nonce", nonce);
919
+ loginUrl.searchParams.set("code_challenge", codeChallenge);
920
+ loginUrl.searchParams.set("code_challenge_method", "S256");
921
+ return redirectWithHydrogenSession(loginUrl.toString());
922
+ }
923
+ const params = new URLSearchParams({ returnTo: returnTo() });
924
+ return redirectResponse(`${proxyBase}/account/login?${params.toString()}`);
925
+ };
926
+ const badRequest = (message) => new Response(message, {
927
+ status: 400,
928
+ headers: { "content-type": "text/plain; charset=utf-8" }
929
+ });
930
+ const graphQLError = (message) => ({
931
+ data: null,
932
+ errors: [{ message }]
933
+ });
934
+ const proxyGraphQL = async (document, options) => {
935
+ const directAccessToken = await hydrogenAccessToken();
936
+ if (directAccessToken !== null) {
937
+ try {
938
+ const res = await doFetch(
939
+ `https://shopify.com/${hydrogenShopId}/account/customer/api/${DEFAULT_CUSTOMER_API_VERSION}/graphql`,
940
+ {
941
+ method: "POST",
942
+ headers: {
943
+ "content-type": "application/json",
944
+ Authorization: directAccessToken,
945
+ Origin: requestHttpsOrigin()
946
+ },
947
+ body: JSON.stringify({ query: document, variables: options?.variables ?? {} })
948
+ }
949
+ );
950
+ if (res.status === 401) {
951
+ clearHydrogenCustomerSession();
952
+ return graphQLError("not logged in");
953
+ }
954
+ if (!res.ok) return graphQLError(`customer-account query failed: HTTP ${res.status}`);
955
+ const body = await res.json();
956
+ return {
957
+ data: body.data ?? null,
958
+ ...body.errors !== void 0 ? { errors: body.errors } : {}
959
+ };
960
+ } catch (cause) {
961
+ return graphQLError(
962
+ `customer-account query failed: ${cause instanceof Error ? cause.message : String(cause)}`
963
+ );
964
+ }
965
+ }
966
+ if (token === null) return graphQLError("not logged in");
967
+ try {
968
+ const res = await proxyWrite("/account/graphql", "POST", {
969
+ query: document,
970
+ variables: options?.variables ?? {}
971
+ });
972
+ if (res.status === 401) return graphQLError("not logged in");
973
+ if (!res.ok) return graphQLError(`customer-account query failed: HTTP ${res.status}`);
974
+ const body = await res.json();
975
+ return {
976
+ data: body.data ?? null,
977
+ ...body.errors !== void 0 ? { errors: body.errors } : {}
978
+ };
979
+ } catch (cause) {
980
+ return graphQLError(
981
+ `customer-account query failed: ${cause instanceof Error ? cause.message : String(cause)}`
982
+ );
983
+ }
984
+ };
985
+ return {
986
+ query: proxyGraphQL,
987
+ mutate: proxyGraphQL,
988
+ login: loginRedirect,
989
+ async authorize() {
990
+ if (!hasHydrogenCustomerAccount) return redirectResponse("/account");
991
+ const url = new URL(opts.request.url);
992
+ const code = url.searchParams.get("code");
993
+ const state = url.searchParams.get("state");
994
+ const sessionValue = readHydrogenCustomerSession();
995
+ if (code === null || state === null) {
996
+ clearHydrogenCustomerSession();
997
+ return badRequest("No code or state parameter found in the redirect URL.");
998
+ }
999
+ if (sessionValue?.state !== state) {
1000
+ clearHydrogenCustomerSession();
1001
+ return badRequest("The session state does not match the state parameter.");
1002
+ }
1003
+ if (sessionValue.codeVerifier === void 0 || sessionValue.codeVerifier.length === 0) {
1004
+ clearHydrogenCustomerSession();
1005
+ return badRequest("No code verifier found in the session.");
1006
+ }
1007
+ const body = new URLSearchParams();
1008
+ body.set("grant_type", "authorization_code");
1009
+ body.set("client_id", hydrogenCustomerAccountId);
1010
+ body.set("redirect_uri", `${requestHttpsOrigin()}/account/authorize`);
1011
+ body.set("code", code);
1012
+ body.set("code_verifier", sessionValue.codeVerifier);
1013
+ const res = await doFetch(
1014
+ `https://shopify.com/authentication/${hydrogenShopId}/oauth/token`,
1015
+ {
1016
+ method: "POST",
1017
+ headers: {
1018
+ "content-type": "application/x-www-form-urlencoded",
1019
+ Origin: requestHttpsOrigin()
1020
+ },
1021
+ body
1022
+ }
1023
+ );
1024
+ if (!res.ok) {
1025
+ return new Response(await res.text(), {
1026
+ status: res.status,
1027
+ headers: { "content-type": "text/html; charset=utf-8" }
1028
+ });
1029
+ }
1030
+ const tokenResponse = await res.json();
1031
+ const responseNonce = idTokenNonce(tokenResponse.id_token ?? "");
1032
+ if (sessionValue.nonce !== responseNonce) {
1033
+ clearHydrogenCustomerSession();
1034
+ return badRequest(
1035
+ `Returned nonce does not match: ${sessionValue.nonce} !== ${responseNonce}`
1036
+ );
1037
+ }
1038
+ setHydrogenCustomerTokens(tokenResponse);
1039
+ return redirectWithHydrogenSession(sessionValue.redirectPath ?? "/account");
1040
+ },
1041
+ async handleAuthStatus() {
1042
+ if (await this.isLoggedIn()) return;
1043
+ throw await loginRedirect();
1044
+ },
1045
+ async requestLoginCode(input) {
1046
+ try {
1047
+ const res = await proxyPost("/account/send", { email: input.email });
1048
+ if (res.status === 200) return { status: "sent" };
1049
+ if (res.status === 429) return { status: "rate_limited" };
1050
+ return { status: "error" };
1051
+ } catch {
1052
+ return { status: "error" };
1053
+ }
1054
+ },
1055
+ async verifyLoginCode(input) {
1056
+ const body = { email: input.email, code: input.code };
1057
+ if (input.returnTo !== void 0) body.returnTo = input.returnTo;
1058
+ try {
1059
+ return await proxyPost("/account/verify", body);
1060
+ } catch {
1061
+ return new Response(null, { status: 503 });
1062
+ }
1063
+ },
1064
+ async logout() {
1065
+ const headers = new Headers();
1066
+ if (hasHydrogenCustomerAccount) {
1067
+ const idToken = readHydrogenCustomerSession()?.idToken;
1068
+ const postLogoutRedirectUri = requestHttpsOrigin();
1069
+ if (idToken !== void 0 && idToken.length > 0) {
1070
+ const logoutUrl = new URL(`https://shopify.com/authentication/${hydrogenShopId}/logout`);
1071
+ logoutUrl.searchParams.set("id_token_hint", idToken);
1072
+ logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutRedirectUri);
1073
+ headers.set("location", logoutUrl.toString());
1074
+ } else {
1075
+ headers.set("location", "/");
1076
+ }
1077
+ clearHydrogenCustomerSession();
1078
+ const destroyedCookie = await hydrogenSession?.destroy?.();
1079
+ if (destroyedCookie !== void 0 && destroyedCookie.length > 0) {
1080
+ headers.append("set-cookie", destroyedCookie);
1081
+ } else {
1082
+ await commitHydrogenSession(headers);
1083
+ }
1084
+ } else {
1085
+ headers.set("location", `${proxyBase}/account/logout`);
1086
+ }
1087
+ headers.append(
1088
+ "set-cookie",
1089
+ clearCustomerSessionCookie({ hostname: shopHostname(opts.request) })
1090
+ );
1091
+ return new Response(null, { status: 302, headers });
1092
+ },
1093
+ async getAccessToken() {
1094
+ const directAccessToken = await hydrogenAccessToken();
1095
+ if (directAccessToken !== null) return directAccessToken;
1096
+ return token;
1097
+ },
1098
+ async isLoggedIn() {
1099
+ if (await hydrogenAccessToken() !== null) return true;
1100
+ if (token === null) return false;
1101
+ try {
1102
+ const res = await proxyGet("/account");
1103
+ return res.ok;
1104
+ } catch {
1105
+ return false;
1106
+ }
1107
+ },
1108
+ async account() {
1109
+ return readJson(await proxyGet("/account"), "account read");
1110
+ },
1111
+ async orders(options) {
1112
+ const params = new URLSearchParams();
1113
+ if (options?.first !== void 0) params.set("first", String(options.first));
1114
+ if (options?.after !== void 0) params.set("after", options.after);
1115
+ const qs = params.toString();
1116
+ return readJson(
1117
+ await proxyGet(`/account/orders${qs ? `?${qs}` : ""}`),
1118
+ "orders read"
1119
+ );
1120
+ },
1121
+ async addresses() {
1122
+ return readJson(await proxyGet("/account/addresses"), "addresses read");
1123
+ },
1124
+ createAddress(input) {
1125
+ return proxyWrite("/account/addresses", "POST", input);
1126
+ },
1127
+ updateAddress(id, input) {
1128
+ return proxyWrite(`/account/addresses/${encodeURIComponent(id)}`, "PUT", input);
1129
+ },
1130
+ deleteAddress(id) {
1131
+ return proxyWrite(`/account/addresses/${encodeURIComponent(id)}`, "DELETE");
1132
+ }
1133
+ };
1134
+ }
1135
+ function randomOAuthToken() {
1136
+ const bytes = new Uint8Array(32);
1137
+ crypto.getRandomValues(bytes);
1138
+ let raw = "";
1139
+ for (const byte of bytes) raw += String.fromCharCode(byte);
1140
+ return base64UrlEncode(raw);
1141
+ }
1142
+ function isHydrogenSessionLike(value) {
1143
+ return value !== null && typeof value === "object" && "get" in value && "set" in value && typeof value.get === "function" && typeof value.set === "function";
1144
+ }
1145
+ async function sha256Base64Url(value) {
1146
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
1147
+ let raw = "";
1148
+ for (const byte of new Uint8Array(digest)) raw += String.fromCharCode(byte);
1149
+ return base64UrlEncode(raw);
1150
+ }
1151
+ function base64UrlEncode(raw) {
1152
+ return btoa(raw).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
1153
+ }
1154
+ function idTokenNonce(idToken) {
1155
+ const [, payload] = idToken.split(".");
1156
+ if (payload === void 0 || payload.length === 0) return null;
1157
+ try {
1158
+ const normalized = payload.replaceAll("-", "+").replaceAll("_", "/");
1159
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
1160
+ const decoded = JSON.parse(atob(padded));
1161
+ return typeof decoded.nonce === "string" ? decoded.nonce : null;
1162
+ } catch {
1163
+ return null;
1164
+ }
1165
+ }
1166
+
1167
+ // src/geo/base64url.ts
1168
+ var toBase64Url = (bytes) => {
1169
+ let bin = "";
1170
+ for (const b of bytes) bin += String.fromCharCode(b);
1171
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1172
+ };
1173
+ var fromBase64Url = (s) => {
1174
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
1175
+ const bin = atob(b64);
1176
+ const out = new Uint8Array(bin.length);
1177
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1178
+ return out;
1179
+ };
1180
+ var encodeUtf8Base64Url = (text) => toBase64Url(new TextEncoder().encode(text));
1181
+ var decodeBase64UrlUtf8 = (s) => new TextDecoder().decode(fromBase64Url(s));
1182
+
1183
+ // src/geo/geo.ts
1184
+ var GEO_HEADER = "x-kotao-geo";
1185
+ var GEO_SIGNATURE_HEADER = "x-kotao-geo-signature";
1186
+ var importKey = (key) => crypto.subtle.importKey(
1187
+ "raw",
1188
+ new TextEncoder().encode(key),
1189
+ { name: "HMAC", hash: "SHA-256" },
1190
+ false,
1191
+ ["sign", "verify"]
1192
+ );
1193
+ var toArrayBufferView = (bytes) => {
1194
+ const view = new Uint8Array(bytes.byteLength);
1195
+ view.set(bytes);
1196
+ return view;
1197
+ };
1198
+ async function signGeo(geo, key) {
1199
+ const payload = encodeUtf8Base64Url(JSON.stringify(geo));
1200
+ const cryptoKey = await importKey(key);
1201
+ const sig = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(payload));
1202
+ return { payload, signature: toBase64Url(new Uint8Array(sig)) };
1203
+ }
1204
+ var isGeo = (v) => {
1205
+ if (typeof v !== "object" || v === null) return false;
1206
+ const o = v;
1207
+ const strOrNull = (x) => x === null || typeof x === "string";
1208
+ const numOrNull = (x) => x === null || typeof x === "number";
1209
+ return strOrNull(o["country"]) && strOrNull(o["city"]) && strOrNull(o["colo"]) && numOrNull(o["botScore"]) && strOrNull(o["clientIp"]);
1210
+ };
1211
+ async function verifyGeo(headers, key) {
1212
+ const payload = headers.get(GEO_HEADER);
1213
+ const signature = headers.get(GEO_SIGNATURE_HEADER);
1214
+ if (!payload || !signature) return null;
1215
+ try {
1216
+ const cryptoKey = await importKey(key);
1217
+ const ok = await crypto.subtle.verify(
1218
+ "HMAC",
1219
+ cryptoKey,
1220
+ toArrayBufferView(fromBase64Url(signature)),
1221
+ new TextEncoder().encode(payload)
1222
+ );
1223
+ if (!ok) return null;
1224
+ const parsed = JSON.parse(decodeBase64UrlUtf8(payload));
1225
+ return isGeo(parsed) ? parsed : null;
1226
+ } catch {
1227
+ return null;
1228
+ }
1229
+ }
1230
+
1231
+ // src/localization/cookie.ts
1232
+ var LOCALIZATION_COOKIE = "kotao_localization";
1233
+ var MAX_AGE = 60 * 60 * 24 * 365;
1234
+ function readLocalizationCookie(cookieHeader) {
1235
+ if (cookieHeader == null || cookieHeader === "") return { country: null, locale: null };
1236
+ const match = cookieHeader.split(";").map((p) => p.trim()).find((p) => p.startsWith(`${LOCALIZATION_COOKIE}=`));
1237
+ if (match === void 0) return { country: null, locale: null };
1238
+ const [country = "", locale = ""] = decodeURIComponent(
1239
+ match.slice(LOCALIZATION_COOKIE.length + 1)
1240
+ ).split("|");
1241
+ return { country: country || null, locale: locale || null };
1242
+ }
1243
+ function buildLocalizationCookie(sel) {
1244
+ const value = `${sel.country ?? ""}|${sel.locale ?? ""}`;
1245
+ return `${LOCALIZATION_COOKIE}=${encodeURIComponent(value)}; Path=/; Max-Age=${MAX_AGE}; SameSite=Lax; Secure; HttpOnly`;
1246
+ }
1247
+
1248
+ // src/localization/routing.ts
1249
+ var LOCALE_SEGMENT_RE = /^[a-z]{2}(?:-[a-zA-Z]{2,4})?$/;
1250
+ var ROUTING_LOCALE_HEADER = "x-kotao-route-locale";
1251
+ var ROUTING_ORIGINAL_PATHNAME_HEADER = "x-kotao-original-pathname";
1252
+ function splitFirstPathSegment(pathname) {
1253
+ if (!pathname.startsWith("/")) return null;
1254
+ const withoutSlash = pathname.slice(1);
1255
+ const slash = withoutSlash.indexOf("/");
1256
+ if (slash === -1) return { segment: withoutSlash, rest: "" };
1257
+ return { segment: withoutSlash.slice(0, slash), rest: withoutSlash.slice(slash) };
1258
+ }
1259
+ function isLocalePathSegment(segment) {
1260
+ return LOCALE_SEGMENT_RE.test(segment);
1261
+ }
1262
+ function normalizeLocaleTag(value) {
1263
+ const [tag = ""] = (value ?? "").trim().replaceAll("_", "-").split(";", 1);
1264
+ if (tag === "" || tag === "*") return null;
1265
+ const [language = "", regionOrScript] = tag.split("-");
1266
+ const base = language.toLowerCase();
1267
+ if (!/^[a-z]{2,3}$/.test(base)) return null;
1268
+ if (regionOrScript === void 0 || regionOrScript.length === 0) return base;
1269
+ if (!/^[a-zA-Z0-9]{2,4}$/.test(regionOrScript)) return null;
1270
+ const suffix = regionOrScript.length === 2 && /^[a-zA-Z]{2}$/.test(regionOrScript) ? regionOrScript.toUpperCase() : regionOrScript.length === 4 && /^[a-zA-Z]{4}$/.test(regionOrScript) ? `${regionOrScript[0].toUpperCase()}${regionOrScript.slice(1).toLowerCase()}` : regionOrScript;
1271
+ return `${base}-${suffix}`;
1272
+ }
1273
+ function qualityFromAcceptLanguagePart(part) {
1274
+ const params = part.split(";").slice(1);
1275
+ const q = params.map((param) => param.trim()).find((param) => param.toLowerCase().startsWith("q="));
1276
+ if (q === void 0) return 1;
1277
+ const value = Number(q.slice(2));
1278
+ return Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0;
1279
+ }
1280
+ function parseAcceptLanguage(header) {
1281
+ if (header === null || header === void 0 || header.trim().length === 0) return [];
1282
+ return header.split(",").map((part, order) => {
1283
+ const locale = normalizeLocaleTag(part);
1284
+ const quality = qualityFromAcceptLanguagePart(part);
1285
+ if (locale === null || quality <= 0) return null;
1286
+ return { locale, quality, order };
1287
+ }).filter((candidate) => candidate !== null).sort((left, right) => right.quality - left.quality || left.order - right.order);
1288
+ }
1289
+ function localeFromAcceptLanguage(header, availableLocales) {
1290
+ const candidates = parseAcceptLanguage(header);
1291
+ if (candidates.length === 0) return null;
1292
+ const available = (availableLocales ?? []).map((locale) => {
1293
+ const normalized = normalizeLocaleTag(locale);
1294
+ return normalized === null ? null : { locale, normalized };
1295
+ }).filter((entry) => entry !== null);
1296
+ if (available.length === 0) return candidates[0].locale;
1297
+ for (const candidate of candidates) {
1298
+ const exact = available.find(
1299
+ (entry) => entry.normalized.toLowerCase() === candidate.locale.toLowerCase()
1300
+ );
1301
+ if (exact !== void 0) return exact.locale;
1302
+ const language = candidate.locale.split("-")[0];
1303
+ const languageMatch = available.find((entry) => entry.normalized.split("-")[0] === language);
1304
+ if (languageMatch !== void 0) return languageMatch.locale;
1305
+ }
1306
+ return null;
1307
+ }
1308
+ function localeFromPathname(pathname) {
1309
+ const first = splitFirstPathSegment(pathname);
1310
+ if (first === null || first.segment.length === 0) return null;
1311
+ return isLocalePathSegment(first.segment) ? first.segment : null;
1312
+ }
1313
+ function originalPathnameFromRequest(request) {
1314
+ const fromHeader = request.headers.get(ROUTING_ORIGINAL_PATHNAME_HEADER);
1315
+ return fromHeader !== null && fromHeader.startsWith("/") ? fromHeader : new URL(request.url).pathname;
1316
+ }
1317
+ function localeFromRequest(request) {
1318
+ const fromHeader = request.headers.get(ROUTING_LOCALE_HEADER);
1319
+ if (fromHeader !== null && isLocalePathSegment(fromHeader)) return fromHeader;
1320
+ return localeFromPathname(originalPathnameFromRequest(request));
1321
+ }
1322
+ function stripLocalePrefixFromPathname(pathname) {
1323
+ const locale = localeFromPathname(pathname);
1324
+ if (locale === null) return { locale: null, pathname };
1325
+ const first = splitFirstPathSegment(pathname);
1326
+ const rest = first?.rest ?? "";
1327
+ return { locale, pathname: rest.length > 0 ? rest : "/" };
1328
+ }
1329
+ function stripLocalePrefixFromRequest(request) {
1330
+ const url = new URL(request.url);
1331
+ const stripped = stripLocalePrefixFromPathname(url.pathname);
1332
+ if (stripped.locale === null) return request;
1333
+ const originalPathname = url.pathname;
1334
+ url.pathname = stripped.pathname;
1335
+ const next = new Request(url, request);
1336
+ next.headers.set(ROUTING_LOCALE_HEADER, stripped.locale);
1337
+ next.headers.set(ROUTING_ORIGINAL_PATHNAME_HEADER, originalPathname);
1338
+ return next;
1339
+ }
1340
+ function normalizeLocale(locale) {
1341
+ const trimmed = locale?.trim() ?? "";
1342
+ return trimmed.length > 0 && isLocalePathSegment(trimmed) ? trimmed : null;
1343
+ }
1344
+ function localizePath(path, locale, options = {}) {
1345
+ const targetLocale = normalizeLocale(locale);
1346
+ const primaryLocale = normalizeLocale(options.primaryLocale);
1347
+ const url = new URL(path.startsWith("/") ? path : "/", "https://storefront.local");
1348
+ const stripped = stripLocalePrefixFromPathname(url.pathname).pathname;
1349
+ const suffix = `${url.search}${url.hash}`;
1350
+ if (targetLocale === null || primaryLocale !== null && targetLocale === primaryLocale) {
1351
+ return `${stripped}${suffix}`;
1352
+ }
1353
+ return `${stripped === "/" ? `/${targetLocale}` : `/${targetLocale}${stripped}`}${suffix}`;
1354
+ }
1355
+
1356
+ // src/storefront-client/client.ts
1357
+ var hashKey = (query, variables) => `sf:${query.replace(/\s+/g, " ").trim()}:${variables ? JSON.stringify(variables) : ""}`;
1358
+ var COUNTRY_RE = /^[A-Z]{2}$/;
1359
+ var DEFAULT_HYDROGEN_STOREFRONT_API_VERSION = "2025-01";
1360
+ function endpointForStorefrontApiVersion(endpoint, version) {
1361
+ if (version === void 0 || version.length === 0) return endpoint;
1362
+ try {
1363
+ const url = new URL(endpoint);
1364
+ if (!/^\/api\/[^/]+\/graphql\.json$/u.test(url.pathname)) return endpoint;
1365
+ url.pathname = url.pathname.replace(
1366
+ /^\/api\/[^/]+\/graphql\.json$/u,
1367
+ `/api/${version}/graphql.json`
1368
+ );
1369
+ return url.toString();
1370
+ } catch {
1371
+ return endpoint;
1372
+ }
1373
+ }
1374
+ function createStorefrontClient(opts) {
1375
+ if (isHydrogenStorefrontClientOptions(opts)) {
1376
+ const i18n = normalizeHydrogenI18n(opts.i18n);
1377
+ const client = createStorefrontClientCore({
1378
+ endpoint: hydrogenStorefrontEndpoint(opts.storeDomain, opts.storefrontApiVersion),
1379
+ token: opts.publicStorefrontToken,
1380
+ tokenHeader: "x-shopify-storefront-access-token",
1381
+ cache: createCache({ cache: opts.cache, waitUntil: opts.waitUntil }),
1382
+ fetch: opts.fetch,
1383
+ defaultStrategy: opts.defaultStrategy,
1384
+ extraHeaders: opts.storefrontHeaders,
1385
+ i18n
1386
+ });
1387
+ return attachHydrogenResult(client);
1388
+ }
1389
+ return attachHydrogenResult(createStorefrontClientCore(opts));
1390
+ }
1391
+ function createStorefrontClientCore(opts) {
1392
+ const doFetch = opts.fetch ?? fetch;
1393
+ const defaultStrategy = opts.defaultStrategy ?? StorefrontDefaultCache();
1394
+ const tokenHeader = opts.tokenHeader ?? "x-kotao-storefront-token";
1395
+ const localeKey = opts.locale !== void 0 && opts.locale.length > 0 ? opts.locale : "";
1396
+ const countryKey = opts.country !== void 0 && COUNTRY_RE.test(opts.country) ? opts.country : "";
1397
+ const i18n = normalizeStorefrontI18n(opts.i18n, opts.locale, countryKey);
1398
+ const execute = async (document, options) => {
1399
+ const endpoint = endpointForStorefrontApiVersion(opts.endpoint, options?.storefrontApiVersion);
1400
+ const headers = {
1401
+ "content-type": "application/json",
1402
+ ...headersRecord(opts.extraHeaders),
1403
+ [tokenHeader]: opts.token,
1404
+ ...opts.signedShopContextHeaders ?? {}
1405
+ };
1406
+ if (opts.siteId !== void 0 && opts.siteId.trim().length > 0) {
1407
+ headers["x-kotao-site-id"] = opts.siteId;
1408
+ }
1409
+ if (localeKey.length > 0) {
1410
+ headers["x-kotao-locale"] = localeKey;
1411
+ }
1412
+ if (countryKey.length > 0) {
1413
+ headers["x-kotao-country"] = countryKey;
1414
+ }
1415
+ const res = await doFetch(endpoint, {
1416
+ method: "POST",
1417
+ headers,
1418
+ body: JSON.stringify({ query: document, variables: options?.variables })
1419
+ });
1420
+ const opName = document.replace(/\s+/g, " ").trim().slice(0, 48);
1421
+ const raw = await res.text();
1422
+ let body;
1423
+ try {
1424
+ body = JSON.parse(raw);
1425
+ } catch {
1426
+ console.error(
1427
+ `[storefront] /api/storefront non-JSON response: status=${res.status} content-type=${res.headers.get("content-type") ?? ""} len=${raw.length} op="${opName}" body=${raw.slice(0, 180)}`
1428
+ );
1429
+ throw new Error(`Storefront API returned a non-JSON response (status ${res.status})`);
1430
+ }
1431
+ if (!res.ok) {
1432
+ const shopCtx = opts.signedShopContextHeaders !== void 0 ? "present" : "absent";
1433
+ console.error(
1434
+ `[storefront] /api/storefront HTTP ${res.status} endpoint=${endpoint} shopCtx=${shopCtx} op="${opName}" body=${raw.slice(0, 180)}`
1435
+ );
1436
+ }
1437
+ if (body.errors?.length) {
1438
+ console.error(`[storefront] GraphQL query failed: ${JSON.stringify(body.errors)}`);
1439
+ throw new Error(body.errors.map((e) => e.message).join("; "));
1440
+ }
1441
+ if (body.data === void 0) {
1442
+ console.error(
1443
+ `[storefront] /api/storefront returned no data: status=${res.status} op="${opName}" body=${raw.slice(0, 180)}`
1444
+ );
1445
+ throw new Error("Storefront API returned no data");
1446
+ }
1447
+ return body.data;
1448
+ };
1449
+ return {
1450
+ CacheNone,
1451
+ CacheShort,
1452
+ CacheLong,
1453
+ CacheCustom,
1454
+ cache: opts.cache.raw,
1455
+ i18n,
1456
+ query(query, options) {
1457
+ const strategy = options?.cache ?? defaultStrategy;
1458
+ const versionKey = options?.storefrontApiVersion ?? "";
1459
+ return opts.cache.run(
1460
+ `${localeKey}:${countryKey}:${versionKey}:${hashKey(query, options?.variables)}`,
1461
+ strategy,
1462
+ () => execute(query, options)
1463
+ );
1464
+ },
1465
+ mutate(mutation2, options) {
1466
+ return execute(mutation2, options);
1467
+ }
1468
+ };
1469
+ }
1470
+ function isHydrogenStorefrontClientOptions(opts) {
1471
+ return "storeDomain" in opts && "publicStorefrontToken" in opts && "waitUntil" in opts;
1472
+ }
1473
+ function attachHydrogenResult(client) {
1474
+ return Object.assign(client, { storefront: client });
1475
+ }
1476
+ function headersRecord(headers) {
1477
+ if (headers === void 0) return {};
1478
+ const result = {};
1479
+ new Headers(headers).forEach((value, key) => {
1480
+ result[key] = value;
1481
+ });
1482
+ return result;
1483
+ }
1484
+ function hydrogenStorefrontEndpoint(storeDomain, version) {
1485
+ const trimmed = storeDomain.trim();
1486
+ if (trimmed === "") throw new Error("storeDomain must not be empty");
1487
+ const url = new URL(/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`);
1488
+ url.pathname = `/api/${version ?? DEFAULT_HYDROGEN_STOREFRONT_API_VERSION}/graphql.json`;
1489
+ url.search = "";
1490
+ url.hash = "";
1491
+ return url.toString();
1492
+ }
1493
+ function normalizeHydrogenI18n(input) {
1494
+ const source = input ?? {};
1495
+ const fallback = i18nFromLocale(void 0, "");
1496
+ return {
1497
+ ...source,
1498
+ language: String(source.language ?? fallback.language).toUpperCase(),
1499
+ country: String(source.country ?? fallback.country).toUpperCase(),
1500
+ currency: String(source.currency ?? fallback.currency).toUpperCase(),
1501
+ pathPrefix: normalizePathPrefix(source.pathPrefix ?? fallback.pathPrefix)
1502
+ };
1503
+ }
1504
+ function normalizeStorefrontI18n(input, locale, fallbackCountry) {
1505
+ const fallback = i18nFromLocale(locale, fallbackCountry);
1506
+ return {
1507
+ ...input,
1508
+ language: String(input?.language ?? fallback.language).toUpperCase(),
1509
+ country: String(input?.country ?? fallback.country).toUpperCase(),
1510
+ currency: String(input?.currency ?? fallback.currency).toUpperCase(),
1511
+ pathPrefix: normalizePathPrefix(input?.pathPrefix ?? fallback.pathPrefix)
1512
+ };
1513
+ }
1514
+ function i18nFromLocale(locale, fallbackCountry) {
1515
+ const trimmedLocale = locale?.trim();
1516
+ const [language, country] = (trimmedLocale && trimmedLocale.length > 0 ? trimmedLocale : "en-us").split(/[-_]/);
1517
+ return {
1518
+ language: (language ?? "en").toUpperCase(),
1519
+ country: (country ?? (fallbackCountry || "US")).toUpperCase(),
1520
+ currency: "USD",
1521
+ pathPrefix: pathPrefixFromLocale(trimmedLocale)
1522
+ };
1523
+ }
1524
+ function pathPrefixFromLocale(locale) {
1525
+ if (locale === void 0 || locale.length === 0) return "";
1526
+ return `/${locale.toLowerCase().replaceAll("_", "-")}`;
1527
+ }
1528
+ function normalizePathPrefix(prefix) {
1529
+ if (prefix === void 0 || prefix.trim().length === 0) return "";
1530
+ const trimmed = prefix.trim();
1531
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
1532
+ }
1533
+
1534
+ // src/with-cache/with-cache.ts
1535
+ var createWithCache = (cache) => (key, strategy, fn) => cache.run(key, strategy, fn);
1536
+ async function localSignedShopContextHeaders(key, siteId, hostname) {
1537
+ if (key === void 0 || key.length === 0) return void 0;
1538
+ if (siteId === void 0 || siteId.trim().length === 0) return void 0;
1539
+ const { payload, signature } = await signShopContext(
1540
+ {
1541
+ shopId: siteId,
1542
+ ...hostname !== void 0 && hostname.length > 0 ? { hostname } : {}
1543
+ },
1544
+ key
1545
+ );
1546
+ return { [SHOP_HEADER]: payload, [SHOP_SIGNATURE_HEADER]: signature };
1547
+ }
1548
+ var CACHE_NAME = "kotao-storefront-v1";
1549
+ var CACHE_VERSION_HEADER = "x-kotao-cache-version";
1550
+ var API_LOCALE_RE = /^[a-z]{2,3}(-[a-zA-Z]{2,4})?$/;
1551
+ var API_COUNTRY_RE = /^[A-Z]{2}$/;
1552
+ var SAME_ORIGIN_PATH2 = /^\/(?![/\\])/;
1553
+ var LEGACY_DIRECT_STOREFRONT_API_ORIGIN = /^https:\/\/storefront-api\.kotao\.(?:com|dev|local)\/?$/;
1554
+ var parseLocale = (request) => {
1555
+ return localeFromRequest(request);
1556
+ };
1557
+ function storefrontAuthHeader(env) {
1558
+ if (env.STOREFRONT_API_TOKEN_HEADER === "x-shopify-storefront-access-token") {
1559
+ return "x-shopify-storefront-access-token";
1560
+ }
1561
+ return env.STOREFRONT_API_TOKEN_HEADER === "x-api-key" ? "x-api-key" : "x-kotao-storefront-token";
1562
+ }
1563
+ function resolveStorefrontApiEndpoint(endpoint, requestUrl) {
1564
+ const trimmed = endpoint.trim();
1565
+ if (SAME_ORIGIN_PATH2.test(trimmed)) return new URL(trimmed, requestUrl).toString();
1566
+ if (LEGACY_DIRECT_STOREFRONT_API_ORIGIN.test(trimmed))
1567
+ return new URL("/api/storefront", requestUrl).toString();
1568
+ return endpoint;
1569
+ }
1570
+ async function createKotaoContext(request, env, executionCtx, deps) {
1571
+ const waitUntil = (p) => {
1572
+ executionCtx.waitUntil(p);
1573
+ };
1574
+ const cacheVersion = request.headers.get(CACHE_VERSION_HEADER)?.trim() || "0";
1575
+ const cacheName = `${CACHE_NAME}-${cacheVersion}`;
1576
+ const rawCache = await (deps?.openCache ? deps.openCache(cacheName) : caches.open(cacheName));
1577
+ const cache = createCache({ cache: rawCache, waitUntil });
1578
+ const storefrontApiEndpoint = env.STOREFRONT_API_ENDPOINT ?? "/api/storefront";
1579
+ const storefrontApiToken = env.STOREFRONT_API_TOKEN ?? env.PRIVATE_STOREFRONT_API_TOKEN ?? env.PUBLIC_STOREFRONT_API_TOKEN ?? "";
1580
+ const storefrontGeoSigningKey = env.STOREFRONT_GEO_SIGNING_KEY ?? "";
1581
+ const geo = await verifyGeo(request.headers, storefrontGeoSigningKey);
1582
+ const resolvedEnv = {
1583
+ ...env,
1584
+ STOREFRONT_API_ENDPOINT: resolveStorefrontApiEndpoint(storefrontApiEndpoint, request.url),
1585
+ STOREFRONT_API_TOKEN: storefrontApiToken,
1586
+ STOREFRONT_GEO_SIGNING_KEY: storefrontGeoSigningKey
1587
+ };
1588
+ const siteId = resolvedEnv.KOTAO_SITE_ID ?? resolvedEnv.KOTAO_SHOP_ID;
1589
+ const inboundShop = request.headers.get(SHOP_HEADER);
1590
+ const inboundShopSignature = request.headers.get(SHOP_SIGNATURE_HEADER);
1591
+ const signedShopContextHeaders = inboundShop !== null && inboundShopSignature !== null ? { [SHOP_HEADER]: inboundShop, [SHOP_SIGNATURE_HEADER]: inboundShopSignature } : await localSignedShopContextHeaders(
1592
+ resolvedEnv.STOREFRONT_SHOP_CONTEXT_KEY,
1593
+ siteId,
1594
+ new URL(request.url).hostname
1595
+ );
1596
+ const pathLocale = parseLocale(request);
1597
+ const localizationCookie = readLocalizationCookie(request.headers.get("cookie"));
1598
+ const cookieLocale = localizationCookie.locale;
1599
+ const browserLocale = localeFromAcceptLanguage(request.headers.get("accept-language"));
1600
+ const selectedLocale = pathLocale ?? cookieLocale ?? browserLocale?.split("-")[0] ?? null;
1601
+ const apiLocale = selectedLocale !== null && API_LOCALE_RE.test(selectedLocale) ? selectedLocale : void 0;
1602
+ const cookieCountry = localizationCookie.country !== null && API_COUNTRY_RE.test(localizationCookie.country) ? localizationCookie.country : null;
1603
+ const geoCountry = geo?.country?.trim().toUpperCase() ?? null;
1604
+ const apiCountry = cookieCountry ?? (geoCountry !== null && API_COUNTRY_RE.test(geoCountry) ? geoCountry : null);
1605
+ const storefront = createStorefrontClient({
1606
+ endpoint: resolvedEnv.STOREFRONT_API_ENDPOINT,
1607
+ token: resolvedEnv.STOREFRONT_API_TOKEN,
1608
+ tokenHeader: storefrontAuthHeader(resolvedEnv),
1609
+ ...siteId !== void 0 && siteId.trim().length > 0 ? { siteId } : {},
1610
+ ...signedShopContextHeaders ? { signedShopContextHeaders } : {},
1611
+ ...apiLocale !== void 0 ? { locale: apiLocale } : {},
1612
+ ...apiCountry !== null ? { country: apiCountry } : {},
1613
+ cache
1614
+ });
1615
+ const cart = createCartHandler({ request, storefront });
1616
+ const contextSession = withSessionPresenceHint(
1617
+ deps?.session,
1618
+ readCustomerSessionToken(request.headers.get("cookie")) !== null
1619
+ );
1620
+ const customerAccount = createCustomerAccountClient({
1621
+ request,
1622
+ customerAccountId: resolvedEnv.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID,
1623
+ shopId: resolvedEnv.SHOP_ID,
1624
+ session: contextSession
1625
+ });
1626
+ return {
1627
+ storefront,
1628
+ cart,
1629
+ customerAccount,
1630
+ geo,
1631
+ withCache: createWithCache(cache),
1632
+ locale: pathLocale,
1633
+ // Cookie-presence hint only — the theme cannot validate the HMAC; the authoritative check is the proxy.
1634
+ session: contextSession,
1635
+ env: resolvedEnv,
1636
+ waitUntil
1637
+ };
1638
+ }
1639
+ function withSessionPresenceHint(session, isLoggedIn) {
1640
+ if (session === void 0) return createEphemeralSession(isLoggedIn);
1641
+ Object.defineProperty(session, "isLoggedIn", {
1642
+ configurable: true,
1643
+ enumerable: true,
1644
+ value: isLoggedIn,
1645
+ writable: true
1646
+ });
1647
+ return session;
1648
+ }
1649
+ function createEphemeralSession(isLoggedIn) {
1650
+ const data = /* @__PURE__ */ new Map();
1651
+ let isPending = false;
1652
+ return {
1653
+ get isPending() {
1654
+ return isPending;
1655
+ },
1656
+ isLoggedIn,
1657
+ has: (name) => data.has(name),
1658
+ get: (name) => data.get(name),
1659
+ flash(name, value) {
1660
+ const previous = data.get(name);
1661
+ if (value !== void 0) {
1662
+ this.set(name, value);
1663
+ return void 0;
1664
+ }
1665
+ this.unset(name);
1666
+ return previous;
1667
+ },
1668
+ unset(name) {
1669
+ isPending = true;
1670
+ data.delete(name);
1671
+ },
1672
+ set(name, value) {
1673
+ isPending = true;
1674
+ data.set(name, value);
1675
+ },
1676
+ destroy() {
1677
+ isPending = false;
1678
+ data.clear();
1679
+ return "";
1680
+ },
1681
+ commit() {
1682
+ isPending = false;
1683
+ return "";
1684
+ }
1685
+ };
1686
+ }
1687
+
1688
+ export { CART_ID_COOKIE, CHECKOUT_CONSENT_CATEGORIES, CHECKOUT_CONSENT_PARAM_PREFIX, CHECKOUT_DESIGN_TOKEN_INPUTS, CUSTOMER_SESSION_COOKIE, CacheCustom, CacheLong, CacheNone, CacheShort, CustomerAccountError, DEFAULT_SESSION_MAX_AGE, GEO_HEADER, GEO_SIGNATURE_HEADER, LOCALIZATION_COOKIE, ROUTING_LOCALE_HEADER, ROUTING_ORIGINAL_PATHNAME_HEADER, StorefrontDefaultCache, buildCartIdCookie, buildCustomerSessionCookie, buildLocalizationCookie, cartGetIdDefault, cartSetIdDefault, clearCustomerSessionCookie, createCache, createCartHandler, createCustomerAccountClient, createKotaoContext, createStorefrontClient, createWithCache, decodeCheckoutConsent, encodeCheckoutConsent, extractCheckoutDesignTokens, generateCacheControlHeader, isLocalePathSegment, localeFromAcceptLanguage, localeFromPathname, localeFromRequest, localizePath, originalPathnameFromRequest, readCartIdCookie, readCustomerSessionToken, readLocalizationCookie, resolveCheckoutUrl, shopHostname, signGeo, stripLocalePrefixFromPathname, stripLocalePrefixFromRequest, verifyGeo };
1689
+ //# sourceMappingURL=chunk-U5677AEA.js.map
1690
+ //# sourceMappingURL=chunk-U5677AEA.js.map