@kotao/storefront 0.2.10 → 0.2.11
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.
- package/README.md +20 -2
- package/dist/{chunk-XZVCUAZU.js → chunk-4OELOXCN.js} +6 -3
- package/dist/chunk-4OELOXCN.js.map +1 -0
- package/dist/{chunk-BV4OLXV4.js → chunk-NRWS7HJ2.js} +64 -17
- package/dist/chunk-NRWS7HJ2.js.map +1 -0
- package/dist/{client-D-M3Y1ul.d.ts → client-Bt8EEr-K.d.ts} +1 -1
- package/dist/events-react.d.ts +2 -2
- package/dist/events.d.ts +2 -2
- package/dist/forms.d.ts +2 -2
- package/dist/index.d.ts +9 -28
- package/dist/index.js +5 -79
- package/dist/index.js.map +1 -1
- package/dist/{pagination-DCrQk02Q.d.ts → pagination-CPgH3fAa.d.ts} +1 -1
- package/dist/product.d.ts +1 -1
- package/dist/react-router.d.ts +4 -4
- package/dist/react-router.js +2 -2
- package/dist/react.d.ts +16 -5
- package/dist/react.js +25 -3
- package/dist/react.js.map +1 -1
- package/dist/{routing-D2GFXAa0.d.ts → routing-hrvYWYEV.d.ts} +1 -1
- package/dist/{shop-4y-4z3oZ.d.ts → shop-wJdcWcVz.d.ts} +17 -7
- package/dist/{types-CSNXbGFW.d.ts → types-BTUTZXxJ.d.ts} +4 -3
- package/package.json +1 -1
- package/dist/chunk-BV4OLXV4.js.map +0 -1
- package/dist/chunk-XZVCUAZU.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { S as StoredThemeSettings } from './fetch-Bq_cc8_G.js';
|
|
2
2
|
import { C as CacheStrategy, K as KotaoCache, S as StorefrontClient, b as StorefrontCacheInput } from './client-D51WdvYR.js';
|
|
3
3
|
import { C as CartHandler } from './createCartHandler-DJEZW4o2.js';
|
|
4
|
-
import { E as EventsClient } from './client-
|
|
4
|
+
import { E as EventsClient } from './client-Bt8EEr-K.js';
|
|
5
5
|
import { H as HydrogenSession } from './types-C4yXcret.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { j as CartFormAction } from './types-C4yXcret.js';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
3
2
|
|
|
4
3
|
interface CartFormProps {
|
|
5
4
|
readonly action: CartFormAction;
|
|
@@ -51,11 +50,22 @@ declare function buildConsentCookie(state: ConsentState): string;
|
|
|
51
50
|
/** Read the consent cookie out of a `document.cookie`-style string. */
|
|
52
51
|
declare function readConsentCookie(cookieHeader: string | null | undefined): ConsentState;
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
readonly
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
interface ContentSecurityPolicyShop {
|
|
54
|
+
readonly checkoutDomain?: string;
|
|
55
|
+
readonly storeDomain?: string;
|
|
56
|
+
}
|
|
57
|
+
interface CreateContentSecurityPolicyOptions {
|
|
58
|
+
readonly shop?: ContentSecurityPolicyShop;
|
|
59
|
+
readonly scriptSrc?: readonly string[];
|
|
60
|
+
readonly connectSrc?: readonly string[];
|
|
61
|
+
readonly imgSrc?: readonly string[];
|
|
62
|
+
readonly styleSrc?: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
interface ContentSecurityPolicyResult {
|
|
65
|
+
readonly nonce: string;
|
|
66
|
+
readonly header: string;
|
|
67
|
+
}
|
|
68
|
+
declare function createContentSecurityPolicy(options?: CreateContentSecurityPolicyOptions): ContentSecurityPolicyResult;
|
|
59
69
|
|
|
60
70
|
/**
|
|
61
71
|
* Coarse visitor geo attached to pixel events — structurally the SDK's `context.geo` (a `Geo` value is
|
|
@@ -227,4 +237,4 @@ interface GetShopAnalyticsOptions {
|
|
|
227
237
|
}
|
|
228
238
|
declare function getShopAnalytics(options: GetShopAnalyticsOptions): ShopAnalytics;
|
|
229
239
|
|
|
230
|
-
export { type AnalyticsContext as A,
|
|
240
|
+
export { type AnalyticsContext as A, buildConsentCookie as B, type ConsentBucket as C, DENIED_CONSENT as D, type EventHandler as E, createAnalyticsBus as F, GRANTED_CONSENT as G, createAnalyticsEmitter as H, createContentSecurityPolicy as I, createPixelTransport as J, getShopAnalytics as K, isGranted as L, mergeConsent as M, parseConsent as N, readConsentCookie as O, type PageViewedPayload as P, serializeConsent as Q, getCartFormInput as R, type SearchViewedPayload as S, type ConsentState as a, ANALYTICS_EVENTS as b, type AnalyticsBus as c, type AnalyticsBusOptions as d, type AnalyticsDeviceClass as e, type AnalyticsEmitter as f, type AnalyticsEventMap as g, type AnalyticsEventName as h, type AnalyticsGeo as i, CONSENT_BUCKETS as j, CONSENT_COOKIE as k, CartForm as l, type CartFormProps as m, type CartViewedPayload as n, type CollectionViewedPayload as o, type ContentSecurityPolicyResult as p, type ContentSecurityPolicyShop as q, type CreateContentSecurityPolicyOptions as r, type GetShopAnalyticsOptions as s, type PixelEvent as t, type PixelSend as u, type PixelTransport as v, type PixelTransportOptions as w, type ProductAddedToCartPayload as x, type ProductViewedPayload as y, type ShopAnalytics as z };
|
|
@@ -18,8 +18,9 @@ interface FormField {
|
|
|
18
18
|
validation?: {
|
|
19
19
|
minLength?: number;
|
|
20
20
|
maxLength?: number;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/** Inclusive number or ISO calendar date (YYYY-MM-DD), matching the field kind. */
|
|
22
|
+
min?: number | string;
|
|
23
|
+
max?: number | string;
|
|
23
24
|
};
|
|
24
25
|
choices?: {
|
|
25
26
|
value: string;
|
|
@@ -105,4 +106,4 @@ interface FormSubmission {
|
|
|
105
106
|
updatedAt: string;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
export type { FormVersionSchema as F, PublicForm as P,
|
|
109
|
+
export type { FormVersionSchema as F, PublicForm as P, FormAnswer as a, FormAnswers as b, FormBlock as c, FormField as d, FormFieldErrors as e, FormLocalizedText as f, FormRenderState as g, FormSubmission as h, FormVisibilityRule as i, PublicFormDefinition as j, PublicFormVersion as k };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kotao/storefront",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Kotao Storefronts theme SDK — framework-agnostic server runtime, React Router 8 binding, and authenticated source-authoritative React/Hydrogen editor bridge (ADR-0003/ADR-0047). Leaf package (ADR-0006).",
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|