@kotao/storefront 0.2.1 → 0.2.3

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 (58) hide show
  1. package/dist/chunk-4OQYNUGM.js +29 -0
  2. package/dist/chunk-4OQYNUGM.js.map +1 -0
  3. package/dist/{chunk-U5677AEA.js → chunk-4TEFGD42.js} +343 -40
  4. package/dist/chunk-4TEFGD42.js.map +1 -0
  5. package/dist/chunk-7MTCGXIG.js +144 -0
  6. package/dist/chunk-7MTCGXIG.js.map +1 -0
  7. package/dist/{chunk-GCQI6OKH.js → chunk-H3D7BAOL.js} +3 -26
  8. package/dist/chunk-H3D7BAOL.js.map +1 -0
  9. package/dist/chunk-IIOHENCG.js +239 -0
  10. package/dist/chunk-IIOHENCG.js.map +1 -0
  11. package/dist/chunk-M7BPQ54D.js +26 -0
  12. package/dist/chunk-M7BPQ54D.js.map +1 -0
  13. package/dist/{chunk-ISAFEOR2.js → chunk-PXVDVUFX.js} +3 -2
  14. package/dist/chunk-PXVDVUFX.js.map +1 -0
  15. package/dist/chunk-VV653S52.js +246 -0
  16. package/dist/chunk-VV653S52.js.map +1 -0
  17. package/dist/chunk-WG7HYVHJ.js +38 -0
  18. package/dist/chunk-WG7HYVHJ.js.map +1 -0
  19. package/dist/{client-D9YYA2w6.d.ts → client-D51WdvYR.d.ts} +55 -8
  20. package/dist/client-bsiNPHBi.d.ts +159 -0
  21. package/dist/createCartHandler-C4RrPwSG.d.ts +58 -0
  22. package/dist/editor.d.ts +1 -1
  23. package/dist/events-react.d.ts +68 -0
  24. package/dist/events-react.js +557 -0
  25. package/dist/events-react.js.map +1 -0
  26. package/dist/events.d.ts +18 -0
  27. package/dist/events.js +5 -0
  28. package/dist/events.js.map +1 -0
  29. package/dist/fetch-M3Y-fQrW.d.ts +47 -0
  30. package/dist/forms.d.ts +58 -0
  31. package/dist/forms.js +3 -0
  32. package/dist/forms.js.map +1 -0
  33. package/dist/http-types-CGh8WGHB.d.ts +11 -0
  34. package/dist/index.d.ts +75 -12
  35. package/dist/index.js +55 -8
  36. package/dist/index.js.map +1 -1
  37. package/dist/offer-token-C-nk_nj8.d.ts +16 -0
  38. package/dist/{pagination-CPgH3fAa.d.ts → pagination-DCrQk02Q.d.ts} +1 -1
  39. package/dist/product.d.ts +1 -1
  40. package/dist/react-router.d.ts +12 -6
  41. package/dist/react-router.js +4 -2
  42. package/dist/react-router.js.map +1 -1
  43. package/dist/react.d.ts +30 -7
  44. package/dist/react.js +275 -156
  45. package/dist/react.js.map +1 -1
  46. package/dist/{routing-BfUp49o8.d.ts → routing-sEonP93J.d.ts} +26 -59
  47. package/dist/seo.d.ts +4 -1
  48. package/dist/seo.js +1 -1
  49. package/dist/settings.d.ts +6 -39
  50. package/dist/settings.js +2 -1
  51. package/dist/{shop-DkQmro-Y.d.ts → shop-BJSvtmnV.d.ts} +2 -2
  52. package/dist/{store-DQK1nhob.d.ts → store-CtCWHI8H.d.ts} +1 -1
  53. package/dist/types-CSNXbGFW.d.ts +108 -0
  54. package/dist/{types-Df3bGbKU.d.ts → types-bcbQzx87.d.ts} +1 -1
  55. package/package.json +13 -1
  56. package/dist/chunk-GCQI6OKH.js.map +0 -1
  57. package/dist/chunk-ISAFEOR2.js.map +0 -1
  58. package/dist/chunk-U5677AEA.js.map +0 -1
