@genlook/storefront 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +180 -0
  2. package/dist/analytics.d.ts +26 -0
  3. package/dist/analytics.js +7 -0
  4. package/dist/anonymous-id.d.ts +30 -0
  5. package/dist/anonymous-id.js +33 -0
  6. package/dist/client.d.ts +289 -0
  7. package/dist/client.js +465 -0
  8. package/dist/consent.d.ts +17 -0
  9. package/dist/consent.js +34 -0
  10. package/dist/create-client.d.ts +84 -0
  11. package/dist/create-client.js +131 -0
  12. package/dist/default-tracking.d.ts +64 -0
  13. package/dist/default-tracking.js +101 -0
  14. package/dist/email.d.ts +5 -0
  15. package/dist/email.js +24 -0
  16. package/dist/entities.d.ts +186 -0
  17. package/dist/entities.js +47 -0
  18. package/dist/erasure.d.ts +36 -0
  19. package/dist/erasure.js +23 -0
  20. package/dist/events.d.ts +226 -0
  21. package/dist/events.js +41 -0
  22. package/dist/fetch-transport.d.ts +65 -0
  23. package/dist/fetch-transport.js +112 -0
  24. package/dist/generation.d.ts +132 -0
  25. package/dist/generation.js +382 -0
  26. package/dist/history.d.ts +76 -0
  27. package/dist/history.js +68 -0
  28. package/dist/memory-storage.d.ts +10 -0
  29. package/dist/memory-storage.js +12 -0
  30. package/dist/pending-upload.d.ts +32 -0
  31. package/dist/pending-upload.js +13 -0
  32. package/dist/persistence.d.ts +12 -0
  33. package/dist/persistence.js +101 -0
  34. package/dist/policy.d.ts +24 -0
  35. package/dist/policy.js +45 -0
  36. package/dist/ports.d.ts +187 -0
  37. package/dist/ports.js +1 -0
  38. package/dist/public-api.d.ts +235 -0
  39. package/dist/public-api.js +1 -0
  40. package/dist/public.d.ts +33 -0
  41. package/dist/public.js +12 -0
  42. package/dist/settings.d.ts +38 -0
  43. package/dist/settings.js +70 -0
  44. package/dist/sharing.d.ts +26 -0
  45. package/dist/sharing.js +42 -0
  46. package/dist/storage-adapters.d.ts +69 -0
  47. package/dist/storage-adapters.js +82 -0
  48. package/dist/store.d.ts +9 -0
  49. package/dist/store.js +23 -0
  50. package/dist/tracker.d.ts +124 -0
  51. package/dist/tracker.js +229 -0
  52. package/dist/types.d.ts +91 -0
  53. package/dist/types.js +1 -0
  54. package/dist/upload.d.ts +123 -0
  55. package/dist/upload.js +176 -0
  56. package/dist/usage.d.ts +45 -0
  57. package/dist/usage.js +110 -0
  58. package/dist/version.d.ts +10 -0
  59. package/dist/version.js +1 -0
  60. package/package.json +30 -0
