@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,226 @@
1
+ import type { UploadRejectionReason } from "./upload";
2
+ /**
3
+ * The per-reason detail a rejection carries, spread FLAT alongside the base
4
+ * fields. Deliberately a CLOSED set of optional keys rather than
5
+ * `Record<string, unknown>`: an open index signature widens `keyof` across the
6
+ * whole union to `string`, which silently defeats the no-PII assertion in
7
+ * analytics.ts and would let any field — an address included — ride along.
8
+ */
9
+ interface UploadRejectionExtra {
10
+ /** invalid_image_type */
11
+ allowedTypes?: readonly string[];
12
+ /** file_too_large */
13
+ maxFileSize?: number;
14
+ /** invalid_dimensions */
15
+ width?: number;
16
+ height?: number;
17
+ minWidth?: number;
18
+ minHeight?: number;
19
+ /** "dimension_read_failed" when the browser could not read the image at all.
20
+ * Distinct from the verdict's own `rejectionReason`. */
21
+ reason?: string;
22
+ /** invalid_dimensions_post_crop — the truncated backend message. */
23
+ error?: string;
24
+ }
25
+ /** The flat, BQ-ready projection of UploadMeta shared by `photo_submitted` and
26
+ * `photo_upload_started` — one shape so the two events can't drift. A closed
27
+ * interface (never an index signature): an open one would widen `keyof` and
28
+ * defeat the PII check in analytics.ts. */
29
+ export interface PhotoMetaFields {
30
+ fileSize: number;
31
+ mimeType: string;
32
+ uploadSource: string;
33
+ skipClientDimensionCheck: boolean;
34
+ /** The picked photo's own dimensions, when the host could read them. Carried
35
+ * because shape is the question behind "does this suit me": a full-length
36
+ * portrait and a square selfie are different inputs. Absent when unreadable
37
+ * or not checked. */
38
+ width?: number;
39
+ height?: number;
40
+ /** Seconds since the file was created (`File.lastModified`) at pick time —
41
+ * ~0 = taken in the chooser, large = an old roll photo. */
42
+ photo_age_s?: number;
43
+ /** `lastModified` fell inside the chooser's open→resolve window: the shopper
44
+ * took the photo via the chooser's camera path rather than picking from the
45
+ * roll. Host-observed (UploadMeta); absent when the host can't tell. */
46
+ captured_in_picker?: boolean;
47
+ /** Chooser open → file returned, in ms. Host-observed; absent when unknown. */
48
+ picker_duration_ms?: number;
49
+ /** The page was backgrounded while the chooser was open (Android camera path,
50
+ * app switches). Host-observed; absent when unknown. */
51
+ picker_backgrounded?: boolean;
52
+ /** How long the photo waited at the consent gate before release, in ms.
53
+ * Core-stamped on `flushStagedPhoto`; PRESENCE means this upload was held —
54
+ * an immediate (already-consented) upload omits it. */
55
+ held_duration_ms?: number;
56
+ }
57
+ /**
58
+ * Everything the core announces about its own lifecycle.
59
+ *
60
+ * Discriminated on `type` so a host can switch exhaustively and the compiler
61
+ * catches a new member. Names are `<subject>:<past-tense>` — an event is a fact
62
+ * about something that already happened, never a request.
63
+ */
64
+ export type CoreEvent =
65
+ /** A picked photo was handed to `stagePhoto` — the pipeline's entry edge,
66
+ * carrying everything known about the photo and how it was acquired. `held` is
67
+ * true when legal consent is still outstanding, so nothing has been sent and
68
+ * nothing will be until consent is accepted. Local pick-time rejections never
69
+ * reach the core, so the complete "chooser returned a file" edge is this event
70
+ * ∪ the host's local-rejection event. */
71
+ ({
72
+ type: "tryon:photo_submitted";
73
+ held: boolean;
74
+ } & PhotoMetaFields)
75
+ /** The tracked photo was dropped (`clearPendingUpload`), in flight or still held
76
+ * pre-consent. */
77
+ | {
78
+ type: "tryon:photo_discarded";
79
+ }
80
+ /** Bytes are now in flight (an immediate stage, or a held photo released by
81
+ * consent). Always preceded by `tryon:photo_submitted`. */
82
+ | ({
83
+ type: "tryon:photo_upload_started";
84
+ } & PhotoMetaFields)
85
+ /** The upload chain completed. Read the cropped URL from
86
+ * `selectCurrentUpload(getState())`. */
87
+ | {
88
+ type: "tryon:photo_upload_succeeded";
89
+ fileId: string;
90
+ fileSize: number;
91
+ mimeType: string;
92
+ uploadSource: string;
93
+ skipClientDimensionCheck: boolean;
94
+ duration_ms: number;
95
+ }
96
+ /** The upload failed for a non-verdict reason (network, prepare, storage).
97
+ * Retryable with the same photo, unlike a rejection. */
98
+ | {
99
+ type: "tryon:photo_upload_failed";
100
+ fileSize: number;
101
+ mimeType: string;
102
+ uploadSource: string;
103
+ error: string;
104
+ kind: "upload_failed";
105
+ code: "UPLOAD_FAILED";
106
+ /** Connectivity when the failure was recorded. ~90% of these arrive as the
107
+ * browser's generic "Failed to fetch" / "Load failed", which name no cause;
108
+ * these separate lost signal from a backgrounded page. Absent when the host
109
+ * supplies no NetworkInfo port. */
110
+ net_online?: boolean;
111
+ net_effective_type?: string;
112
+ net_visibility?: "visible" | "hidden";
113
+ }
114
+ /** The photo was refused — pre-upload validation or the server's post-crop
115
+ * verdict. Terminal for this photo: the shopper must pick another. `extra` is
116
+ * the per-reason detail (allowedTypes / maxFileSize / width+height+min* /
117
+ * dimension_read_failed / truncated error), spread flat. */
118
+ | ({
119
+ type: "tryon:photo_upload_rejected";
120
+ fileSize: number;
121
+ mimeType: string;
122
+ uploadSource: string;
123
+ rejectionReason: UploadRejectionReason;
124
+ kind: "upload_rejected";
125
+ } & UploadRejectionExtra)
126
+ /** Legal consent was recorded. Any photo held by `stagePhoto` is released in the
127
+ * same tick, so `tryon:upload_started` normally follows immediately. */
128
+ | {
129
+ type: "tryon:legal_consent_accepted";
130
+ consent_version: string;
131
+ }
132
+ /** Accepted and started (past the policy check and the dedup window, quota
133
+ * counted). A repeat inside the dedup TTL does NOT re-emit. */
134
+ | {
135
+ type: "tryon:generation_started";
136
+ generationId: string;
137
+ }
138
+ /** Produced an image. The history entry (when the caller passed a `context`) is
139
+ * appended before this fires. */
140
+ | {
141
+ type: "tryon:generation_succeeded";
142
+ generationId: string;
143
+ duration_ms: number;
144
+ productId: string;
145
+ variantId?: string;
146
+ }
147
+ /** Terminal error. `kind` is the GenerationErrorKind a host switches on; `code`
148
+ * is the backend wire code. */
149
+ | {
150
+ type: "tryon:generation_failed";
151
+ generationId: string;
152
+ kind: string;
153
+ code: string;
154
+ }
155
+ /** The generate was refused before anything was spent — by the CLIENT policy
156
+ * layer (reasons like "quota-exceeded" / "credits-expired" / "login-required")
157
+ * or by the SERVER pre-entity, where reason is the raw wire code
158
+ * (QUOTA_EXCEEDED / BILLING_NOT_ALLOWED / RATE_LIMIT_EXCEEDED /
159
+ * FITTING_ROOM_WEEKLY_LIMIT_EXCEEDED). Either way nothing ran and nothing was
160
+ * charged (the server path refunds the counted use). */
161
+ | {
162
+ type: "tryon:generation_blocked";
163
+ reason: string;
164
+ }
165
+ /** A result was rated. `rating` is 1 / -1, or 0 when withdrawn. */
166
+ | {
167
+ type: "tryon:result_rated";
168
+ generationId: string;
169
+ rating: number;
170
+ source: string;
171
+ }
172
+ /** A structured reason was given for a rating. */
173
+ | {
174
+ type: "tryon:result_feedback_given";
175
+ generationId: string;
176
+ reason: string;
177
+ source: string;
178
+ }
179
+ /** A share link was minted for a result. */
180
+ | {
181
+ type: "tryon:share_link_created";
182
+ generationId: string;
183
+ entryId: string;
184
+ }
185
+ /** An email was collected at the gate. NO ADDRESS — it reaches the backend
186
+ * through `POST /shopper/email`, and a copy here would only be a second system
187
+ * to secure and to honour erasure in. */
188
+ | {
189
+ type: "tryon:email_collected";
190
+ emailCollectionStep: number;
191
+ checkboxDisplayed: boolean;
192
+ marketingConsent?: boolean;
193
+ }
194
+ /** A shopper-initiated erasure completed; local state is cleared either way. */
195
+ | {
196
+ type: "tryon:data_erased";
197
+ };
198
+ export type CoreEventType = CoreEvent["type"];
199
+ /** Narrow a CoreEvent to one member by its `type`, so `on("tryon:photo_upload_succeeded", e => …)`
200
+ * hands the listener a payload that already knows it has `fileId`. */
201
+ export type CoreEventOf<T extends CoreEventType> = Extract<CoreEvent, {
202
+ type: T;
203
+ }>;
204
+ export type CoreEventListener<T extends CoreEventType> = (event: CoreEventOf<T>) => void;
205
+ /**
206
+ * Minimal typed emitter. No replay — events are edges, state is level, so read
207
+ * `getState()` for current state. A throwing listener is swallowed and does not
208
+ * stop the others: a host render bug must never break a try-on.
209
+ */
210
+ export declare class CoreEvents {
211
+ private readonly listeners;
212
+ private readonly anyListeners;
213
+ /** Subscribe to one event type. Returns an unsubscribe function. */
214
+ on<T extends CoreEventType>(type: T, listener: CoreEventListener<T>): () => void;
215
+ /**
216
+ * Subscribe to every event, current and future — for consumers generic over the
217
+ * event set (logging, host bridges, dev tooling). A new member of
218
+ * {@link CoreEvent} then needs no change here or in them.
219
+ *
220
+ * Runs after the type-specific listeners; same no-replay guarantee as {@link on}.
221
+ */
222
+ onAny(listener: (event: CoreEvent) => void): () => void;
223
+ /** Announce an event to its subscribers (type-specific first, then wildcard). */
224
+ emit(event: CoreEvent): void;
225
+ }
226
+ export {};
package/dist/events.js ADDED
@@ -0,0 +1,41 @@
1
+ export class CoreEvents {
2
+ listeners = new Map();
3
+ anyListeners = new Set();
4
+ on(type, listener) {
5
+ let set = this.listeners.get(type);
6
+ if (!set) {
7
+ set = new Set();
8
+ this.listeners.set(type, set);
9
+ }
10
+ const entry = listener;
11
+ set.add(entry);
12
+ return () => {
13
+ set.delete(entry);
14
+ };
15
+ }
16
+ onAny(listener) {
17
+ this.anyListeners.add(listener);
18
+ return () => {
19
+ this.anyListeners.delete(listener);
20
+ };
21
+ }
22
+ emit(event) {
23
+ const set = this.listeners.get(event.type);
24
+ if (set) {
25
+ for (const listener of [...set]) {
26
+ try {
27
+ listener(event);
28
+ }
29
+ catch {
30
+ }
31
+ }
32
+ }
33
+ for (const listener of [...this.anyListeners]) {
34
+ try {
35
+ listener(event);
36
+ }
37
+ catch {
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The core's default {@link Transport}: a plain `fetch` client for callers that
3
+ * reach `api.genlook.app` directly with a publishable key (native apps, headless
4
+ * or custom storefronts). `fetch` exists natively in browsers AND React Native,
5
+ * so one transport covers both.
6
+ *
7
+ * Storefront plugins keep their own transport (app-proxy prefix, signed params,
8
+ * redirect-prefix recovery); this one is the zero-wiring path for an integrator
9
+ * who only holds a key.
10
+ */
11
+ import type { Transport, TransportResponse } from "./ports";
12
+ /** Default Genlook API origin. */
13
+ export declare const GENLOOK_API_BASE_URL = "https://api.genlook.app";
14
+ /** Default path segment the storefront endpoints are mounted under. The older
15
+ * `store/v1/public` prefix is still served by the backend until 2027-03-01 and
16
+ * can be passed as `apiPath` by an integration pinned to it. */
17
+ export declare const GENLOOK_STORE_API_PATH = "storefront/v1";
18
+ /** Init the transport hands to the host `fetch`. Structural subset of RequestInit. */
19
+ export interface FetchLikeInit {
20
+ method?: string;
21
+ headers?: Record<string, string>;
22
+ body?: unknown;
23
+ keepalive?: boolean;
24
+ }
25
+ /** The host `fetch`. The browser/React Native `fetch` satisfies it structurally
26
+ * (`Response` satisfies {@link TransportResponse}). */
27
+ export type FetchLike = (url: string, init?: FetchLikeInit) => Promise<TransportResponse>;
28
+ /** A value that may be supplied directly or resolved lazily per request. */
29
+ type Resolvable<T> = T | (() => T);
30
+ export interface FetchTransportOptions {
31
+ /** Publishable key (`pk_…`), sent as `Authorization: Bearer …`. */
32
+ publishableKey: string;
33
+ /** API origin. Default {@link GENLOOK_API_BASE_URL}. */
34
+ baseUrl?: string;
35
+ /** Path the public endpoints live under. Default {@link GENLOOK_STORE_API_PATH}. */
36
+ apiPath?: string;
37
+ /** Anonymous device id → `x-genlook-anonymous-id`. Lazy form supported so the
38
+ * transport can be built before storage hydration resolves. */
39
+ anonymousId?: Resolvable<string | null | undefined>;
40
+ /** Known shopper → `x-genlook-customer-id`. */
41
+ customerId?: Resolvable<string | null | undefined>;
42
+ /** Known shopper email → `x-genlook-customer-email`. */
43
+ customerEmail?: Resolvable<string | null | undefined>;
44
+ /** Client build id → `x-genlook-widget-version`. */
45
+ widgetVersion?: string;
46
+ /** Extra headers on every proxied request (never on `putRaw`). Also the way
47
+ * to relabel the channel: a host that embeds this transport under its own
48
+ * name overrides `x-genlook-integration` / `x-genlook-integration-version`
49
+ * here. */
50
+ headers?: Record<string, string>;
51
+ /** Device fingerprint, read only when a request sets `includeFingerprint`. */
52
+ getFingerprint?: () => string | null | undefined;
53
+ /** Override the host `fetch` (tests, custom agents). Default `globalThis.fetch`. */
54
+ fetchImpl?: FetchLike;
55
+ }
56
+ /**
57
+ * Build the default fetch transport.
58
+ *
59
+ * `fetch(path, init)` hits `{baseUrl}/{apiPath}/{path}` with the publishable key
60
+ * and the identity headers. `putRaw(url, body, headers)` PUTs bytes to an
61
+ * absolute signed storage URL and deliberately carries NO Genlook headers — the
62
+ * GCS signed-URL contract breaks if extra headers are signed in.
63
+ */
64
+ export declare function createFetchTransport(options: FetchTransportOptions): Transport;
65
+ export {};
@@ -0,0 +1,112 @@
1
+ import { SDK_VERSION } from "./version";
2
+ export const GENLOOK_API_BASE_URL = "https://api.genlook.app";
3
+ export const GENLOOK_STORE_API_PATH = "storefront/v1";
4
+ function syntheticResponse(statusText) {
5
+ return {
6
+ ok: true,
7
+ status: 0,
8
+ statusText,
9
+ json: async () => ({}),
10
+ text: async () => "",
11
+ };
12
+ }
13
+ function resolve(value) {
14
+ return typeof value === "function" ? value() : value;
15
+ }
16
+ function joinUrl(base, path) {
17
+ if (!path)
18
+ return base;
19
+ const left = base.endsWith("/") ? base.slice(0, -1) : base;
20
+ const right = path.startsWith("/") ? path.slice(1) : path;
21
+ return `${left}/${right}`;
22
+ }
23
+ function getFetch(override) {
24
+ if (override)
25
+ return override;
26
+ const f = globalThis.fetch;
27
+ return typeof f === "function" ? f.bind(globalThis) : undefined;
28
+ }
29
+ function getSendBeacon() {
30
+ const nav = globalThis.navigator;
31
+ return nav && typeof nav.sendBeacon === "function"
32
+ ? nav.sendBeacon.bind(nav)
33
+ : undefined;
34
+ }
35
+ function beaconPayload(body, contentType) {
36
+ const BlobCtor = globalThis.Blob;
37
+ return BlobCtor ? new BlobCtor([body], { type: contentType }) : body;
38
+ }
39
+ export function createFetchTransport(options) {
40
+ const base = joinUrl(options.baseUrl ?? GENLOOK_API_BASE_URL, options.apiPath ?? GENLOOK_STORE_API_PATH);
41
+ const doFetch = (url, init) => {
42
+ const impl = getFetch(options.fetchImpl);
43
+ if (!impl) {
44
+ return Promise.reject(new Error("[Genlook] No fetch implementation available in this runtime."));
45
+ }
46
+ return impl(url, init);
47
+ };
48
+ const buildHeaders = (init) => {
49
+ const headers = {
50
+ "x-genlook-integration": "tryon_core",
51
+ "x-genlook-integration-version": SDK_VERSION,
52
+ ...options.headers,
53
+ ...init?.headers,
54
+ Authorization: `Bearer ${options.publishableKey}`,
55
+ };
56
+ const anonymousId = resolve(options.anonymousId);
57
+ if (anonymousId)
58
+ headers["x-genlook-anonymous-id"] = anonymousId;
59
+ const customerId = resolve(options.customerId);
60
+ if (customerId)
61
+ headers["x-genlook-customer-id"] = customerId;
62
+ const customerEmail = resolve(options.customerEmail);
63
+ if (customerEmail)
64
+ headers["x-genlook-customer-email"] = customerEmail;
65
+ if (options.widgetVersion) {
66
+ headers["x-genlook-widget-version"] = options.widgetVersion;
67
+ }
68
+ if (init?.includeFingerprint) {
69
+ const fingerprint = options.getFingerprint?.();
70
+ if (fingerprint)
71
+ headers["x-genlook-fingerprint"] = fingerprint;
72
+ }
73
+ return headers;
74
+ };
75
+ return {
76
+ fetch(path, init) {
77
+ const url = joinUrl(base, path);
78
+ const headers = buildHeaders(init);
79
+ if (init?.beacon) {
80
+ const body = init.body ?? "";
81
+ const send = getSendBeacon();
82
+ if (send) {
83
+ const contentType = headers["Content-Type"] ?? "application/json";
84
+ try {
85
+ if (send(url, beaconPayload(body, contentType))) {
86
+ return Promise.resolve(syntheticResponse("beacon"));
87
+ }
88
+ }
89
+ catch {
90
+ }
91
+ }
92
+ if (!getFetch(options.fetchImpl)) {
93
+ return Promise.resolve(syntheticResponse("beacon-unsupported"));
94
+ }
95
+ return doFetch(url, {
96
+ method: init.method ?? "POST",
97
+ headers,
98
+ body: init.body,
99
+ keepalive: true,
100
+ });
101
+ }
102
+ return doFetch(url, {
103
+ method: init?.method,
104
+ headers,
105
+ body: init?.body,
106
+ });
107
+ },
108
+ putRaw(url, body, headers) {
109
+ return doFetch(url, { method: "PUT", headers, body });
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,132 @@
1
+ import type { Transport } from "./ports";
2
+ import type { FittingRoomRequest, FittingRoomResponse, GenerationStatusResponse, CurrentPlanResponse, CheckCreditsResponse, RatingReasonCode } from "./types";
3
+ import type { Store } from "./store";
4
+ import type { CoreState, GenerationJob, GenError } from "./entities";
5
+ /** Failure category the host UI routes on. Derived from the backend's
6
+ * structured codes, never from message text (see classifyErrorKind). */
7
+ export type GenerationErrorKind = "quota" | "weekly_limit" | "rate_limited" | "overloaded" | "creation_failed" | "upload_failed" | "failed";
8
+ /**
9
+ * Derive the error kind from the backend's structured code. The message-text
10
+ * sniff at the bottom is the ONLY message-based classification left in the
11
+ * codebase: it handles Vertex capacity errors predating structured codes
12
+ * (`out of capacity` / RESOURCE_EXHAUSTED+429 with no errorCode). Everything
13
+ * else switches on codes.
14
+ */
15
+ export declare function classifyErrorKind(code: string, message: string): GenerationErrorKind;
16
+ /**
17
+ * Thrown for every generation failure: backend failure codes on job creation,
18
+ * a FAILED poll status, polling timeouts, and upload coordination failures.
19
+ * `code` is the backend's stable wire code (e.g. GENERATION_FAILED,
20
+ * MISSING_REQUIRED_CLASSIFICATION); `kind` is the routing category derived from
21
+ * it; `message` is backend text for logs only and must never be rendered.
22
+ *
23
+ * CROSS-BUNDLE CONTRACT: the error crosses the boot/UI bundle boundary, so
24
+ * hosts recognise it by `error.name === "GenerationFailedError"` and read
25
+ * `kind`/`code` off the object — never `instanceof` (which would not match a
26
+ * separately bundled copy of this class).
27
+ *
28
+ * Defined here (the module that throws it, matching the pre-split layout) rather
29
+ * than a sibling module, so this file carries NO runtime value import — the
30
+ * repo's strip-only `node --test` cannot resolve extensionless sibling imports,
31
+ * and `.ts` extensions in source break the `tsc` build (TS5097).
32
+ */
33
+ export declare class GenerationFailedError extends Error {
34
+ readonly code: string;
35
+ readonly kind: GenerationErrorKind;
36
+ constructor(code: string, message: string, kind?: GenerationErrorKind);
37
+ }
38
+ export declare function createFittingRoomJob(transport: Transport, request: FittingRoomRequest): Promise<FittingRoomResponse>;
39
+ export declare function getGenerationStatus(transport: Transport, jobId: string): Promise<GenerationStatusResponse>;
40
+ export declare function pollGenerationStatus(transport: Transport, jobId: string, onStatusChange?: (status: GenerationStatusResponse) => void, pollInterval?: number, maxAttempts?: number): Promise<GenerationStatusResponse>;
41
+ export declare function getCurrentPlan(transport: Transport): Promise<CurrentPlanResponse>;
42
+ export declare function checkCredits(transport: Transport): Promise<CheckCreditsResponse>;
43
+ export declare function rateGeneration(transport: Transport, generationId: string, rating: 1 | -1 | 0, reason?: RatingReasonCode | null): Promise<{
44
+ rating: number | null;
45
+ ratedAt: string | null;
46
+ ratingReason: string | null;
47
+ }>;
48
+ export interface RateEngineDeps {
49
+ transport: Transport;
50
+ store: Store<CoreState>;
51
+ /** Emits widget:result_rated (bound to analytics by the client). */
52
+ emitResultRated: (generationId: string, rating: 1 | -1 | 0) => void;
53
+ /** Emits widget:result_rating_reason (bound to analytics by the client). */
54
+ emitResultRatingReason: (generationId: string, reason: RatingReasonCode) => void;
55
+ }
56
+ /**
57
+ * Rate a history entry. Without a reason: optimistic rating write-through
58
+ * (clearing any previous reason), emit, send — and revert the rating if the
59
+ * backend rejects. With a reason (thumbs-down follow-up): write the reason
60
+ * through, emit, send. Never throws — backend failures are logged.
61
+ */
62
+ export declare function rateResult(deps: RateEngineDeps, entryId: string, rating: 1 | -1 | 0, reason?: RatingReasonCode | null): Promise<void>;
63
+ export interface GenerationEngineDeps {
64
+ transport: Transport;
65
+ store: Store<CoreState>;
66
+ now: () => number;
67
+ /** Emits the generation_start pair (bound to analytics by the client; spied in tests). */
68
+ emitGenerationStart: (generationId: string) => void;
69
+ /** Emits tryon:generation_succeeded on the done transition. */
70
+ emitGenerationSucceeded: (input: {
71
+ generationId: string;
72
+ durationMs: number;
73
+ productId: string;
74
+ variantId?: string;
75
+ }) => void;
76
+ /** Emits tryon:generation_failed on the error transition. */
77
+ emitGenerationFailed: (input: {
78
+ generationId: string;
79
+ kind: string;
80
+ code: string;
81
+ }) => void;
82
+ /** Pre-entity server policy denial (see blockedReasonFor) — the server-side
83
+ * twin of the client policy layer's tryon:generation_blocked. */
84
+ emitGenerationBlocked: (reason: string) => void;
85
+ pollInterval?: number;
86
+ maxAttempts?: number;
87
+ }
88
+ export interface GenerationInput {
89
+ userImageId: string;
90
+ productId: string;
91
+ variantId?: string;
92
+ /** The quota-window timestamp the client counted for this generation on
93
+ * acceptance (client.recordUsage()'s return value). Stamped onto the job entity
94
+ * so a terminal-error refund removes exactly this entry; also used for the
95
+ * bare pre-entity refund when the backend rejects job creation. */
96
+ usageAt?: number;
97
+ }
98
+ /** Entity-level error classification. Navigation is owned by the host's switch
99
+ * on the rethrown error's `kind`; this only tags the store entity. Switches on
100
+ * the typed error's kind/code — never on message text. */
101
+ export declare function classifyGenerationError(err: unknown): GenError;
102
+ /**
103
+ * Create a generation job and drive it to a terminal state, updating the store
104
+ * on every transition. Fires widget:generation_start on the requested→generating
105
+ * boundary. Throws the typed generation errors a host's error→navigation switch
106
+ * branches on; the store also records a typed error entity.
107
+ */
108
+ export declare function runGeneration(deps: GenerationEngineDeps, input: GenerationInput): Promise<{
109
+ imageUrl: string;
110
+ generationId: string;
111
+ }>;
112
+ /**
113
+ * Resume a rehydrated non-terminal job: fire the start event if it was never
114
+ * reported (crash-before-flush), poll to a terminal state, update the store.
115
+ * Never throws — resumption is a background continuation.
116
+ */
117
+ export declare function resumeGeneration(deps: GenerationEngineDeps, generationId: string): Promise<void>;
118
+ /** Mark a completed result as seen (host called acknowledge). */
119
+ export declare function acknowledgeGeneration(store: Store<CoreState>, generationId: string): void;
120
+ /**
121
+ * Schedule resumption of rehydrated in-flight jobs on the NEXT microtask rather
122
+ * than synchronously in the client constructor.
123
+ *
124
+ * Contract: a composition root (boot.ts — and the Woo/Presta boots, where NO
125
+ * pre-boot stub exists at all) finishes publishing the global runtime SYNCHRONOUSLY
126
+ * AFTER `new TryOnClient(...)` returns. The resume fetch derefs that global's
127
+ * transport (cabinFetch → window.Genlook.cabin.http.fetch), so resuming inline in
128
+ * the ctor would deref a not-yet-published global and mark every
129
+ * refresh-mid-generation job errored. One microtask hop guarantees the transport
130
+ * deps are live before the first status poll fires.
131
+ */
132
+ export declare function scheduleInFlightResumes(deps: GenerationEngineDeps, jobs: GenerationJob[]): void;