@@ -0,0 +1,29 @@
1
+ // src/events/cart.ts
2
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
3
+ var SHOPIFY_CART_GID_RE = new RegExp(
4
+ `^gid://shopify/Cart/(${UUID_RE.source.slice(1, -1)})$`,
5
+ "iu"
6
+ );
7
+ var EventCartPreparationError = class extends Error {
8
+ constructor() {
9
+ super("Unable to prepare event cart");
10
+ this.name = "EventCartPreparationError";
11
+ }
12
+ };
13
+ function eventCartToken(cartId) {
14
+ if (UUID_RE.test(cartId)) return cartId;
15
+ const gidToken = SHOPIFY_CART_GID_RE.exec(cartId)?.[1];
16
+ if (gidToken !== void 0) return gidToken;
17
+ throw new EventCartPreparationError();
18
+ }
19
+ async function prepareEventCart(cart) {
20
+ const existing = await cart.get();
21
+ if (existing) return { cartToken: eventCartToken(existing.id), headers: new Headers() };
22
+ const created = await cart.create();
23
+ if (!created.cart || created.userErrors.length > 0) throw new EventCartPreparationError();
24
+ return { cartToken: eventCartToken(created.cart.id), headers: created.headers };
25
+ }
26
+
27
+ export { EventCartPreparationError, eventCartToken, prepareEventCart };
28
+ //# sourceMappingURL=chunk-4OQYNUGM.js.map
29
+ //# sourceMappingURL=chunk-4OQYNUGM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/events/cart.ts"],"names":[],"mappings":";AAEA,IAAM,OAAA,GAAU,6EAAA;AAChB,IAAM,sBAAsB,IAAI,MAAA;AAAA,EAC9B,wBAAwB,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA,EAAA,CAAA;AAAA,EACnD;AACF,CAAA;AAEO,IAAM,yBAAA,GAAN,cAAwC,KAAA,CAAM;AAAA,EACnD,WAAA,GAAc;AACZ,IAAA,KAAA,CAAM,8BAA8B,CAAA;AACpC,IAAA,IAAA,CAAK,IAAA,GAAO,2BAAA;AAAA,EACd;AACF;AAEO,SAAS,eAAe,MAAA,EAAwB;AACrD,EAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAA,EAAG,OAAO,MAAA;AACjC,EAAA,MAAM,QAAA,GAAW,mBAAA,CAAoB,IAAA,CAAK,MAAM,IAAI,CAAC,CAAA;AACrD,EAAA,IAAI,QAAA,KAAa,QAAW,OAAO,QAAA;AACnC,EAAA,MAAM,IAAI,yBAAA,EAA0B;AACtC;AAEA,eAAsB,iBACpB,IAAA,EACkD;AAClD,EAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,GAAA,EAAI;AAChC,EAAA,IAAI,QAAA,EAAU,OAAO,EAAE,SAAA,EAAW,cAAA,CAAe,QAAA,CAAS,EAAE,CAAA,EAAG,OAAA,EAAS,IAAI,OAAA,EAAQ,EAAE;AACtF,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,MAAA,EAAO;AAClC,EAAA,IAAI,CAAC,QAAQ,IAAA,IAAQ,OAAA,CAAQ,WAAW,MAAA,GAAS,CAAA,EAAG,MAAM,IAAI,yBAAA,EAA0B;AACxF,EAAA,OAAO,EAAE,WAAW,cAAA,CAAe,OAAA,CAAQ,KAAK,EAAE,CAAA,EAAG,OAAA,EAAS,OAAA,CAAQ,OAAA,EAAQ;AAChF","file":"chunk-4OQYNUGM.js","sourcesContent":["import type { CartHandler } from '../cart/createCartHandler'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu\nconst SHOPIFY_CART_GID_RE = new RegExp(\n `^gid://shopify/Cart/(${UUID_RE.source.slice(1, -1)})$`,\n 'iu',\n)\n\nexport class EventCartPreparationError extends Error {\n constructor() {\n super('Unable to prepare event cart')\n this.name = 'EventCartPreparationError'\n }\n}\n\nexport function eventCartToken(cartId: string): string {\n if (UUID_RE.test(cartId)) return cartId\n const gidToken = SHOPIFY_CART_GID_RE.exec(cartId)?.[1]\n if (gidToken !== undefined) return gidToken\n throw new EventCartPreparationError()\n}\n\nexport async function prepareEventCart(\n cart: CartHandler,\n): Promise<{ cartToken: string; headers: Headers }> {\n const existing = await cart.get()\n if (existing) return { cartToken: eventCartToken(existing.id), headers: new Headers() }\n const created = await cart.create()\n if (!created.cart || created.userErrors.length > 0) throw new EventCartPreparationError()\n return { cartToken: eventCartToken(created.cart.id), headers: created.headers }\n}\n"]}
@@ -1,30 +1,44 @@
1
1
  import { CONSENT_BUCKETS, readConsentCookie, CART_FORM_ACTIONS } from './chunk-OGWYTYYS.js';
2
- import { SHOP_HEADER, SHOP_SIGNATURE_HEADER, signShopContext } from '@kotao/storefront-contracts';
2
+ import { fetchThemeSettings } from './chunk-M7BPQ54D.js';
3
+ import { createEventsClient, normalizeStorefrontHeaders } from './chunk-7MTCGXIG.js';
4
+ import { SHOP_HEADER, SHOP_SIGNATURE_HEADER, SHOP_ORIGIN_HEADER, autoRedirectEnabled, AUTO_REDIRECT_LANGUAGE_HEADER, AUTO_REDIRECT_COUNTRY_HEADER, signShopContext } from '@kotao/storefront-contracts';
3
5
 
4
6
  // src/cache/cache.ts
5
- var CacheNone = () => ({ mode: "none", maxAge: 0, staleWhileRevalidate: 0 });
7
+ var DEFAULT_STALE_IF_ERROR_SECONDS = 300;
8
+ var CacheNone = () => ({
9
+ mode: "none",
10
+ maxAge: 0,
11
+ staleWhileRevalidate: 0,
12
+ staleIfError: 0
13
+ });
6
14
  var CacheShort = (o) => ({
7
15
  mode: "short",
8
16
  maxAge: o?.maxAge ?? 1,
9
- staleWhileRevalidate: o?.staleWhileRevalidate ?? 9
17
+ staleWhileRevalidate: o?.staleWhileRevalidate ?? 9,
18
+ staleIfError: o?.staleIfError ?? DEFAULT_STALE_IF_ERROR_SECONDS
10
19
  });
11
20
  var CacheLong = (o) => ({
12
21
  mode: "long",
13
22
  maxAge: o?.maxAge ?? 3600,
14
- staleWhileRevalidate: o?.staleWhileRevalidate ?? 82800
23
+ staleWhileRevalidate: o?.staleWhileRevalidate ?? 82800,
24
+ staleIfError: o?.staleIfError ?? DEFAULT_STALE_IF_ERROR_SECONDS
15
25
  });
16
26
  var CacheCustom = (s) => ({
17
27
  mode: "custom",
28
+ staleIfError: DEFAULT_STALE_IF_ERROR_SECONDS,
18
29
  ...s
19
30
  });
20
31
  var StorefrontDefaultCache = () => ({
21
32
  mode: "custom",
22
33
  maxAge: 1,
23
- staleWhileRevalidate: 86399
34
+ staleWhileRevalidate: 86399,
35
+ staleIfError: DEFAULT_STALE_IF_ERROR_SECONDS
24
36
  });
