@kotao/storefront 0.2.9 → 0.2.10

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.
@@ -1,4 +1,4 @@
1
- import { CART_FORM_ACTIONS } from './chunk-OGWYTYYS.js';
1
+ import { CART_FORM_ACTIONS } from './chunk-GR4QJ7EC.js';
2
2
  import * as ReactRuntime from 'react';
3
3
  import { createContext, createElement, useContext } from 'react';
4
4
 
@@ -162,5 +162,5 @@ function createPixelTransport(options) {
162
162
  }
163
163
 
164
164
  export { CartForm, collectAnalyticsAttribution, createAnalyticsBus, createAnalyticsEmitter, createNonceProvider, createPixelTransport, getCartFormInput, useNonce };
165
- //# sourceMappingURL=chunk-S4HW5NH7.js.map
166
- //# sourceMappingURL=chunk-S4HW5NH7.js.map
165
+ //# sourceMappingURL=chunk-BV4OLXV4.js.map
166
+ //# sourceMappingURL=chunk-BV4OLXV4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cart/CartForm.ts","../src/react/nonce.ts","../src/analytics/attribution.ts","../src/analytics/emitter.ts","../src/analytics/analytics-bus.ts","../src/analytics/pixel-transport.ts"],"names":["CartForm"],"mappings":"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","file":"chunk-S4HW5NH7.js","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { CART_FORM_ACTIONS, type CartFormAction } from './actions'\n\nexport interface CartFormProps {\n readonly action: CartFormAction\n}\n\nexport interface CartFormInputResult {\n readonly action: CartFormAction | null\n readonly inputs: Record<string, any>\n}\n\nfunction parseFormValue(value: unknown): unknown {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n return JSON.parse(trimmed) as unknown\n } catch {\n return value\n }\n }\n return value\n}\n\nexport function getCartFormInput(formData: FormData): CartFormInputResult {\n const action = formData.get('cartAction')\n const inputs: Record<string, any> = {}\n for (const key of new Set(formData.keys())) {\n if (key === 'cartAction') continue\n const values = formData.getAll(key).map(parseFormValue)\n inputs[key] = values.length === 1 ? values[0] : values\n }\n return {\n action: typeof action === 'string' ? (action as CartFormAction) : null,\n inputs,\n }\n}\n\n/**\n * Lightweight CartForm surface marker for Hydrogen-style imports. React render integration can wrap\n * this constant surface; the load-bearing API today is the stable ACTIONS contract used by handlers.\n */\nexport const CartForm = Object.assign(\n function CartForm(_props: CartFormProps): null {\n return null\n },\n { ACTIONS: CART_FORM_ACTIONS, getFormInput: getCartFormInput },\n)\n","import * as ReactRuntime from 'react'\nimport { createContext, createElement, useContext, type ReactNode } from 'react'\n\nconst NonceContext = createContext<string | undefined>(undefined)\n\nexport interface NonceProviderProps {\n readonly children?: ReactNode\n readonly nonce?: string\n}\n\nexport type BoundNonceProvider = (props: { readonly children?: ReactNode }) => ReactNode\n\nexport function NonceProvider(props: NonceProviderProps): ReactNode {\n return createElement(NonceContext.Provider, { value: props.nonce }, props.children)\n}\n\nexport function createNonceProvider(nonce: string): BoundNonceProvider {\n return function BoundNonceProvider(props: { readonly children?: ReactNode }): ReactNode {\n return createElement(NonceContext.Provider, { value: nonce }, props.children)\n }\n}\n\n/** Hydrogen-compatible nonce hook. */\nexport function useNonce(): string | undefined {\n if (!hasReactDispatcher()) return undefined\n return useContext(NonceContext)\n}\n\nfunction hasReactDispatcher(): boolean {\n const runtime = ReactRuntime as unknown as {\n __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?: { H?: unknown }\n __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: {\n ReactCurrentDispatcher?: { current?: unknown }\n }\n }\n return Boolean(\n runtime.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.H ??\n runtime.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentDispatcher?.current,\n )\n}\n","import type { AnalyticsContext, AnalyticsDeviceClass } from './events'\n\nexport interface AnalyticsAttributionEnvironment {\n readonly referrer?: string\n readonly search?: string\n readonly matchMedia?: (query: string) => { readonly matches: boolean }\n}\n\ntype AttributionContext = Pick<\n AnalyticsContext,\n 'referrer' | 'utmSource' | 'utmMedium' | 'utmCampaign' | 'deviceClass'\n>\n\nfunction deviceClass(\n matchMedia: AnalyticsAttributionEnvironment['matchMedia'],\n): AnalyticsDeviceClass | undefined {\n if (matchMedia === undefined) return undefined\n if (matchMedia('(max-width: 767px)').matches) return 'mobile'\n if (matchMedia('(max-width: 1023px)').matches) return 'tablet'\n return 'desktop'\n}\n\n/** Read static landing attribution plus the current responsive device class without retaining state\n * or identifiers. All values stay optional so non-DOM/SSR emitters keep their existing payload. */\nexport function collectAnalyticsAttribution(\n environment: AnalyticsAttributionEnvironment | null,\n): AttributionContext {\n if (environment === null) return {}\n const params = new URLSearchParams(environment.search ?? '')\n const referrer = environment.referrer || undefined\n const utmSource = params.get('utm_source') || undefined\n const utmMedium = params.get('utm_medium') || undefined\n const utmCampaign = params.get('utm_campaign') || undefined\n const classifiedDevice = deviceClass(environment.matchMedia)\n\n return {\n ...(referrer !== undefined ? { referrer } : {}),\n ...(utmSource !== undefined ? { utmSource } : {}),\n ...(utmMedium !== undefined ? { utmMedium } : {}),\n ...(utmCampaign !== undefined ? { utmCampaign } : {}),\n ...(classifiedDevice !== undefined ? { deviceClass: classifiedDevice } : {}),\n }\n}\n","import type { AnalyticsEventMap, AnalyticsEventName } from './events'\n\nexport type EventHandler<E extends AnalyticsEventName> = (payload: AnalyticsEventMap[E]) => void\n\n/** A framework-agnostic typed pub/sub bus (Hydrogen `useAnalytics` shape, minus React). */\nexport interface AnalyticsEmitter {\n subscribe<E extends AnalyticsEventName>(event: E, handler: EventHandler<E>): () => void\n publish<E extends AnalyticsEventName>(event: E, payload: AnalyticsEventMap[E]): void\n}\n\nexport function createAnalyticsEmitter(): AnalyticsEmitter {\n const handlers = new Map<AnalyticsEventName, Set<(payload: unknown) => void>>()\n return {\n subscribe(event, handler) {\n const set = handlers.get(event) ?? new Set<(payload: unknown) => void>()\n const erased = handler as (payload: unknown) => void\n set.add(erased)\n handlers.set(event, set)\n return () => {\n set.delete(erased)\n }\n },\n publish(event, payload) {\n const set = handlers.get(event)\n if (set === undefined) return\n // Snapshot so a handler unsubscribing mid-dispatch doesn't skip a sibling.\n for (const handler of [...set]) {\n try {\n handler(payload)\n } catch {\n // Isolate: a throwing subscriber must not break sibling handlers or the downstream pixel send.\n }\n }\n },\n }\n}\n","import type { AnonymousAnalyticsContext } from './anonymous-context'\nimport type { ConsentState } from './consent'\nimport { createAnalyticsEmitter, type AnalyticsEmitter } from './emitter'\nimport type { AnalyticsContext, AnalyticsEventMap, AnalyticsEventName } from './events'\nimport type { PixelTransport } from './pixel-transport'\n\nexport interface AnalyticsBusOptions {\n readonly transport: PixelTransport\n /** Legacy consent seam retained for API compatibility while Storefront consent UI is disabled. */\n readonly getConsent: () => ConsentState\n /** Ambient context (including attribution and device class) attached to consented events. */\n readonly getContext?: () => AnalyticsContext\n /** Sanitized same-origin page URL used only for the anonymous page-view fallback. */\n readonly getAnonymousContext?: () => AnonymousAnalyticsContext\n /** Clock seam for deterministic timestamps in tests. */\n readonly now?: () => string\n}\n\nexport interface AnalyticsBus {\n readonly emitter: AnalyticsEmitter\n publish<E extends AnalyticsEventName>(event: E, payload: AnalyticsEventMap[E]): void\n flush(): void\n}\n\n/**\n * Wire the emitter to the pixel. Every published event reaches theme subscribers (via the emitter).\n * First-party recording is consent-strict: denied visits get only an anonymous page_viewed row and\n * never evaluate ambient context, which may read attribution or device-class signals.\n */\nexport function createAnalyticsBus(options: AnalyticsBusOptions): AnalyticsBus {\n const emitter = createAnalyticsEmitter()\n const now = options.now ?? ((): string => new Date().toISOString())\n return {\n emitter,\n publish(event, payload) {\n emitter.publish(event, payload)\n const consented = options.getConsent().analytics === true\n if (consented) {\n const context = options.getContext?.() ?? {}\n options.transport.record({ event, payload, context, timestamp: now(), consent: true })\n return\n }\n if (event === 'page_viewed') {\n options.transport.record({\n event,\n payload: {} as AnalyticsEventMap[typeof event],\n context: options.getAnonymousContext?.() ?? {},\n timestamp: now(),\n consent: false,\n })\n }\n },\n flush() {\n options.transport.flush()\n },\n }\n}\n","import type { PixelEvent } from './events'\n\n/** The actual network send. Injectable so tests don't touch the network. */\nexport type PixelSend = (endpoint: string, body: string) => void\n\nexport interface PixelTransportOptions {\n /** Same-origin carve-out endpoint (default '/api/storefront/pixel') — never cross-origin. */\n readonly endpoint?: string\n readonly batchSize?: number\n readonly flushAfterMs?: number\n /**\n * The actual network send. Required + injected: the browser sender (`sendBeacon`/keepalive `fetch`)\n * is DOM-specific and lives in the React layer, keeping this batching core framework-agnostic and\n * unit-testable with a spy. The provider wires the real sender.\n */\n readonly send: PixelSend\n}\n\nexport interface PixelTransport {\n record(event: PixelEvent): void\n flush(): void\n}\n\nconst DEFAULT_ENDPOINT = '/api/storefront/pixel'\nconst DEFAULT_BATCH_SIZE = 10\n\n/**\n * Batches pixel events and POSTs them to the same-origin carve-out. Flushes on `batchSize` or an\n * explicit `flush()` (the provider flushes on page-hide), or after one second. The endpoint is same-origin so themes need\n * no CORS and no token is exposed in the browser.\n */\nexport function createPixelTransport(options: PixelTransportOptions): PixelTransport {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT\n // SECURITY: the pixel must stay same-origin (no CORS, no token leak). Reject a non-relative or\n // protocol-relative endpoint so a misconfigured theme can't exfiltrate visitor data (e.g. clientIp).\n if (!/^\\/(?!\\/)/.test(endpoint)) {\n throw new Error(`pixel endpoint must be a same-origin path starting with '/'; got: ${endpoint}`)\n }\n const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE\n const send = options.send\n let queue: PixelEvent[] = []\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const flush = (): void => {\n if (timer !== undefined) clearTimeout(timer)\n timer = undefined\n if (queue.length === 0) return\n const batch = queue\n queue = []\n send(endpoint, JSON.stringify({ events: batch }))\n }\n\n return {\n record(event) {\n queue.push(event)\n if (queue.length >= batchSize) flush()\n else timer ??= setTimeout(flush, options.flushAfterMs ?? 1000)\n },\n flush,\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/cart/CartForm.ts","../src/react/nonce.ts","../src/analytics/attribution.ts","../src/analytics/emitter.ts","../src/analytics/analytics-bus.ts","../src/analytics/pixel-transport.ts"],"names":["CartForm"],"mappings":"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","file":"chunk-BV4OLXV4.js","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { CART_FORM_ACTIONS, type CartFormAction } from './actions'\n\nexport interface CartFormProps {\n readonly action: CartFormAction\n}\n\nexport interface CartFormInputResult {\n readonly action: CartFormAction | null\n readonly inputs: Record<string, any>\n}\n\nfunction parseFormValue(value: unknown): unknown {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n return JSON.parse(trimmed) as unknown\n } catch {\n return value\n }\n }\n return value\n}\n\nexport function getCartFormInput(formData: FormData): CartFormInputResult {\n const action = formData.get('cartAction')\n const inputs: Record<string, any> = {}\n for (const key of new Set(formData.keys())) {\n if (key === 'cartAction') continue\n const values = formData.getAll(key).map(parseFormValue)\n inputs[key] = values.length === 1 ? values[0] : values\n }\n return {\n action: typeof action === 'string' ? (action as CartFormAction) : null,\n inputs,\n }\n}\n\n/**\n * Lightweight CartForm surface marker for Hydrogen-style imports. React render integration can wrap\n * this constant surface; the load-bearing API today is the stable ACTIONS contract used by handlers.\n */\nexport const CartForm = Object.assign(\n function CartForm(_props: CartFormProps): null {\n return null\n },\n { ACTIONS: CART_FORM_ACTIONS, getFormInput: getCartFormInput },\n)\n","import * as ReactRuntime from 'react'\nimport { createContext, createElement, useContext, type ReactNode } from 'react'\n\nconst NonceContext = createContext<string | undefined>(undefined)\n\nexport interface NonceProviderProps {\n readonly children?: ReactNode\n readonly nonce?: string\n}\n\nexport type BoundNonceProvider = (props: { readonly children?: ReactNode }) => ReactNode\n\nexport function NonceProvider(props: NonceProviderProps): ReactNode {\n return createElement(NonceContext.Provider, { value: props.nonce }, props.children)\n}\n\nexport function createNonceProvider(nonce: string): BoundNonceProvider {\n return function BoundNonceProvider(props: { readonly children?: ReactNode }): ReactNode {\n return createElement(NonceContext.Provider, { value: nonce }, props.children)\n }\n}\n\n/** Hydrogen-compatible nonce hook. */\nexport function useNonce(): string | undefined {\n if (!hasReactDispatcher()) return undefined\n return useContext(NonceContext)\n}\n\nfunction hasReactDispatcher(): boolean {\n const runtime = ReactRuntime as unknown as {\n __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?: { H?: unknown }\n __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: {\n ReactCurrentDispatcher?: { current?: unknown }\n }\n }\n return Boolean(\n runtime.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.H ??\n runtime.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentDispatcher?.current,\n )\n}\n","import type { AnalyticsContext, AnalyticsDeviceClass } from './events'\n\nexport interface AnalyticsAttributionEnvironment {\n readonly referrer?: string\n readonly search?: string\n readonly matchMedia?: (query: string) => { readonly matches: boolean }\n}\n\ntype AttributionContext = Pick<\n AnalyticsContext,\n 'referrer' | 'utmSource' | 'utmMedium' | 'utmCampaign' | 'deviceClass'\n>\n\nfunction deviceClass(\n matchMedia: AnalyticsAttributionEnvironment['matchMedia'],\n): AnalyticsDeviceClass | undefined {\n if (matchMedia === undefined) return undefined\n if (matchMedia('(max-width: 767px)').matches) return 'mobile'\n if (matchMedia('(max-width: 1023px)').matches) return 'tablet'\n return 'desktop'\n}\n\n/** Read static landing attribution plus the current responsive device class without retaining state\n * or identifiers. All values stay optional so non-DOM/SSR emitters keep their existing payload. */\nexport function collectAnalyticsAttribution(\n environment: AnalyticsAttributionEnvironment | null,\n): AttributionContext {\n if (environment === null) return {}\n const params = new URLSearchParams(environment.search ?? '')\n const referrer = environment.referrer || undefined\n const utmSource = params.get('utm_source') || undefined\n const utmMedium = params.get('utm_medium') || undefined\n const utmCampaign = params.get('utm_campaign') || undefined\n const classifiedDevice = deviceClass(environment.matchMedia)\n\n return {\n ...(referrer !== undefined ? { referrer } : {}),\n ...(utmSource !== undefined ? { utmSource } : {}),\n ...(utmMedium !== undefined ? { utmMedium } : {}),\n ...(utmCampaign !== undefined ? { utmCampaign } : {}),\n ...(classifiedDevice !== undefined ? { deviceClass: classifiedDevice } : {}),\n }\n}\n","import type { AnalyticsEventMap, AnalyticsEventName } from './events'\n\nexport type EventHandler<E extends AnalyticsEventName> = (payload: AnalyticsEventMap[E]) => void\n\n/** A framework-agnostic typed pub/sub bus (Hydrogen `useAnalytics` shape, minus React). */\nexport interface AnalyticsEmitter {\n subscribe<E extends AnalyticsEventName>(event: E, handler: EventHandler<E>): () => void\n publish<E extends AnalyticsEventName>(event: E, payload: AnalyticsEventMap[E]): void\n}\n\nexport function createAnalyticsEmitter(): AnalyticsEmitter {\n const handlers = new Map<AnalyticsEventName, Set<(payload: unknown) => void>>()\n return {\n subscribe(event, handler) {\n const set = handlers.get(event) ?? new Set<(payload: unknown) => void>()\n const erased = handler as (payload: unknown) => void\n set.add(erased)\n handlers.set(event, set)\n return () => {\n set.delete(erased)\n }\n },\n publish(event, payload) {\n const set = handlers.get(event)\n if (set === undefined) return\n // Snapshot so a handler unsubscribing mid-dispatch doesn't skip a sibling.\n for (const handler of [...set]) {\n try {\n handler(payload)\n } catch {\n // Isolate: a throwing subscriber must not break sibling handlers or the downstream pixel send.\n }\n }\n },\n }\n}\n","import type { AnonymousAnalyticsContext } from './anonymous-context'\nimport type { ConsentState } from './consent'\nimport { createAnalyticsEmitter, type AnalyticsEmitter } from './emitter'\nimport type { AnalyticsContext, AnalyticsEventMap, AnalyticsEventName } from './events'\nimport type { PixelTransport } from './pixel-transport'\n\nexport interface AnalyticsBusOptions {\n readonly transport: PixelTransport\n /** Legacy consent seam retained for API compatibility while Storefront consent UI is disabled. */\n readonly getConsent: () => ConsentState\n /** Ambient context (including attribution and device class) attached to consented events. */\n readonly getContext?: () => AnalyticsContext\n /** Sanitized same-origin page URL used only for the anonymous page-view fallback. */\n readonly getAnonymousContext?: () => AnonymousAnalyticsContext\n /** Clock seam for deterministic timestamps in tests. */\n readonly now?: () => string\n}\n\nexport interface AnalyticsBus {\n readonly emitter: AnalyticsEmitter\n publish<E extends AnalyticsEventName>(event: E, payload: AnalyticsEventMap[E]): void\n flush(): void\n}\n\n/**\n * Wire the emitter to the pixel. Every published event reaches theme subscribers (via the emitter).\n * First-party recording is consent-strict: denied visits get only an anonymous page_viewed row and\n * never evaluate ambient context, which may read attribution or device-class signals.\n */\nexport function createAnalyticsBus(options: AnalyticsBusOptions): AnalyticsBus {\n const emitter = createAnalyticsEmitter()\n const now = options.now ?? ((): string => new Date().toISOString())\n return {\n emitter,\n publish(event, payload) {\n emitter.publish(event, payload)\n const consented = options.getConsent().analytics === true\n if (consented) {\n const context = options.getContext?.() ?? {}\n options.transport.record({ event, payload, context, timestamp: now(), consent: true })\n return\n }\n if (event === 'page_viewed') {\n options.transport.record({\n event,\n payload: {} as AnalyticsEventMap[typeof event],\n context: options.getAnonymousContext?.() ?? {},\n timestamp: now(),\n consent: false,\n })\n }\n },\n flush() {\n options.transport.flush()\n },\n }\n}\n","import type { PixelEvent } from './events'\n\n/** The actual network send. Injectable so tests don't touch the network. */\nexport type PixelSend = (endpoint: string, body: string) => void\n\nexport interface PixelTransportOptions {\n /** Same-origin carve-out endpoint (default '/api/storefront/pixel') — never cross-origin. */\n readonly endpoint?: string\n readonly batchSize?: number\n readonly flushAfterMs?: number\n /**\n * The actual network send. Required + injected: the browser sender (`sendBeacon`/keepalive `fetch`)\n * is DOM-specific and lives in the React layer, keeping this batching core framework-agnostic and\n * unit-testable with a spy. The provider wires the real sender.\n */\n readonly send: PixelSend\n}\n\nexport interface PixelTransport {\n record(event: PixelEvent): void\n flush(): void\n}\n\nconst DEFAULT_ENDPOINT = '/api/storefront/pixel'\nconst DEFAULT_BATCH_SIZE = 10\n\n/**\n * Batches pixel events and POSTs them to the same-origin carve-out. Flushes on `batchSize` or an\n * explicit `flush()` (the provider flushes on page-hide), or after one second. The endpoint is same-origin so themes need\n * no CORS and no token is exposed in the browser.\n */\nexport function createPixelTransport(options: PixelTransportOptions): PixelTransport {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT\n // SECURITY: the pixel must stay same-origin (no CORS, no token leak). Reject a non-relative or\n // protocol-relative endpoint so a misconfigured theme can't exfiltrate visitor data (e.g. clientIp).\n if (!/^\\/(?!\\/)/.test(endpoint)) {\n throw new Error(`pixel endpoint must be a same-origin path starting with '/'; got: ${endpoint}`)\n }\n const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE\n const send = options.send\n let queue: PixelEvent[] = []\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const flush = (): void => {\n if (timer !== undefined) clearTimeout(timer)\n timer = undefined\n if (queue.length === 0) return\n const batch = queue\n queue = []\n send(endpoint, JSON.stringify({ events: batch }))\n }\n\n return {\n record(event) {\n queue.push(event)\n if (queue.length >= batchSize) flush()\n else timer ??= setTimeout(flush, options.flushAfterMs ?? 1000)\n },\n flush,\n }\n}\n"]}
@@ -30,30 +30,18 @@ var GRANTED_CONSENT = Object.freeze({
30
30
  var CONSENT_COOKIE = "kotao_consent";
31
31
  var CONSENT_MAX_AGE = 60 * 60 * 24 * 180;
32
32
  function isGranted(state, bucket) {
33
- return true;
33
+ return state[bucket] === true;
34
34
  }
35
35
  function mergeConsent(state, patch) {
36
36
  return { ...state, ...patch };
37
37
  }
38
38
  function serializeConsent(state) {
39
- return JSON.stringify({
40
- analytics: state.analytics,
41
- marketing: state.marketing,
42
- preferences: state.preferences
43
- });
39
+ return `v1.${state.analytics ? "1" : "0"}.${state.marketing ? "1" : "0"}.${state.preferences ? "1" : "0"}`;
44
40
  }
45
41
  function parseConsent(value) {
46
- if (value === null || value === void 0 || value === "") return GRANTED_CONSENT;
47
- try {
48
- const parsed = JSON.parse(value);
49
- return {
50
- analytics: parsed.analytics === true,
51
- marketing: parsed.marketing === true,
52
- preferences: parsed.preferences === true
53
- };
54
- } catch {
55
- return GRANTED_CONSENT;
56
- }
42
+ const match = /^v1\.([01])\.([01])\.([01])$/.exec(value ?? "");
43
+ if (match === null) return DENIED_CONSENT;
44
+ return { analytics: match[1] === "1", marketing: match[2] === "1", preferences: match[3] === "1" };
57
45
  }
58
46
  function buildConsentCookie(state) {
59
47
  const value = encodeURIComponent(serializeConsent(state));
@@ -61,12 +49,16 @@ function buildConsentCookie(state) {
61
49
  }
62
50
  function readConsentCookie(cookieHeader) {
63
51
  if (cookieHeader === null || cookieHeader === void 0 || cookieHeader === "")
64
- return GRANTED_CONSENT;
52
+ return DENIED_CONSENT;
65
53
  const match = cookieHeader.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${CONSENT_COOKIE}=`));
66
- if (match === void 0) return GRANTED_CONSENT;
67
- return GRANTED_CONSENT;
54
+ if (match === void 0) return DENIED_CONSENT;
55
+ try {
56
+ return parseConsent(decodeURIComponent(match.slice(CONSENT_COOKIE.length + 1)));
57
+ } catch {
58
+ return DENIED_CONSENT;
59
+ }
68
60
  }
69
61
 
70
62
  export { CART_FORM_ACTIONS, CONSENT_BUCKETS, CONSENT_COOKIE, DENIED_CONSENT, GRANTED_CONSENT, buildConsentCookie, isGranted, mergeConsent, parseConsent, readConsentCookie, serializeConsent };
71
- //# sourceMappingURL=chunk-OGWYTYYS.js.map
72
- //# sourceMappingURL=chunk-OGWYTYYS.js.map
63
+ //# sourceMappingURL=chunk-GR4QJ7EC.js.map
64
+ //# sourceMappingURL=chunk-GR4QJ7EC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cart/actions.ts","../src/analytics/consent.ts"],"names":[],"mappings":"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","file":"chunk-GR4QJ7EC.js","sourcesContent":["export const CART_FORM_ACTIONS = Object.freeze({\n LinesAdd: 'LinesAdd',\n LinesUpdate: 'LinesUpdate',\n LinesRemove: 'LinesRemove',\n DiscountCodesUpdate: 'DiscountCodesUpdate',\n GiftCardCodesAdd: 'GiftCardCodesAdd',\n GiftCardCodesRemove: 'GiftCardCodesRemove',\n BuyerIdentityUpdate: 'BuyerIdentityUpdate',\n NoteUpdate: 'NoteUpdate',\n AttributesUpdate: 'AttributesUpdate',\n MetafieldsSet: 'MetafieldsSet',\n MetafieldsDelete: 'MetafieldsDelete',\n DeliveryAddressesReplace: 'DeliveryAddressesReplace',\n SelectedDeliveryOptionsUpdate: 'SelectedDeliveryOptionsUpdate',\n} as const)\n\nexport type CartFormAction = (typeof CART_FORM_ACTIONS)[keyof typeof CART_FORM_ACTIONS]\n","/**\n * Shared native-CMP consent state. Unknown choices deny optional categories; the versioned cookie\n * is the same contract used by dispatch and checkout.\n */\nexport type ConsentBucket = 'analytics' | 'marketing' | 'preferences'\nexport type ConsentState = Readonly<Record<ConsentBucket, boolean>>\n\nexport const CONSENT_BUCKETS: readonly ConsentBucket[] = ['analytics', 'marketing', 'preferences']\nexport const DENIED_CONSENT: ConsentState = Object.freeze({\n analytics: false,\n marketing: false,\n preferences: false,\n})\nexport const GRANTED_CONSENT: ConsentState = Object.freeze({\n analytics: true,\n marketing: true,\n preferences: true,\n})\n\nexport const CONSENT_COOKIE = 'kotao_consent'\nconst CONSENT_MAX_AGE = 60 * 60 * 24 * 180 // ~180 days\n\nexport function isGranted(state: ConsentState, bucket: ConsentBucket): boolean {\n return state[bucket] === true\n}\n\nexport function mergeConsent(state: ConsentState, patch: Partial<ConsentState>): ConsentState {\n return { ...state, ...patch }\n}\n\nexport function serializeConsent(state: ConsentState): string {\n return `v1.${state.analytics ? '1' : '0'}.${state.marketing ? '1' : '0'}.${state.preferences ? '1' : '0'}`\n}\n\nexport function parseConsent(value: string | null | undefined): ConsentState {\n const match = /^v1\\.([01])\\.([01])\\.([01])$/.exec(value ?? '')\n if (match === null) return DENIED_CONSENT\n return { analytics: match[1] === '1', marketing: match[2] === '1', preferences: match[3] === '1' }\n}\n\n/** The `document.cookie` string to persist consent (Secure, SameSite=Lax, Path=/, ~180d). */\nexport function buildConsentCookie(state: ConsentState): string {\n const value = encodeURIComponent(serializeConsent(state))\n return `${CONSENT_COOKIE}=${value}; Path=/; Max-Age=${CONSENT_MAX_AGE}; SameSite=Lax; Secure`\n}\n\n/** Read the consent cookie out of a `document.cookie`-style string. */\nexport function readConsentCookie(cookieHeader: string | null | undefined): ConsentState {\n if (cookieHeader === null || cookieHeader === undefined || cookieHeader === '')\n return DENIED_CONSENT\n const match = cookieHeader\n .split(';')\n .map((part) => part.trim())\n .find((part) => part.startsWith(`${CONSENT_COOKIE}=`))\n if (match === undefined) return DENIED_CONSENT\n try {\n return parseConsent(decodeURIComponent(match.slice(CONSENT_COOKIE.length + 1)))\n } catch {\n return DENIED_CONSENT\n }\n}\n"]}
@@ -13,7 +13,7 @@ function splitCdnCgi(url) {
13
13
  const [k, v] = pair.split("=");
14
14
  if (k && v) opts.set(k, v);
15
15
  }
16
- return { bare: `${u.origin}${path}`, opts };
16
+ return { bare: `${u.origin}${path}${u.search}`, opts };
17
17
  }
18
18
  var OPT_ORDER = ["width", "height", "fit", "format", "quality"];
19
19
  function imageUrl(url, options = {}) {
@@ -23,11 +23,12 @@ function imageUrl(url, options = {}) {
23
23
  if (options.height != null) opts.set("height", String(options.height));
24
24
  if (options.fit != null) opts.set("fit", options.fit);
25
25
  if (options.format != null) opts.set("format", options.format);
26
+ else if (!opts.has("format")) opts.set("format", "auto");
26
27
  if (options.quality != null) opts.set("quality", String(options.quality));
27
28
  const ordered = OPT_ORDER.filter((k) => opts.has(k)).map((k) => `${k}=${opts.get(k)}`);
28
29
  if (ordered.length === 0) return bare;
29
30
  const u = new URL(bare);
30
- return `${u.origin}${CDN_CGI}${ordered.join(",")}${u.pathname}`;
31
+ return `${u.origin}${CDN_CGI}${ordered.join(",")}${u.pathname}${u.search}`;
31
32
  }
32
33
  function imageSrcSet(url, options) {
33
34
  const { widths, ...rest } = options;
@@ -39,24 +40,32 @@ var DEFAULT_WIDTHS = [320, 640, 768, 1024, 1280, 1536];
39
40
  var FALLBACK_WIDTH = 1280;
40
41
  function buildImageAttrs(options) {
41
42
  const url = options.src ?? options.data.url;
42
- const widths = options.srcSetWidths ?? (options.width ? [options.width, options.width * 2, options.width * 3] : DEFAULT_WIDTHS);
43
+ const requestedWidths = options.srcSetWidths ?? (options.width ? [options.width, options.width * 2, options.width * 3] : DEFAULT_WIDTHS);
44
+ const urlTransform = /^https?:\/\/cdn\.kotao\.[^/]+\/cdn-cgi\/image\/([^/]+)/.exec(url)?.[1];
45
+ const urlOptions = new URLSearchParams(urlTransform?.replaceAll(",", "&"));
46
+ const urlWidth = Number(urlOptions.get("width")) || null;
47
+ const urlHeight = Number(urlOptions.get("height")) || null;
48
+ const sourceWidths = [options.data.width, urlWidth].filter((width) => width != null && width > 0);
49
+ const sourceWidth = sourceWidths.length ? Math.min(...sourceWidths) : null;
50
+ const widths = [...new Set(requestedWidths.map((width) => sourceWidth === null ? width : Math.min(width, sourceWidth)))];
43
51
  const aspectRatio = options.aspectRatio ?? (options.data.width != null && options.data.height != null ? `${options.data.width}/${options.data.height}` : void 0);
44
52
  const transform = {};
45
53
  if (options.crop) transform.fit = "cover";
46
54
  if (options.format != null) transform.format = options.format;
47
55
  if (options.quality != null) transform.quality = options.quality;
48
- const heightPerWidth = options.width != null && options.height != null ? options.height / options.width : null;
56
+ const heightPerWidth = options.width != null && options.height != null ? options.height / options.width : urlWidth !== null && urlHeight !== null ? urlHeight / urlWidth : null;
49
57
  const candidate = (w) => {
50
58
  const opts = { ...transform, width: w };
51
59
  if (heightPerWidth != null) opts.height = Math.round(w * heightPerWidth);
52
60
  return opts;
53
61
  };
54
- const srcOptions = { ...transform, width: options.width ?? FALLBACK_WIDTH };
55
- if (options.height != null) srcOptions.height = options.height;
62
+ const fallbackWidth = Math.min(options.width ?? FALLBACK_WIDTH, sourceWidth ?? Infinity);
63
+ const srcOptions = candidate(fallbackWidth);
64
+ if (heightPerWidth === null && options.height != null) srcOptions.height = options.height;
56
65
  return {
57
66
  src: imageUrl(url, srcOptions),
58
67
  srcSet: widths.map((w) => `${imageUrl(url, candidate(w))} ${w}w`).join(", "),
59
- ...options.sizes != null ? { sizes: options.sizes } : {},
68
+ ...options.sizes != null ? { sizes: options.sizes } : options.width != null ? { sizes: `${options.width}px` } : {},
60
69
  ...options.width != null ? { width: options.width } : {},
61
70
  ...options.height != null ? { height: options.height } : {},
62
71
  alt: options.data.altText ?? "",
@@ -66,5 +75,5 @@ function buildImageAttrs(options) {
66
75
  }
67
76
 
68
77
  export { buildImageAttrs, imageSrcSet, imageUrl };
69
- //# sourceMappingURL=chunk-3YQXJNR7.js.map
70
- //# sourceMappingURL=chunk-3YQXJNR7.js.map
78
+ //# sourceMappingURL=chunk-PWVXOMDE.js.map
79
+ //# sourceMappingURL=chunk-PWVXOMDE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/image/image-url.ts","../src/image/image-attrs.ts"],"names":[],"mappings":"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","file":"chunk-PWVXOMDE.js","sourcesContent":["/**\n * Framework-agnostic image-URL builders for the storefront SDK (ADR-0003 §SDK image-URL contract).\n * They build Cloudflare `/cdn-cgi/image/<opts>/<path>` path-form transform URLs for `https://cdn.kotao.*`\n * bases, allowing the CDN to serve optimized renderings. Non-CDN URLs (external, data:, theme `/assets/...`)\n * pass through unchanged. Wire-native params — `width`/`height`/`fit`/`format`/`quality` — matching\n * Cloudflare's native image resizing; a Hydrogen-shaped React `<Image>` (with `crop`, `srcSet`/`sizes`\n * props) layers on top in the framework presets (#92). Pure string building, no framework imports, so it\n * stays in the SDK core.\n */\n\n/** Cloudflare-native resize mode (the Gateway's `fit` param). */\nexport type ImageFit = 'scale-down' | 'contain' | 'pad' | 'squeeze' | 'cover' | 'crop'\n/** Output format query value (the Gateway maps it to a content-type). */\nexport type ImageFormat = 'auto' | 'webp' | 'avif' | 'jpeg' | 'png'\n\nexport interface ImageUrlOptions {\n width?: number\n height?: number\n fit?: ImageFit\n format?: ImageFormat\n /** 1–100. */\n quality?: number\n}\n\nconst CDN_CGI = '/cdn-cgi/image/'\nconst isCdnUrl = (url: string): boolean => /^https?:\\/\\/cdn\\.kotao\\./.test(url)\n\n/** Strip a `/cdn-cgi/image/<opts>/` segment back to `{origin}{path}`, returning [bareUrl, existingOpts]. */\nfunction splitCdnCgi(url: string): { bare: string; opts: URLSearchParams } {\n const u = new URL(url)\n if (!u.pathname.startsWith(CDN_CGI)) return { bare: url, opts: new URLSearchParams() }\n const rest = u.pathname.slice(CDN_CGI.length)\n const slash = rest.indexOf('/')\n const optStr = slash === -1 ? rest : rest.slice(0, slash)\n const path = slash === -1 ? '' : rest.slice(slash) // includes leading '/'\n const opts = new URLSearchParams()\n for (const pair of optStr.split(',')) {\n const [k, v] = pair.split('=')\n if (k && v) opts.set(k, v)\n }\n return { bare: `${u.origin}${path}${u.search}`, opts }\n}\n\nconst OPT_ORDER = ['width', 'height', 'fit', 'format', 'quality'] as const\n\n/**\n * Build a Cloudflare `/cdn-cgi/image/` path-form transform URL for a CDN image. For transformable\n * URLs (absolute `https://cdn.kotao.*`), emits `/cdn-cgi/image/{opts}/{path}` where `{opts}` is\n * the comma-joined key=value list (in stable order: width,height,fit,format,quality) and `{path}`\n * is the original pathname. Chaining replaces any existing `/cdn-cgi/image/` segment.\n * Non-CDN URLs pass through unchanged.\n */\nexport function imageUrl(url: string, options: ImageUrlOptions = {}): string {\n if (!isCdnUrl(url)) return url\n const { bare, opts } = splitCdnCgi(url)\n if (options.width != null) opts.set('width', String(options.width))\n if (options.height != null) opts.set('height', String(options.height))\n if (options.fit != null) opts.set('fit', options.fit)\n if (options.format != null) opts.set('format', options.format)\n else if (!opts.has('format')) opts.set('format', 'auto')\n if (options.quality != null) opts.set('quality', String(options.quality))\n const ordered = OPT_ORDER.filter((k) => opts.has(k)).map((k) => `${k}=${opts.get(k)}`)\n if (ordered.length === 0) return bare\n const u = new URL(bare)\n return `${u.origin}${CDN_CGI}${ordered.join(',')}${u.pathname}${u.search}`\n}\n\nexport interface ImageSrcSetOptions extends Omit<ImageUrlOptions, 'width'> {\n /** Candidate widths (px) for the `srcset` descriptors. */\n widths: number[]\n}\n\n/**\n * Build a `srcset` string — one `url width` candidate per width — for responsive `<img srcset>` /\n * `<source srcset>`. Pair with a `sizes` attribute the theme controls.\n */\nexport function imageSrcSet(url: string, options: ImageSrcSetOptions): string {\n const { widths, ...rest } = options\n return widths.map((width) => `${imageUrl(url, { ...rest, width })} ${width}w`).join(', ')\n}\n","/**\n * Framework-agnostic `<img>` attribute builder (ADR-0003 §SDK image-URL contract). Turns the\n * Hydrogen-shaped `<Image>` surface (data/aspectRatio/crop/sizes/width/height) into concrete img\n * attributes (src, srcSet, sizes, width, height, style) over the `cdn.kotao.*` R2-native transform\n * contract, via the pure `imageUrl`/`imageSrcSet` builders. The React `<Image>` is a thin wrapper\n * that spreads these — so all logic lives here, in the core, unit-tested without a DOM.\n */\nimport { imageUrl, type ImageFormat, type ImageUrlOptions } from './image-url'\n\nexport interface ImageData {\n url: string\n altText?: string | null\n width?: number | null\n height?: number | null\n}\n\n/** Hydrogen crop region. v1 maps any crop to a fill+crop (Cloudflare `fit=cover`); positional gravity\n * (top/bottom/left/right) is not on the wire yet, so all values currently centre-crop. */\nexport type ImageCrop = 'center' | 'top' | 'bottom' | 'left' | 'right'\n\n// Optionals are `| undefined` on purpose: the React <Image> feeds them straight from optional props,\n// so explicit `undefined` must be assignable under exactOptionalPropertyTypes.\nexport interface ImageAttrsOptions {\n data: ImageData\n /** Override `data.url`. */\n src?: string | undefined\n /** \"width/height\" (e.g. \"4/5\") — sets the intrinsic box for CLS-free layout. */\n aspectRatio?: string | undefined\n crop?: ImageCrop | undefined\n /** The responsive `sizes` attribute; the theme owns it. */\n sizes?: string | undefined\n /** Fixed render width (px). With it, the srcSet becomes 1x/2x/3x instead of the responsive set. */\n width?: number | undefined\n height?: number | undefined\n loading?: ('eager' | 'lazy') | undefined\n /** srcSet candidate widths; defaults to a responsive set (or 1x/2x/3x when `width` is fixed). */\n srcSetWidths?: number[] | undefined\n format?: ImageFormat | undefined\n quality?: number | undefined\n}\n\nexport interface ImageAttrs {\n src: string\n srcSet: string\n sizes?: string\n width?: number\n height?: number\n alt: string\n loading: 'eager' | 'lazy'\n style?: { aspectRatio: string }\n}\n\n/** Responsive widths when no fixed `width` is given (mirrors Hydrogen's default interval set). */\nconst DEFAULT_WIDTHS = [320, 640, 768, 1024, 1280, 1536]\n/** The `src` fallback width for browsers that ignore srcSet. */\nconst FALLBACK_WIDTH = 1280\n\nexport function buildImageAttrs(options: ImageAttrsOptions): ImageAttrs {\n const url = options.src ?? options.data.url\n\n const requestedWidths =\n options.srcSetWidths ??\n (options.width ? [options.width, options.width * 2, options.width * 3] : DEFAULT_WIDTHS)\n // A preceding Liquid image_url transform can make this a small thumbnail. Never undo that\n // bound by advertising full-page candidates (or upscale beyond an original image's pixels).\n const urlTransform = /^https?:\\/\\/cdn\\.kotao\\.[^/]+\\/cdn-cgi\\/image\\/([^/]+)/.exec(url)?.[1]\n const urlOptions = new URLSearchParams(urlTransform?.replaceAll(',', '&'))\n const urlWidth = Number(urlOptions.get('width')) || null\n const urlHeight = Number(urlOptions.get('height')) || null\n const sourceWidths = [options.data.width, urlWidth]\n .filter((width): width is number => width != null && width > 0)\n const sourceWidth = sourceWidths.length ? Math.min(...sourceWidths) : null\n const widths = [...new Set(requestedWidths.map(width =>\n sourceWidth === null ? width : Math.min(width, sourceWidth)))]\n\n const aspectRatio =\n options.aspectRatio ??\n (options.data.width != null && options.data.height != null\n ? `${options.data.width}/${options.data.height}`\n : undefined)\n\n // Shared transform params — set only when present (exactOptionalPropertyTypes forbids explicit\n // `undefined`). A crop maps to Cloudflare `fit=cover` (fill+crop).\n const transform: ImageUrlOptions = {}\n if (options.crop) transform.fit = 'cover'\n if (options.format != null) transform.format = options.format\n if (options.quality != null) transform.quality = options.quality\n\n // For a fixed box (width+height), the srcSet candidates must scale BOTH dimensions so every DPR\n // variant keeps the same crop aspect (a uniform height would distort the 2x/3x candidates).\n const heightPerWidth =\n options.width != null && options.height != null ? options.height / options.width\n : urlWidth !== null && urlHeight !== null ? urlHeight / urlWidth : null\n const candidate = (w: number): ImageUrlOptions => {\n const opts: ImageUrlOptions = { ...transform, width: w }\n if (heightPerWidth != null) opts.height = Math.round(w * heightPerWidth)\n return opts\n }\n\n const fallbackWidth = Math.min(options.width ?? FALLBACK_WIDTH, sourceWidth ?? Infinity)\n const srcOptions = candidate(fallbackWidth)\n if (heightPerWidth === null && options.height != null) srcOptions.height = options.height\n\n return {\n src: imageUrl(url, srcOptions),\n srcSet: widths.map((w) => `${imageUrl(url, candidate(w))} ${w}w`).join(', '),\n ...(options.sizes != null ? { sizes: options.sizes }\n : options.width != null ? { sizes: `${options.width}px` } : {}),\n ...(options.width != null ? { width: options.width } : {}),\n ...(options.height != null ? { height: options.height } : {}),\n alt: options.data.altText ?? '',\n loading: options.loading ?? 'lazy',\n ...(aspectRatio != null ? { style: { aspectRatio } } : {}),\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { CONSENT_BUCKETS, readConsentCookie, CART_FORM_ACTIONS } from './chunk-OGWYTYYS.js';
1
+ import { CONSENT_BUCKETS, readConsentCookie, CART_FORM_ACTIONS } from './chunk-GR4QJ7EC.js';
2
2
  import { fetchThemeSettings } from './chunk-2MRB6SMP.js';
3
3
  import { encodeUtf8Base64Url, toBase64Url, fromBase64Url, decodeBase64UrlUtf8 } from './chunk-CKGI3Y7L.js';
4
4
  import { createEventsClient, normalizeStorefrontHeaders } from './chunk-R4N565GX.js';
@@ -248,6 +248,13 @@ function resolveCheckoutUrl(options) {
248
248
  const origin = resolveShopOrigin(options);
249
249
  const rawCheckoutUrl = options.checkoutUrl.trim();
250
250
  const url = parseCheckoutUrl(options.checkoutUrl, origin);
251
+ const locale = options.locale?.trim().replace(/_/g, "-").toLowerCase();
252
+ if (locale && locale.length <= 35 && /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/.test(locale)) {
253
+ url.searchParams.set("locale", locale);
254
+ }
255
+ if (options.pathPrefix?.startsWith("/") && !options.pathPrefix.startsWith("//")) {
256
+ url.searchParams.set("market_path", options.pathPrefix);
257
+ }
251
258
  if (url.origin !== origin) {
252
259
  if (!isExternalHttpsCheckoutUrl(rawCheckoutUrl, url)) {
253
260
  throw new Error(`checkoutUrl must stay on the same origin as the shop domain (${origin})`);
@@ -321,6 +328,7 @@ function extractCheckoutDesignTokens(snapshot) {
321
328
  const logoUrl = optionalString(global["checkoutLogoUrl"], "checkoutLogoUrl");
322
329
  const logoAlt = optionalString(global["checkoutLogoAlt"], "checkoutLogoAlt");
323
330
  return {
331
+ ...typeof global.checkoutCornerRadius === "number" && Number.isFinite(global.checkoutCornerRadius) && global.checkoutCornerRadius >= 0 && global.checkoutCornerRadius <= 48 ? { cornerRadius: global.checkoutCornerRadius } : {},
324
332
  logo: logoUrl === null ? null : { src: logoUrl, altText: logoAlt },
325
333
  colors: {
326
334
  accent: requiredString(global["checkoutColorAccent"], "checkoutColorAccent"),
@@ -647,6 +655,8 @@ function createCartHandler(opts) {
647
655
  ...cart,
648
656
  checkoutUrl: resolveCheckoutUrl({
649
657
  checkoutUrl: cart.checkoutUrl,
658
+ locale: opts.storefront.i18n.language,
659
+ pathPrefix: opts.storefront.i18n.pathPrefix,
650
660
  requestUrl: request,
651
661
  consent
652
662
  })
@@ -2055,5 +2065,5 @@ function createEphemeralSession(isLoggedIn) {
2055
2065
  }
2056
2066
 
2057
2067
  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, clearCartIdCookie, clearCustomerSessionCookie, clearHostedCustomerSessionCookie, clearLegacyCustomerSessionCookie, createCache, createCartHandler, createCustomerAccountClient, createKotaoContext, createStorefrontClient, createWithCache, decodeCheckoutConsent, encodeCheckoutConsent, extractCheckoutDesignTokens, generateCacheControlHeader, isLocalePathSegment, localeFromAcceptLanguage, localeFromPathname, localeFromRequest, localizePath, marketContextFromHeaders, originalPathnameFromRequest, readCartIdCookie, readCustomerSessionToken, readHostedCustomerCsrfToken, readHostedCustomerSessionToken, readLegacyCustomerSessionToken, readLocalizationCookie, resolveCheckoutUrl, shopHostname, signGeo, stripLocalePrefixFromPathname, stripLocalePrefixFromRequest, trustedShopOriginFromHeaders, verifyGeo };
2058
- //# sourceMappingURL=chunk-PHEKJDFW.js.map
2059
- //# sourceMappingURL=chunk-PHEKJDFW.js.map
2068
+ //# sourceMappingURL=chunk-XZVCUAZU.js.map
2069
+ //# sourceMappingURL=chunk-XZVCUAZU.js.map