@@ -0,0 +1,101 @@
1
+ export const GENERATION_TIMEOUT_MS = 60 * 1000;
2
+ export const STALE_TTL_MS = GENERATION_TIMEOUT_MS + 30 * 1000;
3
+ export const RETENTION_MS = 24 * 60 * 60 * 1000;
4
+ const STALE_ERROR = { code: "network", retryable: true, backendCode: "STALE" };
5
+ function storageKey(storeId) {
6
+ return `tryon-jobs:${storeId || "default"}`;
7
+ }
8
+ function toPersisted(job) {
9
+ const base = {
10
+ id: job.id,
11
+ status: job.status,
12
+ photoId: job.photoId,
13
+ productId: job.productId,
14
+ variantId: job.variantId,
15
+ createdAt: job.createdAt,
16
+ seen: job.seen,
17
+ reported: job.reported,
18
+ usageAt: job.usageAt,
19
+ usageRefunded: job.usageRefunded,
20
+ };
21
+ if (job.status === "done") {
22
+ base.resultImageUrl = job.resultImageUrl;
23
+ base.resultImageKey = job.resultImageKey;
24
+ }
25
+ else if (job.status === "error") {
26
+ base.error = job.error;
27
+ }
28
+ return base;
29
+ }
30
+ function fromPersisted(p, now) {
31
+ const terminal = p.status === "done" || p.status === "error";
32
+ if (terminal) {
33
+ if (now - p.createdAt > RETENTION_MS)
34
+ return null;
35
+ if (p.status === "done") {
36
+ return {
37
+ id: p.id, photoId: p.photoId, productId: p.productId, variantId: p.variantId,
38
+ createdAt: p.createdAt, seen: p.seen, reported: p.reported,
39
+ usageAt: p.usageAt, usageRefunded: p.usageRefunded,
40
+ status: "done", resultImageUrl: p.resultImageUrl ?? "", resultImageKey: p.resultImageKey,
41
+ };
42
+ }
43
+ return {
44
+ id: p.id, photoId: p.photoId, productId: p.productId, variantId: p.variantId,
45
+ createdAt: p.createdAt, seen: p.seen, reported: p.reported,
46
+ usageAt: p.usageAt, usageRefunded: p.usageRefunded,
47
+ status: "error", error: p.error ?? STALE_ERROR,
48
+ };
49
+ }
50
+ if (now - p.createdAt > STALE_TTL_MS) {
51
+ return {
52
+ id: p.id, photoId: p.photoId, productId: p.productId, variantId: p.variantId,
53
+ createdAt: p.createdAt, seen: p.seen, reported: p.reported,
54
+ usageAt: p.usageAt, usageRefunded: p.usageRefunded,
55
+ status: "error", error: STALE_ERROR,
56
+ };
57
+ }
58
+ return {
59
+ id: p.id, photoId: p.photoId, productId: p.productId, variantId: p.variantId,
60
+ createdAt: p.createdAt, seen: p.seen, reported: p.reported,
61
+ usageAt: p.usageAt, usageRefunded: p.usageRefunded,
62
+ status: p.status === "requested" ? "requested" : "generating",
63
+ };
64
+ }
65
+ export function saveJobs(storage, storeId, jobs) {
66
+ try {
67
+ const persisted = jobs.map(toPersisted);
68
+ storage.setItem(storageKey(storeId), JSON.stringify(persisted));
69
+ }
70
+ catch {
71
+ }
72
+ }
73
+ export function loadJobs(storage, storeId, now) {
74
+ let raw;
75
+ try {
76
+ raw = storage.getItem(storageKey(storeId));
77
+ }
78
+ catch {
79
+ return [];
80
+ }
81
+ if (!raw)
82
+ return [];
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(raw);
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ if (!Array.isArray(parsed))
91
+ return [];
92
+ const out = [];
93
+ for (const item of parsed) {
94
+ if (!item || typeof item.id !== "string")
95
+ continue;
96
+ const job = fromPersisted(item, now);
97
+ if (job)
98
+ out.push(job);
99
+ }
100
+ return out;
101
+ }
@@ -0,0 +1,24 @@
1
+ import type { CoreState } from "./entities";
2
+ export type PolicyAction = "upload" | "generate";
3
+ export type BlockedReason = "email-required" | "quota-exceeded" | "login-required" | "credits-expired" | "concurrency"
4
+ /** Legal consent is required by the host and not yet on record. Opt-in per
5
+ * host (`TryOnClientConfig.requireLegalConsent`); blocks upload AND generate. */
6
+ | "consent-required";
7
+ export type PolicyResult = {
8
+ ok: true;
9
+ } | {
10
+ ok: false;
11
+ blocked: BlockedReason;
12
+ };
13
+ /** Thrown by generate() when called while blocked; carries the same typed reason. */
14
+ export declare class PolicyBlockedError extends Error {
15
+ readonly reason: BlockedReason;
16
+ constructor(reason: BlockedReason);
17
+ }
18
+ /**
19
+ * Evaluate whether an action is permitted. Precedence (first blocking reason wins):
20
+ * login-required → consent-required → quota-exceeded → email-required →
21
+ * credits-expired → concurrency.
22
+ * `email-required` is a soft gate the host satisfies by collecting an email, not an error.
23
+ */
24
+ export declare function can(action: PolicyAction, state: CoreState, now: number): PolicyResult;
package/dist/policy.js ADDED
@@ -0,0 +1,45 @@
1
+ const DAY_MS = 24 * 60 * 60 * 1000;
2
+ function countInWindow(tryOns, period, now) {
3
+ const cutoff = now - (period === "daily" ? DAY_MS : 7 * DAY_MS);
4
+ return tryOns.filter((ts) => ts > cutoff).length;
5
+ }
6
+ function isInFlight(job) {
7
+ return job.status === "requested" || job.status === "generating";
8
+ }
9
+ export class PolicyBlockedError extends Error {
10
+ reason;
11
+ constructor(reason) {
12
+ super(`Blocked: ${reason}`);
13
+ this.name = "PolicyBlockedError";
14
+ this.reason = reason;
15
+ }
16
+ }
17
+ export function can(action, state, now) {
18
+ const { limits, identity } = state;
19
+ if (limits.loggedInCustomersOnly && !identity.loggedInCustomerId) {
20
+ return { ok: false, blocked: "login-required" };
21
+ }
22
+ if (limits.requireLegalConsent && !state.legalConsent) {
23
+ return { ok: false, blocked: "consent-required" };
24
+ }
25
+ if (action === "upload")
26
+ return { ok: true };
27
+ const used = countInWindow(limits.tryOns, limits.period, now);
28
+ if (used >= limits.maxGenerations) {
29
+ return { ok: false, blocked: "quota-exceeded" };
30
+ }
31
+ const hasEmail = !!identity.email;
32
+ if (limits.totalTryOns >= limits.emailCollectionStep && !hasEmail) {
33
+ return { ok: false, blocked: "email-required" };
34
+ }
35
+ if (limits.creditsAllowed === false) {
36
+ return { ok: false, blocked: "credits-expired" };
37
+ }
38
+ if (typeof limits.maxConcurrent === "number" && Number.isFinite(limits.maxConcurrent)) {
39
+ const inFlight = Object.values(state.generations).filter(isInFlight).length;
40
+ if (inFlight >= limits.maxConcurrent) {
41
+ return { ok: false, blocked: "concurrency" };
42
+ }
43
+ }
44
+ return { ok: true };
45
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Ports for the headless try-on core. Everything the domain layer needs from the
3
+ * host runtime is an interface here, so @genlook/storefront references no browser
4
+ * or Node global (tsconfig `lib: ["ES2022"]`, no "dom" — the headlessness guard).
5
+ */
6
+ import type { ConsentSnapshot } from "./types";
7
+ /** Response the core reads back. The browser `Response` structurally satisfies it. */
8
+ export interface TransportResponse {
9
+ readonly ok: boolean;
10
+ readonly status: number;
11
+ readonly statusText: string;
12
+ json(): Promise<unknown>;
13
+ text(): Promise<string>;
14
+ }
15
+ /** Per-request options handed to the transport. A structural subset of the
16
+ * per-request options a typical HTTP client already takes, so an existing
17
+ * client satisfies Transport as-is. */
18
+ export interface TransportRequestInit {
19
+ method?: string;
20
+ headers?: Record<string, string>;
21
+ body?: string;
22
+ /** Attach the device-fingerprint header — set ONLY on the generation and
23
+ * upload-completion calls (backend ingests it there). */
24
+ includeFingerprint?: boolean;
25
+ /** Send with unload semantics (keepalive / navigator.sendBeacon) for the
26
+ * page-hide flush. The web transport maps it to sendBeacon with a plain-fetch
27
+ * keepalive fallback; other transports may treat it as a no-op hint. */
28
+ beacon?: boolean;
29
+ }
30
+ /** Opaque shopper-supplied media forwarded verbatim to Transport.putRaw (a
31
+ * browser File/Blob today). The core never inspects it. Real media abstraction
32
+ * lands in a later phase. */
33
+ export type MediaInput = unknown;
34
+ /**
35
+ * The two network capabilities the core needs:
36
+ * - fetch — Genlook proxy call by relative path; the impl injects the app-proxy
37
+ * prefix + identity headers.
38
+ * - putRaw — direct byte upload to a signed (absolute) storage URL, carrying NO
39
+ * Genlook identity headers (preserves the GCS signed-URL contract).
40
+ */
41
+ export interface Transport {
42
+ fetch(path: string, init?: TransportRequestInit): Promise<TransportResponse>;
43
+ putRaw(url: string, body: MediaInput, headers: Record<string, string>): Promise<TransportResponse>;
44
+ }
45
+ /** String key/value storage. A browser's `localStorage` satisfies it directly;
46
+ * async native stores go through the hydrated storage adapter, since this port
47
+ * is synchronous by design. */
48
+ export interface KVStorage {
49
+ getItem(key: string): string | null;
50
+ setItem(key: string, value: string): void;
51
+ removeItem(key: string): void;
52
+ }
53
+ /**
54
+ * Consent-state source for the tracker. The impl wraps the platform adapter's
55
+ * `getConsentSnapshot`/`onConsentChange` (Shopify's real CMP tracker; the
56
+ * base-adapter placeholder for the rest). The tracker's binary gate reads the
57
+ * snapshot's `analytics_allowed` and stamps the snapshot on each batch.
58
+ */
59
+ export interface ConsentSource {
60
+ /** Current snapshot, or null when no adapter/CMP has answered. */
61
+ getSnapshot(): ConsentSnapshot | null;
62
+ /** Subscribe to snapshot changes (banner accept/decline). */
63
+ onChange(cb: (snapshot: ConsentSnapshot | null) => void): void;
64
+ }
65
+ /**
66
+ * The named surfaces that can produce try-on traffic. A closed vocabulary the
67
+ * backend validates against, so "which surface is this?" is answered by the
68
+ * sender rather than inferred from the shape of the row.
69
+ *
70
+ * - `widget` — the storefront widget on a merchant's shop.
71
+ * - `tryon_core` — this package, driving try-ons directly (native app,
72
+ * headless or custom storefront).
73
+ * - `sdk_node` — the server-side Try-On API SDK.
74
+ * - `raw_http` — a caller hitting the API by hand.
75
+ * - `mcp_client` — an MCP client.
76
+ */
77
+ export type IntegrationName = "widget" | "tryon_core" | "sdk_node" | "raw_http" | "mcp_client";
78
+ /** Common-tier tracking context — no shopper identity, no device-storage reads.
79
+ * Assembled by the web PageContext impl from DOM/host + platform state.
80
+ *
81
+ * The page/browser fields are nullable: a non-browser host (native app, server)
82
+ * has no URL, no viewport and no user agent, and a null there is worth more than
83
+ * an invented value that would pollute the warehouse. The backend accepts them
84
+ * as optional-nullable; a browser host keeps filling all of them. */
85
+ export interface TrackingBaseContext {
86
+ $current_url: string | null;
87
+ $pathname: string | null;
88
+ $host: string | null;
89
+ $screen_width: number;
90
+ $screen_height: number;
91
+ $viewport_width: number | null;
92
+ $viewport_height: number | null;
93
+ $raw_user_agent: string | null;
94
+ $browser_language: string | null;
95
+ $timezone: string;
96
+ widget_enabled: boolean;
97
+ platform: string;
98
+ store_id: string | null;
99
+ product_id: string | null;
100
+ variant_id: string | null;
101
+ widget_version: string;
102
+ /** Content hash of the APP sources the boot was built from. `widget_version`
103
+ * hashes only the runtime shell and barely moves, so it cannot answer "did my
104
+ * deploy reach this shopper?". Null for a host that bakes no define. */
105
+ app_version?: string | null;
106
+ page_type: string | null;
107
+ collection_id: string | null;
108
+ /** Which surface produced the batch. Every host names itself — null means
109
+ * "unknown", never "widget", so a page-level query can exclude page-less
110
+ * surfaces (which send null URL/viewport/user agent) explicitly. */
111
+ integration: IntegrationName | null;
112
+ /** Version of the thing named by {@link integration}: the `wv_` build id for
113
+ * the storefront widget, this package's semver for `tryon_core`. Null when
114
+ * the surface has no version to report. */
115
+ integration_version: string | null;
116
+ }
117
+ /**
118
+ * First-touch attribution record — captured ONCE at the first page the widget
119
+ * observes in a session and never overwritten afterwards. Persisted host-side
120
+ * (ePrivacy-gated storage) and read lazily only for a full-mode batch. The three
121
+ * `utm_*` fields are the same first-touch UTMs the wire has always carried;
122
+ * `landing_path` (pathname + search, so the query string survives), `referrer`,
123
+ * `referring_domain`, and `ts` (ISO-8601 capture time) extend the same record.
124
+ */
125
+ export interface GenlookFirstTouch {
126
+ utm_source: string | null;
127
+ utm_medium: string | null;
128
+ utm_campaign: string | null;
129
+ landing_path: string | null;
130
+ referrer: string | null;
131
+ referring_domain: string | null;
132
+ ts: string;
133
+ }
134
+ /** Full-mode-only tracking context: shopper identity + attribution + referrer.
135
+ * Read from device storage, so the tracker requests it ONLY in full mode.
136
+ * `$referrer`/`$referring_domain` are the CURRENT page's referrer (re-read each
137
+ * batch); `first_touch` is the immutable first-observed-page record. */
138
+ export interface TrackingIdentityContext {
139
+ $referrer: string | null;
140
+ $referring_domain: string | null;
141
+ session_id: string;
142
+ anonymous_id: string;
143
+ first_touch: GenlookFirstTouch | null;
144
+ }
145
+ /**
146
+ * DOM/host inputs the tracker context needs. Implemented by the host; the core
147
+ * never touches the DOM. `getIdentityContext` is the only method that reads
148
+ * ePrivacy-gated storage, and the tracker calls it exclusively in full mode.
149
+ */
150
+ export interface PageContext {
151
+ /** True for crawlers/headless agents — the tracker drops all events then. */
152
+ isBot(): boolean;
153
+ /** Current pathname; the tracker mints a fresh pageview id when it changes.
154
+ * Null for a host with no pages — it must then match `$pathname` so the
155
+ * pageview id stays stable instead of re-minting on every batch. */
156
+ getPathname(): string | null;
157
+ /** Common-tier context (no identity, no gated storage reads). */
158
+ getBaseContext(): TrackingBaseContext;
159
+ /** Full-mode-only context (identity + attribution + referrer). */
160
+ getIdentityContext(): TrackingIdentityContext;
161
+ /** Drop any cached session id so a later re-read reflects the new basis. */
162
+ resetSessionId(): void;
163
+ /** Remove the two tracking-owned storage keys (anonymous id + attribution). */
164
+ purgeTrackingStorage(): void;
165
+ }
166
+ /**
167
+ * Connectivity as the host sees it, sampled at the moment a failure is recorded.
168
+ *
169
+ * A port because `navigator` and `document` are browser APIs and the core is
170
+ * platform-agnostic — a native or server host implements this differently, or not
171
+ * at all (it is optional).
172
+ *
173
+ * Exists because upload failures currently arrive as `"Failed to fetch"` /
174
+ * `"Load failed"` — the browser's generic network errors, which say nothing about
175
+ * cause. These three fields separate the common explanations: the shopper lost
176
+ * signal, they were on a slow connection, or the page was backgrounded and the
177
+ * in-flight request was killed.
178
+ */
179
+ export interface NetworkInfo {
180
+ /** `navigator.onLine`. False is conclusive; true only means "not obviously off". */
181
+ online: boolean;
182
+ /** Network Information API effective type ("4g", "3g", "slow-2g"). Chromium-only. */
183
+ effectiveType?: string;
184
+ /** `document.visibilityState`. A hidden page has its fetches killed on mobile,
185
+ * which is a prime suspect for the generic errors. */
186
+ visibility?: "visible" | "hidden";
187
+ }
package/dist/ports.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,235 @@
1
+ import type { CoreState } from "./entities";
2
+ import type { UploadMeta } from "./entities";
3
+ import type { Listener } from "./store";
4
+ import type { CoreEvent, CoreEventListener, CoreEventType } from "./events";
5
+ import type { PolicyAction, PolicyResult } from "./policy";
6
+ import type { MediaInput } from "./ports";
7
+ import type { TryOnResult } from "./history";
8
+ import type { GetShareUrlOptions } from "./sharing";
9
+ import type { GenerateInput, GenerateOutput } from "./client";
10
+ import type { PrepareUploadRequest, UploadResult, RatingReasonCode, CurrentPlanResponse, CheckCreditsResponse, CollectEmailRequest, CollectEmailResponse } from "./types";
11
+ /**
12
+ * The public headless try-on surface, reachable at `window.Genlook.tryon`.
13
+ *
14
+ * The concrete `TryOnClient` implements it; hosts should type against this
15
+ * interface rather than the class so they only depend on the supported surface.
16
+ * See the module header for the cross-bundle error-recognition contract shared
17
+ * by every rejecting method here.
18
+ */
19
+ export interface GenlookTryOn {
20
+ /**
21
+ * Current snapshot of the core state tree (limits window, identity, in-flight
22
+ * generations, history). Pair with {@link subscribe} to render a reactive view;
23
+ * the returned object is the live snapshot — treat it as read-only.
24
+ */
25
+ getState(): CoreState;
26
+ /**
27
+ * Subscribe to state transitions (a new snapshot is available after any
28
+ * mutating op: upload, generate, email collection, history/rating change).
29
+ * The listener takes no args — read the fresh state via {@link getState}.
30
+ * Returns an unsubscribe function. Notifications fire only when state identity
31
+ * actually changes.
32
+ */
33
+ subscribe(listener: Listener): () => void;
34
+ /**
35
+ * Subscribe to a named lifecycle event. Returns an unsubscribe function.
36
+ *
37
+ * The edge-triggered counterpart to {@link subscribe}: use `on` to react to a
38
+ * transition ("the upload just got rejected") and `subscribe` + {@link getState}
39
+ * to render a value ("the upload is rejected"). Without this, every host has to
40
+ * keep a shadow copy of the previous state and diff it to spot the transition —
41
+ * the same edge detection re-implemented, and mis-implemented, per UI.
42
+ *
43
+ * Events are not replayed: a listener attached after the fact does not see it.
44
+ * A throwing listener is swallowed and does not stop the others.
45
+ */
46
+ on<T extends CoreEventType>(type: T, listener: CoreEventListener<T>): () => void;
47
+ /**
48
+ * Subscribe to EVERY core event, current and future. Returns an unsubscribe
49
+ * function.
50
+ *
51
+ * For consumers that are generic over the event set rather than reacting to one
52
+ * thing: logging, forwarding to a host bridge, dev tooling. Prefer it wherever
53
+ * the consumer would otherwise hardcode a list of event names — a new event then
54
+ * appears automatically instead of being silently omitted.
55
+ */
56
+ onAny(listener: (event: CoreEvent) => void): () => void;
57
+ /**
58
+ * Evaluate whether an action is currently permitted by the policy layer
59
+ * (login gate, legal-consent gate when the host opted in, quota window,
60
+ * email-collection step, credits, concurrency)
61
+ * WITHOUT performing it or mutating state. Returns `{ ok: true }` or
62
+ * `{ ok: false, blocked }` with a typed {@link PolicyResult} reason. `generate`
63
+ * runs this same check internally and throws `PolicyBlockedError` on a block,
64
+ * so calling `can("generate")` first lets a host gate its UI ahead of the call.
65
+ */
66
+ can(action: PolicyAction): PolicyResult;
67
+ /**
68
+ * Record shopper acceptance of the legal-consent gate at `version` (a
69
+ * host-owned wording version). Persists the `{ version, acceptedAt }` record
70
+ * (storage-compatible with the pre-headless mobile sheet, so an already-consented
71
+ * shopper is recognized at boot), reflects it into `state.legalConsent`
72
+ * (subscribers notified — read the gate via `getState().legalConsent`, `null`
73
+ * means never accepted), and emits `tryon:legal_consent_accepted`.
74
+ *
75
+ * Re-accepting a NEW version overwrites the record and emits again; re-accepting
76
+ * the SAME version is a no-op (no duplicate event).
77
+ *
78
+ * Coupling to the policy layer is the host's choice: with
79
+ * `requireLegalConsent: true` in the client config, `can("upload")` and
80
+ * `can("generate")` return `consent-required` until this runs (and `generate()`
81
+ * throws `PolicyBlockedError`) — the gate is then enforced by the core, not by
82
+ * the UI. Left off (the default), `can()` is unaffected and showing a consent
83
+ * screen stays a UI decision.
84
+ *
85
+ * Emits: `tryon:legal_consent_accepted { consent_version }` (except on a
86
+ * same-version no-op).
87
+ */
88
+ acceptLegalConsent(version: string): void;
89
+ /**
90
+ * Upload a shopper photo. `file` is opaque host media (a browser `File`/`Blob`)
91
+ * forwarded verbatim to storage; `meta` carries upload provenance + the
92
+ * host-read validation inputs (file name + DOM dimensions); `productContext`
93
+ * optionally scopes the /uploads call.
94
+ *
95
+ * Registers the returned promise as the client's LATEST in-flight upload
96
+ * (supersede-latest): a newer `uploadPhoto` supersedes an older one, so a later
97
+ * `generate({ userImage: "latest" })` awaits whichever upload was newest at the
98
+ * time generate is called. A superseded upload still settles for its own awaiter
99
+ * and still emits its terminal event, but is no longer "current".
100
+ *
101
+ * PROGRESS is observable from state, mirroring generation: the photo entity
102
+ * transitions uploading → ready | failed through the store, so
103
+ * `selectCurrentUpload(getState())` + {@link subscribe} render the current
104
+ * upload's status without a return-value round-trip.
105
+ *
106
+ * POLICY-GATED: rejects with `PolicyBlockedError` (`name`/`reason`) before any
107
+ * network call when `can("upload")` blocks — under
108
+ * `requireLegalConsent: true` that includes `consent-required`, so an
109
+ * integrator's own consent screen cannot be short-circuited by calling this
110
+ * directly. Prefer {@link stagePhoto} at pick time: it HOLDS the photo instead
111
+ * of refusing it.
112
+ *
113
+ * The core VALIDATES `meta` before any network call; on a verdict it rejects
114
+ * with `UploadRejectedError` (`name`/`reason`, recognised by name) and creates
115
+ * NO entity (rejection ≠ failure). Otherwise it rejects with a typed upload
116
+ * failure on network/prepare errors, and the entity transitions to `failed`.
117
+ *
118
+ * Emits: `widget:image_upload` (start) then `widget:image_upload_success` /
119
+ * `widget:image_upload_error`; a validation verdict OR a post-crop rejection
120
+ * emits `widget:image_upload_rejected` instead (never both).
121
+ */
122
+ uploadPhoto(file: MediaInput, meta: UploadMeta, productContext?: PrepareUploadRequest): Promise<UploadResult>;
123
+ /**
124
+ * Hand over a freshly picked photo and let the core decide when it may be sent:
125
+ * uploaded IMMEDIATELY if {@link acceptLegalConsent} has already been recorded,
126
+ * otherwise held in memory until it is. Nothing reaches the network while
127
+ * consent is outstanding.
128
+ *
129
+ * Prefer this over {@link uploadPhoto} at the moment of the pick. It lets a UI
130
+ * put the upload behind whatever screen follows the pick (a confirm step, a
131
+ * consent gate) without having to know whether sending yet is allowed — and it
132
+ * makes "no bytes before consent" a property of the core instead of a rule each
133
+ * UI re-implements.
134
+ *
135
+ * Returns nothing on purpose. Observe progress via
136
+ * `selectCurrentUpload(getState())` + {@link subscribe}, and let
137
+ * `generate({ userImage: "latest" })` await the result — it resolves against
138
+ * whatever this started, including an upload still in flight.
139
+ *
140
+ * Supersede-latest: staging again replaces the previous photo (in flight or
141
+ * held). {@link clearPendingUpload} discards a held photo as well as an
142
+ * in-flight one.
143
+ *
144
+ * Never rejects the photo: a pick made while consent is outstanding is HELD, not
145
+ * refused, even under `requireLegalConsent: true`. Quota is not pre-checked
146
+ * either — `can("generate")` is still evaluated by generate().
147
+ *
148
+ * Emits: nothing while held; the normal {@link uploadPhoto} event sequence once
149
+ * released.
150
+ */
151
+ stagePhoto(file: MediaInput, meta: UploadMeta, productContext?: PrepareUploadRequest): void;
152
+ /**
153
+ * Drop the tracked in-flight upload (the shopper discarded the selected photo):
154
+ * clears the pending-upload pointer, any photo still held by {@link stagePhoto},
155
+ * AND the current-upload state pointer, so the
156
+ * next `generate({ userImage: "latest" })` no longer awaits it and
157
+ * `selectCurrentUpload(getState())` reads null. Photo entities are left in the
158
+ * store. Pure client-side state; no network, no events.
159
+ */
160
+ clearPendingUpload(): void;
161
+ /**
162
+ * Create and drive a try-on generation to completion. Resolve the user image
163
+ * from `input.userImageId` or, with `userImage: "latest"`, by awaiting the
164
+ * in-flight {@link uploadPhoto}. Deduped within a short TTL, gated by the policy
165
+ * layer, and counted against the quota window on acceptance. When `input.context`
166
+ * is provided, the assembled `TryOnResult` is appended to history and returned
167
+ * as `result`.
168
+ *
169
+ * Rejects with `PolicyBlockedError` (`name`/`reason`) when blocked, or
170
+ * `GenerationFailedError` (`name`/`kind`/`code`) on upload-coordination or
171
+ * generation failure — recognise both by `name`, never `instanceof`.
172
+ *
173
+ * Emits: `widget:generation_start` on acceptance.
174
+ */
175
+ generate(input: GenerateInput): Promise<GenerateOutput>;
176
+ /** Full try-on history (newest-last), the same list {@link subscribe} tracks. */
177
+ getHistory(): TryOnResult[];
178
+ /**
179
+ * Read-side retention view of history filtered to the merchant's retention
180
+ * window (stored history is never pruned; this only filters the read). Pass the
181
+ * retention in days, or `null`/omit for the default window.
182
+ */
183
+ recentResults(retentionDays?: number | null): TryOnResult[];
184
+ /** Append one result to history (notifies subscribers, persists). */
185
+ appendResult(entry: TryOnResult): void;
186
+ /** Patch a history entry by id (notifies subscribers, persists). */
187
+ updateResult(id: string, patch: Partial<TryOnResult>): void;
188
+ /** Wholesale-replace the history slice (notifies subscribers, persists). */
189
+ replaceHistory(entries: TryOnResult[]): void;
190
+ /** Clear all history (notifies subscribers, persists). */
191
+ clearHistory(): void;
192
+ /**
193
+ * Shopper "delete my data": erase the shopper's data server-side, then clear
194
+ * the local history + tracked pending upload. On a confirmed backend erasure
195
+ * emits `tryon:data_erased`; on failure the local state is still cleared (the
196
+ * screen empties either way) and no event fires. Never rejects — a host can
197
+ * `await` it and navigate away unconditionally.
198
+ *
199
+ * Emits: `tryon:data_erased` (only on backend success).
200
+ */
201
+ deleteMyData(): Promise<void>;
202
+ /**
203
+ * Resolve a public share URL for a history entry: short-circuits on a
204
+ * cached/prebaked `entry.shareUrl` (zero network), otherwise creates a share
205
+ * link for its generation and writes the URL back into the entry (write-through
206
+ * cache). `opts` supplies DOM-derived context (effective domain, product URL).
207
+ * Rejects if the entry id is unknown or has no generation to share.
208
+ */
209
+ getShareUrl(entryId: string, opts?: GetShareUrlOptions): Promise<string>;
210
+ /**
211
+ * Rate a history entry: `1` (up), `-1` (down), `0` (clear). `reason` is an
212
+ * optional typed code paired with a thumbs-down. Writes the rating through to
213
+ * the entry and the backend. Never rejects (backend failures are swallowed).
214
+ *
215
+ * Emits: `widget:result_rated`, plus `widget:result_rating_reason` when a
216
+ * reason is supplied.
217
+ */
218
+ rateResult(entryId: string, rating: 1 | -1 | 0, reason?: RatingReasonCode | null): Promise<void>;
219
+ /** The merchant's current plan tier. */
220
+ getCurrentPlan(): Promise<CurrentPlanResponse>;
221
+ /**
222
+ * Whether the store may still spend generation credits. Cached for a short TTL
223
+ * and mirrored into `state.limits.creditsAllowed` (drives the `credits-expired`
224
+ * policy block).
225
+ */
226
+ checkCredits(): Promise<CheckCreditsResponse>;
227
+ /**
228
+ * Record a shopper email against the email-collection gate: sets identity
229
+ * (satisfying the `email-required` policy block) and sends the backend
230
+ * /shopper/email call.
231
+ *
232
+ * Emits: `widget:email_collected`.
233
+ */
234
+ collectEmail(request: CollectEmailRequest): Promise<CollectEmailResponse>;
235
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ export { SDK_VERSION } from "./version";
2
+ export { TryOnClient, PolicyBlockedError } from "./client";
3
+ export type { GenerateInput, GenerateOutput, TryOnClientConfig, TryOnClientDeps } from "./client";
4
+ export { createTryOnClient } from "./create-client";
5
+ export type { CreateTryOnClientOptions } from "./create-client";
6
+ export type { StoreSettings } from "./settings";
7
+ export { SETTINGS_CACHE_TTL_MS } from "./settings";
8
+ export { GenerationFailedError } from "./generation";
9
+ export type { GenerationErrorKind } from "./generation";
10
+ export type { GenlookTryOn } from "./public-api";
11
+ export type { PolicyAction, PolicyResult, BlockedReason } from "./policy";
12
+ export type { Listener } from "./store";
13
+ export type { NetworkInfo } from "./ports";
14
+ export type { CoreEvent, CoreEventType, CoreEventOf, CoreEventListener } from "./events";
15
+ export type { GetShareUrlOptions } from "./sharing";
16
+ export type { CurrentPlanResponse, CheckCreditsResponse, CollectEmailRequest, CollectEmailResponse, } from "./types";
17
+ export { DEFAULT_RETENTION_DAYS } from "./history";
18
+ export type { TryOnResult, GenerationContext } from "./history";
19
+ export type { Transport, TransportResponse, TransportRequestInit, MediaInput, KVStorage, ConsentSource, PageContext, TrackingBaseContext, TrackingIdentityContext, GenlookFirstTouch, IntegrationName, } from "./ports";
20
+ export { memoryStorage } from "./memory-storage";
21
+ export { createFetchTransport, GENLOOK_API_BASE_URL, GENLOOK_STORE_API_PATH, } from "./fetch-transport";
22
+ export type { FetchTransportOptions, FetchLike, FetchLikeInit, } from "./fetch-transport";
23
+ export { localStorageAdapter, isLocalStorageAvailable, createHydratedStorage, defaultStorage, } from "./storage-adapters";
24
+ export type { WebStorageLike, AsyncKVStore, HydratedStorageOptions, } from "./storage-adapters";
25
+ export { getAnonymousId, GENLOOK_ANONYMOUS_ID_STORAGE_KEY, GENLOOK_ANONYMOUS_ID_PREFIX, } from "./anonymous-id";
26
+ export type { UploadResult, PrepareUploadRequest, RatingReasonCode, ConsentSnapshot, } from "./types";
27
+ export type { TrackSink } from "./analytics";
28
+ export { staticConsent, createDefaultPageContext } from "./default-tracking";
29
+ export type { TrackingConsentMode, DefaultPageContextOptions, ScreenSize, } from "./default-tracking";
30
+ export type { UploadMeta, UploadDimensions } from "./entities";
31
+ export { MAX_UPLOAD_BYTES, ALLOWED_UPLOAD_MIME_TYPES, MIN_UPLOAD_WIDTH, MIN_UPLOAD_HEIGHT, isHeicLike, validateUpload, classifyUploadFailure, UploadRejectedError, } from "./upload";
32
+ export type { UploadRejectionReason, UploadValidationInput, UploadVerdict } from "./upload";
33
+ export type { CoreState, PhotoUpload, LegalConsent } from "./entities";
package/dist/public.js ADDED
@@ -0,0 +1,12 @@
1
+ export { SDK_VERSION } from "./version";
2
+ export { TryOnClient, PolicyBlockedError } from "./client";
3
+ export { createTryOnClient } from "./create-client";
4
+ export { SETTINGS_CACHE_TTL_MS } from "./settings";
5
+ export { GenerationFailedError } from "./generation";
6
+ export { DEFAULT_RETENTION_DAYS } from "./history";
7
+ export { memoryStorage } from "./memory-storage";
8
+ export { createFetchTransport, GENLOOK_API_BASE_URL, GENLOOK_STORE_API_PATH, } from "./fetch-transport";
9
+ export { localStorageAdapter, isLocalStorageAvailable, createHydratedStorage, defaultStorage, } from "./storage-adapters";
10
+ export { getAnonymousId, GENLOOK_ANONYMOUS_ID_STORAGE_KEY, GENLOOK_ANONYMOUS_ID_PREFIX, } from "./anonymous-id";
11
+ export { staticConsent, createDefaultPageContext } from "./default-tracking";
12
+ export { MAX_UPLOAD_BYTES, ALLOWED_UPLOAD_MIME_TYPES, MIN_UPLOAD_WIDTH, MIN_UPLOAD_HEIGHT, isHeicLike, validateUpload, classifyUploadFailure, UploadRejectedError, } from "./upload";