25
37
  function generateCacheControlHeader(strategy) {
26
38
  if (strategy.mode === "none") return "no-store";
27
- return `public, max-age=${String(strategy.maxAge)}, stale-while-revalidate=${String(strategy.staleWhileRevalidate)}`;
39
+ const staleIfError = strategy.staleIfError ?? 0;
40
+ const staleIfErrorDirective = staleIfError > 0 ? `, stale-if-error=${String(staleIfError)}` : "";
41
+ return `public, max-age=${String(strategy.maxAge)}, stale-while-revalidate=${String(strategy.staleWhileRevalidate)}${staleIfErrorDirective}`;
28
42
  }
29
43
  var requestFor = (key) => new Request(`https://kotao.cache/${encodeURIComponent(key)}`);
30
44
  function createCache(opts) {
@@ -41,7 +55,7 @@ function createCache(opts) {
41
55
  };
42
56
  const write = async (key, value, strategy) => {
43
57
  const body = { value, storedAt: now() };
44
- const ttl = strategy.maxAge + strategy.staleWhileRevalidate;
58
+ const ttl = strategy.maxAge + strategy.staleWhileRevalidate + (strategy.staleIfError ?? 0);
45
59
  await cache.put(
46
60
  requestFor(key),
47
61
  new Response(JSON.stringify(body), {
@@ -51,7 +65,7 @@ function createCache(opts) {
51
65
  };
52
66
  return {
53
67
  raw: cache,
54
- async run(key, strategy, fn) {
68
+ async run(key, strategy, fn, onStaleIfError) {
55
69
  if (strategy.mode === "none") return fn();
56
70
  const stored = await read(key);
57
71
  if (stored) {
@@ -62,10 +76,23 @@ function createCache(opts) {
62
76
  (async () => {
63
77
  const fresh = await fn();
64
78
  await write(key, fresh, strategy);
65
- })()
79
+ })().catch(() => void 0)
66
80
  );
67
81
  return stored.value;
68
82
  }
83
+ if (ageSec <= strategy.maxAge + strategy.staleWhileRevalidate + (strategy.staleIfError ?? 0)) {
84
+ try {
85
+ const value2 = await fn();
86
+ opts.waitUntil(write(key, value2, strategy));
87
+ return value2;
88
+ } catch (error) {
89
+ try {
90
+ onStaleIfError?.({ ageSeconds: ageSec, error });
91
+ } catch {
92
+ }
93
+ return stored.value;
94
+ }
95
+ }
69
96
  }
70
97
  const value = await fn();
71
98
  opts.waitUntil(write(key, value, strategy));
@@ -609,7 +636,8 @@ function createCartHandler(opts) {
609
636
  const id = await currentCartId();
610
637
  if (!id) return null;
611
638
  const data = await opts.storefront.query(CART_QUERY, {
612
- variables: { cartId: id }
639
+ variables: { cartId: id },
640
+ cache: CacheNone()
613
641
  });
614
642
  return withCheckoutUrl(data.cart);
615
643
  },
@@ -707,6 +735,12 @@ function appendSetCartIdHeaders(headers, value) {
707
735
  // src/customer-account/session.ts
708
736
  var CUSTOMER_SESSION_COOKIE = "kotao_customer_session";
709
737
  var DEFAULT_SESSION_MAX_AGE = 60 * 60 * 24 * 30;
738
+ var HOSTED_CUSTOMER_SESSION_COOKIE = "__Host-kotao_customer_session";
739
+ var HOSTED_CUSTOMER_CSRF_COOKIE = "__Host-kotao_customer_csrf";
740
+ var HOSTED_CUSTOMER_CSRF_HEADER = "x-kotao-csrf-token";
741
+ var MAX_HOSTED_SESSION_MAX_AGE = 60 * 60 * 24 * 365;
742
+ var DEFAULT_HOSTED_SESSION_MAX_AGE = MAX_HOSTED_SESSION_MAX_AGE;
743
+ var MAX_LEGACY_CUSTOMER_SESSION_TOKEN_LENGTH = 4096;
710
744
  var assertHostname = (hostname) => {
711
745
  if (hostname === "" || hostname.startsWith(".") || hostname.endsWith(".")) {
712
746
  throw new Error(`invalid shop hostname for the customer session cookie: '${hostname}'`);
@@ -736,6 +770,48 @@ function readCustomerSessionToken(cookieHeader) {
736
770
  return null;
737
771
  }
738
772
  }
773
+ function buildHostedCustomerSessionCookie(token, attrs = {}) {
774
+ if (token === "") throw new Error("hosted customer session token must not be empty");
775
+ const maxAge = attrs.maxAgeSeconds ?? DEFAULT_HOSTED_SESSION_MAX_AGE;
776
+ if (!Number.isInteger(maxAge) || maxAge <= 0 || maxAge > MAX_HOSTED_SESSION_MAX_AGE) {
777
+ throw new Error("hosted customer session Max-Age must be between 1 second and 365 days");
778
+ }
779
+ return `${HOSTED_CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(token)}; Path=/; Max-Age=${maxAge}; HttpOnly; Secure; SameSite=Lax`;
780
+ }
781
+ function clearHostedCustomerSessionCookie() {
782
+ return `${HOSTED_CUSTOMER_SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`;
783
+ }
784
+ function readHostedCustomerSessionToken(cookieHeader) {
785
+ return readBoundedCookie(cookieHeader, HOSTED_CUSTOMER_SESSION_COOKIE);
786
+ }
787
+ function readHostedCustomerCsrfToken(cookieHeader) {
788
+ const token = readBoundedCookie(cookieHeader, HOSTED_CUSTOMER_CSRF_COOKIE, 43);
789
+ return token !== null && /^[A-Za-z0-9_-]{43}$/.test(token) ? token : null;
790
+ }
791
+ function readLegacyCustomerSessionToken(cookieHeader, options) {
792
+ if (!options.allowLegacy) return null;
793
+ return readBoundedCookie(
794
+ cookieHeader,
795
+ CUSTOMER_SESSION_COOKIE,
796
+ MAX_LEGACY_CUSTOMER_SESSION_TOKEN_LENGTH
797
+ );
798
+ }
799
+ function clearLegacyCustomerSessionCookie(attrs) {
800
+ return clearCustomerSessionCookie(attrs);
801
+ }
802
+ function readBoundedCookie(cookieHeader, name, maxLength = MAX_LEGACY_CUSTOMER_SESSION_TOKEN_LENGTH) {
803
+ if (cookieHeader === null || cookieHeader === void 0 || cookieHeader === "") return null;
804
+ const match = cookieHeader.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`));
805
+ if (match === void 0) return null;
806
+ const raw = match.slice(name.length + 1);
807
+ if (raw === "" || raw.length > maxLength) return null;
808
+ try {
809
+ const decoded = decodeURIComponent(raw);
810
+ return decoded.length <= maxLength ? decoded : null;
811
+ } catch {
812
+ return null;
813
+ }
814
+ }
739
815
 
740
816
  // src/customer-account/types.ts
741
817
  var CustomerAccountError = class extends Error {
@@ -760,7 +836,11 @@ function createCustomerAccountClient(opts) {
760
836
  }
761
837
  const doFetch = opts.fetch ?? fetch;
762
838
  const cookieHeader = opts.request.headers.get("cookie");
763
- const token = readCustomerSessionToken(cookieHeader);
839
+ const hostedToken = readHostedCustomerSessionToken(cookieHeader);
840
+ const legacyToken = hostedToken === null && (opts.allowLegacySessionForwarding ?? true) ? readCustomerSessionToken(cookieHeader) : null;
841
+ const token = hostedToken ?? legacyToken;
842
+ const sessionCookie = hostedToken !== null ? `${HOSTED_CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(hostedToken)}` : legacyToken !== null ? `${CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(legacyToken)}` : null;
843
+ const configuredShopOrigin = configuredHttpsOrigin(opts.shopOrigin);
764
844
  const hydrogenCustomerAccountId = opts.customerAccountId?.trim();
765
845
  const hydrogenShopId = opts.shopId?.trim();
766
846
  const hasHydrogenCustomerAccount = hydrogenCustomerAccountId !== void 0 && hydrogenCustomerAccountId.length > 0 && hydrogenShopId !== void 0 && hydrogenShopId.length > 0;
@@ -768,7 +848,8 @@ function createCustomerAccountClient(opts) {
768
848
  const proxyGet = async (path) => {
769
849
  try {
770
850
  return await doFetch(`${proxyBase}${path}`, {
771
- headers: token !== null ? { cookie: `${CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(token)}` } : {}
851
+ redirect: "manual",
852
+ headers: sessionCookie !== null ? { cookie: sessionCookie } : {}
772
853
  });
773
854
  } catch (cause) {
774
855
  throw new CustomerAccountError(
@@ -791,21 +872,45 @@ function createCustomerAccountClient(opts) {
791
872
  }
792
873
  };
793
874
  const proxyPost = async (path, body) => {
875
+ const csrf = await csrfHeaders();
876
+ if (csrf === null) return new Response(null, { status: 503 });
794
877
  return doFetch(`${proxyBase}${path}`, {
795
878
  method: "POST",
796
- headers: { "content-type": "application/json" },
879
+ redirect: "manual",
880
+ headers: { "content-type": "application/json", ...csrf.headers },
797
881
  body: JSON.stringify(body)
798
882
  });
799
883
  };
800
884
  const proxyWrite = async (path, method, body) => {
801
- const headers = { "content-type": "application/json" };
802
- if (token !== null) headers.cookie = `${CUSTOMER_SESSION_COOKIE}=${encodeURIComponent(token)}`;
885
+ const csrf = await csrfHeaders();
886
+ if (csrf === null) return new Response(null, { status: 503 });
887
+ const headers = { "content-type": "application/json", ...csrf.headers };
803
888
  return doFetch(`${proxyBase}${path}`, {
804
889
  method,
890
+ redirect: "manual",
805
891
  headers,
806
892
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
807
893
  });
808
894
  };
895
+ const csrfHeaders = async () => {
896
+ if (configuredShopOrigin === null) return null;
897
+ let csrfToken = readHostedCustomerCsrfToken(cookieHeader);
898
+ if (csrfToken === null) {
899
+ const bootstrap = await doFetch(`${proxyBase}/account/csrf`, {
900
+ redirect: "manual",
901
+ headers: sessionCookie !== null ? { cookie: sessionCookie } : {}
902
+ });
903
+ csrfToken = csrfTokenFromSetCookie(bootstrap.headers);
904
+ if (csrfToken === null) return null;
905
+ }
906
+ return {
907
+ headers: {
908
+ Origin: configuredShopOrigin,
909
+ [HOSTED_CUSTOMER_CSRF_HEADER]: csrfToken,
910
+ cookie: [sessionCookie, `${HOSTED_CUSTOMER_CSRF_COOKIE}=${csrfToken}`].filter((value) => value !== null).join("; ")
911
+ }
912
+ };
913
+ };
809
914
  const returnTo = () => {
810
915
  const url = new URL(opts.request.url);
811
916
  return `${url.pathname}${url.search}`;
@@ -1045,11 +1150,11 @@ function createCustomerAccountClient(opts) {
1045
1150
  async requestLoginCode(input) {
1046
1151
  try {
1047
1152
  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" };
1153
+ if (res.status === 200) return { status: "sent", response: res };
1154
+ if (res.status === 429) return { status: "rate_limited", response: res };
1155
+ return { status: "error", response: res };
1051
1156
  } catch {
1052
- return { status: "error" };
1157
+ return { status: "error", response: new Response(null, { status: 503 }) };
1053
1158
  }
1054
1159
  },
1055
1160
  async verifyLoginCode(input) {
@@ -1082,18 +1187,23 @@ function createCustomerAccountClient(opts) {
1082
1187
  await commitHydrogenSession(headers);
1083
1188
  }
1084
1189
  } else {
1085
- headers.set("location", `${proxyBase}/account/logout`);
1190
+ try {
1191
+ return await proxyWrite("/account/logout", "POST");
1192
+ } catch {
1193
+ headers.set("location", "/");
1194
+ }
1086
1195
  }
1087
1196
  headers.append(
1088
1197
  "set-cookie",
1089
1198
  clearCustomerSessionCookie({ hostname: shopHostname(opts.request) })
1090
1199
  );
1200
+ headers.append("set-cookie", clearHostedCustomerSessionCookie());
1091
1201
  return new Response(null, { status: 302, headers });
1092
1202
  },
1093
1203
  async getAccessToken() {
1094
1204
  const directAccessToken = await hydrogenAccessToken();
1095
1205
  if (directAccessToken !== null) return directAccessToken;
1096
- return token;
1206
+ return hasHydrogenCustomerAccount ? token : null;
1097
1207
  },
1098
1208
  async isLoggedIn() {
1099
1209
  if (await hydrogenAccessToken() !== null) return true;
@@ -1139,6 +1249,26 @@ function randomOAuthToken() {
1139
1249
  for (const byte of bytes) raw += String.fromCharCode(byte);
1140
1250
  return base64UrlEncode(raw);
1141
1251
  }
1252
+ function configuredHttpsOrigin(value) {
1253
+ if (!value) return null;
1254
+ try {
1255
+ const candidate = value.includes("://") ? value : `https://${value}`;
1256
+ const url = new URL(candidate);
1257
+ if (url.protocol !== "https:" || url.username || url.password || url.port || url.pathname !== "/")
1258
+ return null;
1259
+ return url.origin;
1260
+ } catch {
1261
+ return null;
1262
+ }
1263
+ }
1264
+ function csrfTokenFromSetCookie(headers) {
1265
+ const values = "getSetCookie" in headers && typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [headers.get("set-cookie") ?? ""];
1266
+ for (const value of values) {
1267
+ const match = value.match(/(?:^|,\s*)__Host-kotao_customer_csrf=([A-Za-z0-9_-]{43})(?:;|$)/);
1268
+ if (match?.[1]) return match[1];
1269
+ }
1270
+ return null;
1271
+ }
1142
1272
  function isHydrogenSessionLike(value) {
1143
1273
  return value !== null && typeof value === "object" && "get" in value && "set" in value && typeof value.get === "function" && typeof value.set === "function";
1144
1274
  }
@@ -1354,9 +1484,32 @@ function localizePath(path, locale, options = {}) {
1354
1484
  }
1355
1485
 
1356
1486
  // src/storefront-client/client.ts
1487
+ var StorefrontQueryTimeoutError = class extends Error {
1488
+ operationName;
1489
+ timeoutMs;
1490
+ constructor(operationName, timeoutMs) {
1491
+ super(`Storefront query ${operationName} timed out after ${String(timeoutMs)}ms`);
1492
+ this.name = "StorefrontQueryTimeoutError";
1493
+ this.operationName = operationName;
1494
+ this.timeoutMs = timeoutMs;
1495
+ }
1496
+ };
1497
+ var StorefrontUpstreamError = class extends Error {
1498
+ operationName;
1499
+ status;
1500
+ cause;
1501
+ constructor(operationName, message, options) {
1502
+ super(message);
1503
+ this.name = "StorefrontUpstreamError";
1504
+ this.operationName = operationName;
1505
+ this.status = options?.status;
1506
+ this.cause = options?.cause;
1507
+ }
1508
+ };
1357
1509
  var hashKey = (query, variables) => `sf:${query.replace(/\s+/g, " ").trim()}:${variables ? JSON.stringify(variables) : ""}`;
1358
1510
  var COUNTRY_RE = /^[A-Z]{2}$/;
1359
1511
  var DEFAULT_HYDROGEN_STOREFRONT_API_VERSION = "2025-01";
1512
+ var DEFAULT_QUERY_TIMEOUT_MS = 5e3;
1360
1513
  function endpointForStorefrontApiVersion(endpoint, version) {
1361
1514
  if (version === void 0 || version.length === 0) return endpoint;
1362
1515
  try {
@@ -1381,6 +1534,7 @@ function createStorefrontClient(opts) {
1381
1534
  cache: createCache({ cache: opts.cache, waitUntil: opts.waitUntil }),
1382
1535
  fetch: opts.fetch,
1383
1536
  defaultStrategy: opts.defaultStrategy,
1537
+ queryTimeoutMs: opts.queryTimeoutMs,
1384
1538
  extraHeaders: opts.storefrontHeaders,
1385
1539
  i18n
1386
1540
  });
@@ -1391,11 +1545,13 @@ function createStorefrontClient(opts) {
1391
1545
  function createStorefrontClientCore(opts) {
1392
1546
  const doFetch = opts.fetch ?? fetch;
1393
1547
  const defaultStrategy = opts.defaultStrategy ?? StorefrontDefaultCache();
1548
+ const defaultQueryTimeoutMs = opts.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
1394
1549
  const tokenHeader = opts.tokenHeader ?? "x-kotao-storefront-token";
1395
1550
  const localeKey = opts.locale !== void 0 && opts.locale.length > 0 ? opts.locale : "";
1396
1551
  const countryKey = opts.country !== void 0 && COUNTRY_RE.test(opts.country) ? opts.country : "";
1397
1552
  const i18n = normalizeStorefrontI18n(opts.i18n, opts.locale, countryKey);
1398
- const execute = async (document, options) => {
1553
+ const execute = async (document, options, signal, timing) => {
1554
+ const operationStartedAt = timing === void 0 ? 0 : Date.now();
1399
1555
  const endpoint = endpointForStorefrontApiVersion(opts.endpoint, options?.storefrontApiVersion);
1400
1556
  const headers = {
1401
1557
  "content-type": "application/json",
@@ -1412,13 +1568,67 @@ function createStorefrontClientCore(opts) {
1412
1568
  if (countryKey.length > 0) {
1413
1569
  headers["x-kotao-country"] = countryKey;
1414
1570
  }
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();
1571
+ const operationName = operationNameFromDocument(document);
1572
+ let res;
1573
+ try {
1574
+ res = await doFetch(endpoint, {
1575
+ method: "POST",
1576
+ headers,
1577
+ body: JSON.stringify({ query: document, variables: options?.variables }),
1578
+ ...signal === void 0 ? {} : { signal }
1579
+ });
1580
+ } catch (cause) {
1581
+ if (signal?.aborted === true) {
1582
+ const timeoutMs = options?.timeoutMs ?? defaultQueryTimeoutMs;
1583
+ console.error(
1584
+ JSON.stringify({
1585
+ event: "storefront_query_failed",
1586
+ failure: "timeout",
1587
+ operationName,
1588
+ timeoutMs
1589
+ })
1590
+ );
1591
+ throw new StorefrontQueryTimeoutError(operationName, timeoutMs);
1592
+ }
1593
+ console.error(
1594
+ JSON.stringify({
1595
+ event: "storefront_query_failed",
1596
+ failure: "transport",
1597
+ operationName
1598
+ })
1599
+ );
1600
+ throw new StorefrontUpstreamError(operationName, "Storefront API request failed", { cause });
1601
+ }
1602
+ const opName = operationName;
1603
+ let raw;
1604
+ try {
1605
+ raw = await res.text();
1606
+ } catch (cause) {
1607
+ if (signal?.aborted === true) {
1608
+ const timeoutMs = options?.timeoutMs ?? defaultQueryTimeoutMs;
1609
+ console.error(
1610
+ JSON.stringify({
1611
+ event: "storefront_query_failed",
1612
+ failure: "timeout",
1613
+ operationName,
1614
+ timeoutMs
1615
+ })
1616
+ );
1617
+ throw new StorefrontQueryTimeoutError(operationName, timeoutMs);
1618
+ }
1619
+ console.error(
1620
+ JSON.stringify({
1621
+ event: "storefront_query_failed",
1622
+ failure: "response_body",
1623
+ operationName,
1624
+ status: res.status
1625
+ })
1626
+ );
1627
+ throw new StorefrontUpstreamError(operationName, "Storefront API response body failed", {
1628
+ status: res.status,
1629
+ cause
1630
+ });
1631
+ }
1422
1632
  let body;
1423
1633
  try {
1424
1634
  body = JSON.parse(raw);
@@ -1426,13 +1636,22 @@ function createStorefrontClientCore(opts) {
1426
1636
  console.error(
1427
1637
  `[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
1638
  );
1429
- throw new Error(`Storefront API returned a non-JSON response (status ${res.status})`);
1639
+ throw new StorefrontUpstreamError(
1640
+ operationName,
1641
+ `Storefront API returned a non-JSON response (status ${String(res.status)})`,
1642
+ { status: res.status }
1643
+ );
1430
1644
  }
1431
1645
  if (!res.ok) {
1432
1646
  const shopCtx = opts.signedShopContextHeaders !== void 0 ? "present" : "absent";
1433
1647
  console.error(
1434
1648
  `[storefront] /api/storefront HTTP ${res.status} endpoint=${endpoint} shopCtx=${shopCtx} op="${opName}" body=${raw.slice(0, 180)}`
1435
1649
  );
1650
+ throw new StorefrontUpstreamError(
1651
+ operationName,
1652
+ `Storefront API returned HTTP ${String(res.status)}`,
1653
+ { status: res.status }
1654
+ );
1436
1655
  }
1437
1656
  if (body.errors?.length) {
1438
1657
  console.error(`[storefront] GraphQL query failed: ${JSON.stringify(body.errors)}`);
@@ -1444,6 +1663,19 @@ function createStorefrontClientCore(opts) {
1444
1663
  );
1445
1664
  throw new Error("Storefront API returned no data");
1446
1665
  }
1666
+ if (timing !== void 0) {
1667
+ console.info(
1668
+ JSON.stringify({
1669
+ event: "storefront_operation_timing",
1670
+ operationName,
1671
+ wallTimeMs: Math.max(0, Date.now() - operationStartedAt),
1672
+ cacheStatus: timing.cacheStatus,
1673
+ queryCount: 1,
1674
+ payloadBytes: raw.length,
1675
+ ...timing.timeoutMs !== void 0 ? { timeoutMs: timing.timeoutMs } : {}
1676
+ })
1677
+ );
1678
+ }
1447
1679
  return body.data;
1448
1680
  };
1449
1681
  return {
@@ -1456,10 +1688,27 @@ function createStorefrontClientCore(opts) {
1456
1688
  query(query, options) {
1457
1689
  const strategy = options?.cache ?? defaultStrategy;
1458
1690
  const versionKey = options?.storefrontApiVersion ?? "";
1691
+ const operationName = operationNameFromDocument(query);
1459
1692
  return opts.cache.run(
1460
1693
  `${localeKey}:${countryKey}:${versionKey}:${hashKey(query, options?.variables)}`,
1461
1694
  strategy,
1462
- () => execute(query, options)
1695
+ () => {
1696
+ const timeoutMs = options?.timeoutMs ?? defaultQueryTimeoutMs;
1697
+ return execute(query, { ...options, timeoutMs }, AbortSignal.timeout(timeoutMs), {
1698
+ cacheStatus: "miss",
1699
+ timeoutMs
1700
+ });
1701
+ },
1702
+ ({ ageSeconds, error }) => {
1703
+ console.error(
1704
+ JSON.stringify({
1705
+ event: "storefront_query_cache_fallback",
1706
+ operationName,
1707
+ staleAgeSeconds: ageSeconds,
1708
+ failure: storefrontFailureKind(error)
1709
+ })
1710
+ );
1711
+ }
1463
1712
  );
1464
1713
  },
1465
1714
  mutate(mutation2, options) {
@@ -1467,6 +1716,14 @@ function createStorefrontClientCore(opts) {
1467
1716
  }
1468
1717
  };
1469
1718
  }
1719
+ function operationNameFromDocument(document) {
1720
+ return /\b(?:query|mutation)\s+([_A-Za-z][_0-9A-Za-z]*)/u.exec(document)?.[1] ?? "anonymous";
1721
+ }
1722
+ function storefrontFailureKind(error) {
1723
+ if (error instanceof StorefrontQueryTimeoutError) return "timeout";
1724
+ if (error instanceof StorefrontUpstreamError) return "upstream";
1725
+ return "unknown";
1726
+ }
1470
1727
  function isHydrogenStorefrontClientOptions(opts) {
1471
1728
  return "storeDomain" in opts && "publicStorefrontToken" in opts && "waitUntil" in opts;
1472
1729
  }
@@ -1476,7 +1733,7 @@ function attachHydrogenResult(client) {
1476
1733
  function headersRecord(headers) {
1477
1734
  if (headers === void 0) return {};
1478
1735
  const result = {};
1479
- new Headers(headers).forEach((value, key) => {
1736
+ normalizeStorefrontHeaders(headers).forEach((value, key) => {
1480
1737
  result[key] = value;
1481
1738
  });
1482
1739
  return result;
@@ -1547,6 +1804,24 @@ async function localSignedShopContextHeaders(key, siteId, hostname) {
1547
1804
  }
1548
1805
  var CACHE_NAME = "kotao-storefront-v1";
1549
1806
  var CACHE_VERSION_HEADER = "x-kotao-cache-version";
1807
+ var REQUEST_ID_HEADER = "x-kotao-request-id";
1808
+ var DEPLOYMENT_ID_HEADER = "x-kotao-deployment-id";
1809
+ function trustedShopOrigin(value) {
1810
+ if (!value) return void 0;
1811
+ try {
1812
+ const url = new URL(value);
1813
+ if (url.protocol !== "https:" || url.username || url.password || url.port || url.pathname !== "/" || url.search || url.hash) {
1814
+ return void 0;
1815
+ }
1816
+ return url.origin;
1817
+ } catch {
1818
+ return void 0;
1819
+ }
1820
+ }
1821
+ function trustedShopOriginFromHeaders(headers) {
1822
+ if (!headers.has(SHOP_HEADER) || !headers.has(SHOP_SIGNATURE_HEADER)) return void 0;
1823
+ return trustedShopOrigin(headers.get(SHOP_ORIGIN_HEADER));
1824
+ }
1550
1825
  var API_LOCALE_RE = /^[a-z]{2,3}(-[a-zA-Z]{2,4})?$/;
1551
1826
  var API_COUNTRY_RE = /^[A-Z]{2}$/;
1552
1827
  var SAME_ORIGIN_PATH2 = /^\/(?![/\\])/;
@@ -1596,12 +1871,21 @@ async function createKotaoContext(request, env, executionCtx, deps) {
1596
1871
  const pathLocale = parseLocale(request);
1597
1872
  const localizationCookie = readLocalizationCookie(request.headers.get("cookie"));
1598
1873
  const cookieLocale = localizationCookie.locale;
1599
- const browserLocale = localeFromAcceptLanguage(request.headers.get("accept-language"));
1874
+ const autoLanguage = autoRedirectEnabled(request.headers, AUTO_REDIRECT_LANGUAGE_HEADER);
1875
+ const autoCountry = autoRedirectEnabled(request.headers, AUTO_REDIRECT_COUNTRY_HEADER);
1876
+ const browserLocale = autoLanguage ? localeFromAcceptLanguage(request.headers.get("accept-language")) : null;
1600
1877
  const selectedLocale = pathLocale ?? cookieLocale ?? browserLocale?.split("-")[0] ?? null;
1601
1878
  const apiLocale = selectedLocale !== null && API_LOCALE_RE.test(selectedLocale) ? selectedLocale : void 0;
1602
1879
  const cookieCountry = localizationCookie.country !== null && API_COUNTRY_RE.test(localizationCookie.country) ? localizationCookie.country : null;
1603
- const geoCountry = geo?.country?.trim().toUpperCase() ?? null;
1880
+ const geoCountry = autoCountry ? geo?.country?.trim().toUpperCase() ?? null : null;
1604
1881
  const apiCountry = cookieCountry ?? (geoCountry !== null && API_COUNTRY_RE.test(geoCountry) ? geoCountry : null);
1882
+ const extraHeaders = {};
1883
+ const requestId = request.headers.get(REQUEST_ID_HEADER)?.trim();
1884
+ if (requestId !== void 0 && requestId.length > 0) extraHeaders[REQUEST_ID_HEADER] = requestId;
1885
+ const deploymentId = request.headers.get(DEPLOYMENT_ID_HEADER)?.trim();
1886
+ if (deploymentId !== void 0 && deploymentId.length > 0) {
1887
+ extraHeaders[DEPLOYMENT_ID_HEADER] = deploymentId;
1888
+ }
1605
1889
  const storefront = createStorefrontClient({
1606
1890
  endpoint: resolvedEnv.STOREFRONT_API_ENDPOINT,
1607
1891
  token: resolvedEnv.STOREFRONT_API_TOKEN,
@@ -1610,21 +1894,34 @@ async function createKotaoContext(request, env, executionCtx, deps) {
1610
1894
  ...signedShopContextHeaders ? { signedShopContextHeaders } : {},
1611
1895
  ...apiLocale !== void 0 ? { locale: apiLocale } : {},
1612
1896
  ...apiCountry !== null ? { country: apiCountry } : {},
1897
+ ...Object.keys(extraHeaders).length > 0 ? { extraHeaders } : {},
1613
1898
  cache
1614
1899
  });
1900
+ const eventHeaders = new Headers(signedShopContextHeaders);
1901
+ if (apiLocale !== void 0) eventHeaders.set("x-kotao-locale", apiLocale);
1902
+ if (apiCountry !== null) eventHeaders.set("x-kotao-country", apiCountry);
1903
+ for (const [name, value] of Object.entries(extraHeaders)) eventHeaders.set(name, value);
1904
+ const events = createEventsClient({
1905
+ baseUrl: resolvedEnv.STOREFRONT_API_ENDPOINT,
1906
+ fetch,
1907
+ defaultHeaders: eventHeaders
1908
+ });
1615
1909
  const cart = createCartHandler({ request, storefront });
1616
1910
  const contextSession = withSessionPresenceHint(
1617
1911
  deps?.session,
1618
- readCustomerSessionToken(request.headers.get("cookie")) !== null
1912
+ readHostedCustomerSessionToken(request.headers.get("cookie")) !== null || readCustomerSessionToken(request.headers.get("cookie")) !== null
1619
1913
  );
1620
1914
  const customerAccount = createCustomerAccountClient({
1621
1915
  request,
1622
1916
  customerAccountId: resolvedEnv.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID,
1623
1917
  shopId: resolvedEnv.SHOP_ID,
1624
- session: contextSession
1918
+ session: contextSession,
1919
+ shopOrigin: trustedShopOriginFromHeaders(request.headers) ?? (inboundShop === null && inboundShopSignature === null ? trustedShopOrigin(resolvedEnv.STOREFRONT_SHOP_ORIGIN) : void 0)
1625
1920
  });
1921
+ const settingsEndpoint = `${resolvedEnv.STOREFRONT_API_ENDPOINT.replace(/\/+$/, "")}/settings`;
1626
1922
  return {
1627
1923
  storefront,
1924
+ events,
1628
1925
  cart,
1629
1926
  customerAccount,
1630
1927
  geo,
@@ -1633,7 +1930,13 @@ async function createKotaoContext(request, env, executionCtx, deps) {
1633
1930
  // Cookie-presence hint only — the theme cannot validate the HMAC; the authoritative check is the proxy.
1634
1931
  session: contextSession,
1635
1932
  env: resolvedEnv,
1636
- waitUntil
1933
+ waitUntil,
1934
+ loadThemeSettings: (opts) => fetchThemeSettings({
1935
+ endpoint: settingsEndpoint,
1936
+ allowCrossOrigin: true,
1937
+ ...signedShopContextHeaders ? { shopContextHeaders: signedShopContextHeaders } : {},
1938
+ ...opts?.fetch ? { fetch: opts.fetch } : {}
1939
+ })
1637
1940
  };
1638
1941
  }
1639
1942
  function withSessionPresenceHint(session, isLoggedIn) {
@@ -1685,6 +1988,6 @@ function createEphemeralSession(isLoggedIn) {
1685
1988
  };
1686
1989
  }
1687
1990
 
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
1991
+ export { CART_ID_COOKIE, CHECKOUT_CONSENT_CATEGORIES, CHECKOUT_CONSENT_PARAM_PREFIX, CHECKOUT_DESIGN_TOKEN_INPUTS, CUSTOMER_SESSION_COOKIE, CacheCustom, CacheLong, CacheNone, CacheShort, CustomerAccountError, DEFAULT_HOSTED_SESSION_MAX_AGE, DEFAULT_SESSION_MAX_AGE, GEO_HEADER, GEO_SIGNATURE_HEADER, HOSTED_CUSTOMER_CSRF_COOKIE, HOSTED_CUSTOMER_CSRF_HEADER, HOSTED_CUSTOMER_SESSION_COOKIE, LOCALIZATION_COOKIE, MAX_HOSTED_SESSION_MAX_AGE, MAX_LEGACY_CUSTOMER_SESSION_TOKEN_LENGTH, ROUTING_LOCALE_HEADER, ROUTING_ORIGINAL_PATHNAME_HEADER, StorefrontDefaultCache, StorefrontQueryTimeoutError, StorefrontUpstreamError, buildCartIdCookie, buildCustomerSessionCookie, buildHostedCustomerSessionCookie, buildLocalizationCookie, cartGetIdDefault, cartSetIdDefault, clearCustomerSessionCookie, clearHostedCustomerSessionCookie, clearLegacyCustomerSessionCookie, createCache, createCartHandler, createCustomerAccountClient, createKotaoContext, createStorefrontClient, createWithCache, decodeCheckoutConsent, encodeCheckoutConsent, extractCheckoutDesignTokens, generateCacheControlHeader, isLocalePathSegment, localeFromAcceptLanguage, localeFromPathname, localeFromRequest, localizePath, originalPathnameFromRequest, readCartIdCookie, readCustomerSessionToken, readHostedCustomerCsrfToken, readHostedCustomerSessionToken, readLegacyCustomerSessionToken, readLocalizationCookie, resolveCheckoutUrl, shopHostname, signGeo, stripLocalePrefixFromPathname, stripLocalePrefixFromRequest, trustedShopOriginFromHeaders, verifyGeo };
1992
+ //# sourceMappingURL=chunk-4TEFGD42.js.map
1993
+ //# sourceMappingURL=chunk-4TEFGD42.js.map