@arkyc/widget 1.2.11 → 2.0.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.
package/dist/client.d.mts CHANGED
@@ -20,18 +20,16 @@ interface ClientSession {
20
20
  workflow?: WorkflowConfig | null;
21
21
  }
22
22
  /**
23
- * Realtime connection info for this session (Phase 16). `transport` selects how
24
- * the widget watches the session: `pusher`/`firebase` subscribe to `channel`;
25
- * `polling` (and `off`/`memory`) mean poll the session endpoint instead. The
26
- * remaining fields carry the active transport's public connection params; `token`
27
- * is a per-session Firebase custom token when applicable.
23
+ * How the widget watches this session live: the API's public realtime config
24
+ * (`transport` plus that transport's settings) and the session's own channel.
25
+ * Transports the widget does not stream over mean polling the session instead.
28
26
  */
29
27
  interface ClientRealtime {
30
- transport: 'pusher' | 'firebase' | 'polling' | 'off' | 'memory';
28
+ transport: 'pusher' | 'firebase' | 'firebase-database' | 'polling' | 'memory';
31
29
  /** The private channel this session's events publish to. */
32
30
  channel: string;
33
- /** Per-session Firebase custom token (firebase transport only). */
34
- token?: string | null;
31
+ pusher?: Record<string, unknown>;
32
+ 'firebase-database'?: Record<string, unknown>;
35
33
  [param: string]: unknown;
36
34
  }
37
35
  /** Whether (and where) the widget may offer cross-device handoff. */
@@ -115,9 +113,15 @@ declare class WidgetApiError extends Error {
115
113
  readonly error?: ApiErrorKey;
116
114
  constructor(message: string, status: number, error?: ApiErrorKey);
117
115
  }
116
+ /** Where {@link ArkycClient} keeps this browser's device token between page loads. */
117
+ type ClientTokenStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
118
118
  /** Options for {@link ArkycClient}. */
119
119
  interface ArkycClientOptions {
120
- /** Short-lived client token minted for this session. */
120
+ /**
121
+ * The session's single-use launch token (from session creation) or a handoff
122
+ * token (from a cross-device QR code). The client exchanges it for this
123
+ * browser's device token on its first request.
124
+ */
121
125
  token: string;
122
126
  /**
123
127
  * The Client API **base** the widget appends each endpoint to — `{baseUrl}/session`,
@@ -131,6 +135,11 @@ interface ArkycClientOptions {
131
135
  baseUrl?: string;
132
136
  /** Injectable for testing; defaults to the global `fetch`. */
133
137
  fetch?: typeof fetch;
138
+ /**
139
+ * Keeps the device token so a reload or second tab can continue the session.
140
+ * Defaults to `localStorage`; pass `null` to keep it in memory only.
141
+ */
142
+ storage?: ClientTokenStorage | null;
134
143
  }
135
144
  /**
136
145
  * A thin, framework-agnostic client for the Arkyc Client/Widget API. Drives a
@@ -141,7 +150,22 @@ declare class ArkycClient {
141
150
  private readonly token;
142
151
  private readonly baseUrl;
143
152
  private readonly fetchImpl;
153
+ private readonly storage;
154
+ private deviceToken?;
144
155
  constructor(options: ArkycClientOptions);
156
+ /**
157
+ * This browser's device token, claiming the launch or handoff token the first
158
+ * time. Other Client API calls (and realtime channel auth) send this token.
159
+ *
160
+ * @returns
161
+ */
162
+ authToken(): Promise<string>;
163
+ /**
164
+ * Mint the single-use token a cross-device QR code carries.
165
+ *
166
+ * @returns
167
+ */
168
+ createHandoffToken(): Promise<string>;
145
169
  /**
146
170
  * The resolved Client API base (always origin-qualified: the absolute/proxy
147
171
  * value, or the hosted default when `baseUrl` was omitted). Callers that need
@@ -193,6 +217,12 @@ declare class ArkycClient {
193
217
  */
194
218
  complete(input?: CompleteInput): Promise<ClientSession>;
195
219
  private request;
220
+ /** The storage key for the device token claimed from this client's token. */
221
+ private get storageKey();
222
+ private resolveDeviceToken;
223
+ private readStoredToken;
224
+ private writeStoredToken;
225
+ private send;
196
226
  }
197
227
  //#endregion
198
228
  export { ArkycClient, ArkycClientOptions, ClientRealtime, ClientSession, ProviderSignalHints, WidgetApiError };
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;UAYiB,aAAA;EACf,EAAA;EACA,MAAA,EAAQ,kBAAA;EACR,UAAA;EADQ;EAGR,aAAA,GAAgB,YAAA;EAEM;EAAtB,mBAAA,GAAsB,iBAAA;EAIX;EAFX,OAAA,GAAU,aAAA;EAMC;EAJX,QAAA,GAAW,cAAA;EAIc;EAFzB,QAAA,GAAW,eAAA;EAXX;EAaA,QAAA,GAAW,cAAA;AAAA;;;;;;;;UAUI,cAAA;EACf,SAAA;EAbW;EAeX,OAAA;EAbW;EAeX,KAAA;EAAA,CACC,KAAA;AAAA;;UAIc,aAAA;EACf,OAAA;EAVA;EAYA,aAAA;EARA;EAUA,GAAA;AAAA;AATc;AAIhB;;;AAJgB,UAgBC,mBAAA;EACf,aAAA;EACA,cAAA;EACA,OAAA;EACA,cAAA;EACA,eAAA;EACA,cAAA;EACA,eAAA;EACA,iBAAA;EACA,aAAA;EACA,cAAA;AAAA;;UAIe,YAAA;EATf;EAWA,KAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;EAVc;EAYd,OAAA;EARe;EAUf,GAAA,GAAM,IAAA;;EAEN,QAAA;EACA,SAAA;EACA,OAAA,GAAU,mBAAmB;AAAA;;UAId,kBAAA;EACf,KAAA,GAAQ,IAAA;EACR,OAAA;EACA,YAAA,GAAe,YAAA;EACf,OAAA,GAAU,mBAAA;AAAA;;UAIK,iBAAA;EACf,KAAA,GAAQ,IAAA;EACR,OAAA;EACA,YAAA,GAAe,YAAY;AAAA;;UAIZ,aAAA;EACf,MAAA,GAAS,IAAA;EAZC;EAcV,KAAA,GAAQ,IAAA;EAdqB;EAgB7B,IAAA,GAAO,YAAA;EAnBC;EAqBR,UAAA,GAAa,iBAAA;EACb,OAAA,GAAU,mBAAA;AAAA;;UAIK,aAAA;EACf,OAAA,GAAU,mBAAmB;AAAA;AApB/B;;;AAAA,cA0Ba,cAAA,SAAuB,KAAA;EAAA,SACzB,MAAA;EA1BD;;;;EAAA,SA+BC,KAAA,GAAQ,WAAA;cAEL,OAAA,UAAiB,MAAA,UAAgB,KAAA,GAAQ,WAAA;AAAA;;UAStC,kBAAA;EAnCN;EAqCT,KAAA;EAjCO;;;;;;;;;EA2CP,OAAA;EA3CO;EA6CP,KAAA,UAAe,KAAK;AAAA;;;;;;cAkDT,WAAA;EAAA,iBACM,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,SAAA;cAEL,OAAA,EAAS,kBAAA;;;;;;;AA9E2C;MA8F5D,IAAA;EArF6B;;;;;EA8FjC,UAAA,IAAc,OAAA,CAAQ,aAAA;EAhFP;;AAAK;AAkDtB;;;EAwCE,mBAAA,CAAoB,KAAA,EAAO,kBAAA,GAAqB,OAAA,CAAQ,aAAA;EAVlC;;;;;;EAyBtB,kBAAA,CAAmB,KAAA,EAAO,iBAAA,GAAoB,OAAA,CAAQ,aAAA;EAAR;;;;;;EAc9C,cAAA,CAAe,KAAA,EAAO,aAAA,GAAgB,OAAA,CAAQ,aAAA;EA2C9B;;;;;;;EA1BhB,aAAA,CAAc,KAAA,EAAO,YAAA,GAAe,OAAA,CAAQ,aAAA;;;;;;;EA0B5C,QAAA,CAAS,KAAA,GAAO,aAAA,GAAqB,OAAA,CAAQ,aAAA;EAAA,QAI/B,OAAA;AAAA"}
1
+ {"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;UAYiB,aAAA;EACf,EAAA;EACA,MAAA,EAAQ,kBAAA;EACR,UAAA;EADQ;EAGR,aAAA,GAAgB,YAAA;EAEM;EAAtB,mBAAA,GAAsB,iBAAA;EAIX;EAFX,OAAA,GAAU,aAAA;EAMC;EAJX,QAAA,GAAW,cAAA;EAIc;EAFzB,QAAA,GAAW,eAAA;EAXX;EAaA,QAAA,GAAW,cAAA;AAAA;;;;;;UAQI,cAAA;EACf,SAAA;EAbW;EAeX,OAAA;EACA,MAAA,GAAS,MAAA;EACT,mBAAA,GAAsB,MAAM;EAAA,CAC3B,KAAA;AAAA;AAdwB;AAAA,UAkBV,aAAA;EACf,OAAA;;EAEA,aAAA;EAZA;EAcA,GAAA;AAAA;;;;;UAOe,mBAAA;EACf,aAAA;EACA,cAAA;EACA,OAAA;EACA,cAAA;EACA,eAAA;EACA,cAAA;EACA,eAAA;EACA,iBAAA;EACA,aAAA;EACA,cAAA;AAAA;;UAIe,YAAA;EAdmB;EAgBlC,KAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;EAdA;EAgBA,OAAA;EAdA;EAgBA,GAAA,GAAM,IAAA;EAdN;EAgBA,QAAA;EACA,SAAA;EACA,OAAA,GAAU,mBAAmB;AAAA;;UAId,kBAAA;EACf,KAAA,GAAQ,IAAA;EACR,OAAA;EACA,YAAA,GAAe,YAAA;EACf,OAAA,GAAU,mBAAA;AAAA;;UAIK,iBAAA;EACf,KAAA,GAAQ,IAAA;EACR,OAAA;EACA,YAAA,GAAe,YAAY;AAAA;;UAIZ,aAAA;EACf,MAAA,GAAS,IAAA;EAhBM;EAkBf,KAAA,GAAQ,IAAA;;EAER,IAAA,GAAO,YAAA;EAjBQ;EAmBf,UAAA,GAAa,iBAAA;EACb,OAAA,GAAU,mBAAA;AAAA;;UAIK,aAAA;EACf,OAAA,GAAU,mBAAmB;AAAA;;;;cAMlB,cAAA,SAAuB,KAAA;EAAA,SACzB,MAAA;EA3BM;;;;EAAA,SAgCN,KAAA,GAAQ,WAAA;cAEL,OAAA,UAAiB,MAAA,UAAgB,KAAA,GAAQ,WAAA;AAAA;;KAS3C,kBAAA,GAAqB,IAAI,CAAC,OAAA;;UAGrB,kBAAA;EAvCA;;;;;EA6Cf,KAAA;EAtCa;;;;;;;;;EAgDb,OAAA;EAhDA;EAkDA,KAAA,UAAe,KAAA;EAjDf;;;AAA6B;EAsD7B,OAAA,GAAU,kBAAkB;AAAA;;;;;;cAqEjB,WAAA;EAAA,iBACM,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,SAAA;EAAA,iBACA,OAAA;EAAA,QACT,WAAA;cAEI,OAAA,EAAS,kBAAA;EAtGO;;;AAAe;AAG7C;;EAmHE,SAAA,IAAa,OAAA;EA5Fe;;;;;EA0GtB,kBAAA,IAAsB,OAAA;EA1GlB;;AAAkB;AAqE9B;;;;EArEY,IAuHN,IAAA;EAbwB;;;;;EAsB5B,UAAA,IAAc,OAAA,CAAQ,aAAA;EAyBI;;;;;;EAf1B,mBAAA,CAAoB,KAAA,EAAO,kBAAA,GAAqB,OAAA,CAAQ,aAAA;EA8CZ;;;;;;EA/B5C,kBAAA,CAAmB,KAAA,EAAO,iBAAA,GAAoB,OAAA,CAAQ,aAAA;EAnFrC;;;;;;EAiGjB,cAAA,CAAe,KAAA,EAAO,aAAA,GAAgB,OAAA,CAAQ,aAAA;EA3FlC;;;;;;;EA4GZ,aAAA,CAAc,KAAA,EAAO,YAAA,GAAe,OAAA,CAAQ,aAAA;EAxDtB;;;;;;EAkFtB,QAAA,CAAS,KAAA,GAAO,aAAA,GAAqB,OAAA,CAAQ,aAAA;EAAA,QAI/B,OAAA;EA7DK;EAAA,YAkEP,UAAA;EAAA,QAIE,kBAAA;EAAA,QAUN,eAAA;EAAA,QAgBA,gBAAA;EAAA,QAQM,IAAA;AAAA"}
package/dist/client.mjs CHANGED
@@ -16,6 +16,18 @@ var WidgetApiError = class extends Error {
16
16
  this.error = error;
17
17
  }
18
18
  };
19
+ /**
20
+ * `localStorage` when the page may use it, otherwise null.
21
+ *
22
+ * @returns
23
+ */
24
+ function defaultStorage() {
25
+ try {
26
+ return globalThis.localStorage ?? null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
19
31
  const DEFAULT_CLIENT_BASE_URL = "https://api.arkyc.toneflix.net/api/v1/client";
20
32
  /**
21
33
  * Resolve the request base: an explicit `baseUrl` overrides the build-time
@@ -48,6 +60,8 @@ var ArkycClient = class {
48
60
  token;
49
61
  baseUrl;
50
62
  fetchImpl;
63
+ storage;
64
+ deviceToken;
51
65
  constructor(options) {
52
66
  if (!options.token) throw new Error("ArkycClient requires a client `token`.");
53
67
  this.token = options.token;
@@ -55,6 +69,28 @@ var ArkycClient = class {
55
69
  const f = options.fetch ?? globalThis.fetch;
56
70
  if (!f) throw new Error("No `fetch` implementation available.");
57
71
  this.fetchImpl = f.bind(globalThis);
72
+ this.storage = options.storage === void 0 ? defaultStorage() : options.storage;
73
+ }
74
+ /**
75
+ * This browser's device token, claiming the launch or handoff token the first
76
+ * time. Other Client API calls (and realtime channel auth) send this token.
77
+ *
78
+ * @returns
79
+ */
80
+ authToken() {
81
+ this.deviceToken ??= this.resolveDeviceToken().catch((error) => {
82
+ this.deviceToken = void 0;
83
+ throw error;
84
+ });
85
+ return this.deviceToken;
86
+ }
87
+ /**
88
+ * Mint the single-use token a cross-device QR code carries.
89
+ *
90
+ * @returns
91
+ */
92
+ async createHandoffToken() {
93
+ return (await this.send("POST", "/handoff", await this.authToken(), {})).token;
58
94
  }
59
95
  /**
60
96
  * The resolved Client API base (always origin-qualified: the absolute/proxy
@@ -150,7 +186,36 @@ var ArkycClient = class {
150
186
  return this.request("POST", "/complete", input.signals ?? {});
151
187
  }
152
188
  async request(method, path, body) {
153
- const headers = { "X-Client-Token": this.token };
189
+ return await this.send(method, path, await this.authToken(), body);
190
+ }
191
+ /** The storage key for the device token claimed from this client's token. */
192
+ get storageKey() {
193
+ return `arkyc:device:${this.token}`;
194
+ }
195
+ async resolveDeviceToken() {
196
+ const stored = this.readStoredToken();
197
+ if (stored) return stored;
198
+ const claimed = await this.send("POST", "/claim", this.token, {});
199
+ this.writeStoredToken(claimed);
200
+ return claimed.token;
201
+ }
202
+ readStoredToken() {
203
+ try {
204
+ const raw = this.storage?.getItem(this.storageKey);
205
+ if (!raw) return null;
206
+ const stored = JSON.parse(raw);
207
+ if (new Date(stored.expires_at).getTime() > Date.now()) return stored.token;
208
+ this.storage?.removeItem(this.storageKey);
209
+ } catch {}
210
+ return null;
211
+ }
212
+ writeStoredToken(token) {
213
+ try {
214
+ this.storage?.setItem(this.storageKey, JSON.stringify(token));
215
+ } catch {}
216
+ }
217
+ async send(method, path, token, body) {
218
+ const headers = { "X-Client-Token": token };
154
219
  let payload;
155
220
  if (body instanceof FormData) payload = body;
156
221
  else if (body) {
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n ApiErrorKey,\n CaptureModel,\n DocumentType,\n LivenessChallenge,\n LivenessMode,\n ProjectBranding,\n VerificationStatus,\n WorkflowConfig,\n} from '@arkyc/types'\n\n/** The session view exposed by the Client/Widget API (`GET /v1/client/session`). */\nexport interface ClientSession {\n id: string\n status: VerificationStatus\n expires_at: string\n /** The capture flow this session runs (Phase 17). */\n capture_model?: CaptureModel\n /** The active-liveness challenge sequence to prompt the user through. */\n liveness_challenges?: LivenessChallenge[]\n /** Cross-device handoff config resolved from the project setting. */\n handoff?: ClientHandoff\n /** How to watch this session live (push transport params or `polling`). */\n realtime?: ClientRealtime\n /** Project branding (colours/logo/name) so the widget themes itself at runtime. */\n branding?: ProjectBranding | null\n /** The applied workflow (ordered, toggleable stages + skip_ocr), or null for the default flow. */\n workflow?: WorkflowConfig | null\n}\n\n/**\n * Realtime connection info for this session (Phase 16). `transport` selects how\n * the widget watches the session: `pusher`/`firebase` subscribe to `channel`;\n * `polling` (and `off`/`memory`) mean poll the session endpoint instead. The\n * remaining fields carry the active transport's public connection params; `token`\n * is a per-session Firebase custom token when applicable.\n */\nexport interface ClientRealtime {\n transport: 'pusher' | 'firebase' | 'polling' | 'off' | 'memory'\n /** The private channel this session's events publish to. */\n channel: string\n /** Per-session Firebase custom token (firebase transport only). */\n token?: string | null\n [param: string]: unknown\n}\n\n/** Whether (and where) the widget may offer cross-device handoff. */\nexport interface ClientHandoff {\n enabled: boolean\n /** Whether a desktop user may continue on the desktop device instead of handing off. */\n allow_desktop: boolean\n /** First-party hosted page the QR points to (the phone resumes the session there). */\n url: string\n}\n\n/**\n * Mock-driver signal hints. Real provider drivers ignore these; the `mock`\n * drivers use them to make the demo/playground flow deterministic.\n */\nexport interface ProviderSignalHints {\n quality_score?: number\n ocr_confidence?: number\n expired?: boolean\n liveness_score?: number\n liveness_passed?: boolean\n multiple_faces?: boolean\n face_similarity?: number\n face_match_passed?: boolean\n address_score?: number\n address_passed?: boolean\n}\n\n/** Input for the address-verification submission (opt-in stage). */\nexport interface AddressInput {\n /** The claimed address the user entered. */\n line1?: string | null\n line2?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n /** ISO 3166-1 alpha-2 country code. */\n country?: string | null\n /** Proof-of-address document image. */\n poa?: Blob | null\n /** Captured device coordinates. */\n latitude?: number | null\n longitude?: number | null\n signals?: ProviderSignalHints\n}\n\n/** Input for the document-front submission. */\nexport interface DocumentFrontInput {\n image?: Blob | null\n country?: string | null\n documentType?: DocumentType | null\n signals?: ProviderSignalHints\n}\n\n/** Input for the document-back submission. */\nexport interface DocumentBackInput {\n image?: Blob | null\n country?: string | null\n documentType?: DocumentType | null\n}\n\n/** Input for the liveness submission (passive selfie or active challenge video). */\nexport interface LivenessInput {\n selfie?: Blob | null\n /** Recorded challenge video (active mode). */\n video?: Blob | null\n /** Passive (selfie) or active (challenge video). Defaults to passive. */\n mode?: LivenessMode\n /** The challenge sequence the user performed, in order (active mode). */\n challenges?: LivenessChallenge[]\n signals?: ProviderSignalHints\n}\n\n/** Input for completing the session (runs face-match + decision). */\nexport interface CompleteInput {\n signals?: ProviderSignalHints\n}\n\n/**\n * Raised when the Client API returns a non-2xx response.\n */\nexport class WidgetApiError extends Error {\n readonly status: number\n /**\n * Stable error key (e.g. `session_expired`) for errors Arkyc\n * deliberately raises. Undefined for unexpected errors.\n */\n readonly error?: ApiErrorKey\n\n constructor(message: string, status: number, error?: ApiErrorKey) {\n super(message)\n this.name = 'WidgetApiError'\n this.status = status\n this.error = error\n }\n}\n\n/** Options for {@link ArkycClient}. */\nexport interface ArkycClientOptions {\n /** Short-lived client token minted for this session. */\n token: string\n /**\n * The Client API **base** the widget appends each endpoint to — `{baseUrl}/session`,\n * `{baseUrl}/document/front`, etc. (no version prefix is added; `baseUrl` owns it).\n *\n * **Optional** — defaults to the hosted Arkyc API ({@link DEFAULT_CLIENT_BASE_URL}),\n * so the hosted product needs no config. Override only to self-host or proxy: a\n * relative path resolves against the current origin (same-origin, no CORS); an\n * absolute URL is used as-is (the API must allow your origin via CORS).\n */\n baseUrl?: string\n /** Injectable for testing; defaults to the global `fetch`. */\n fetch?: typeof fetch\n}\n\n/**\n * The default Client API base, used when no `baseUrl` is passed — so the hosted\n * product is zero-config: the browser calls it cross-origin with the client\n * token (the API allows it via CORS).\n *\n * Baked at **build time** from the `ARKYC_API_URL` env (via the tsdown `define`),\n * falling back to the hosted Arkyc API. Self-hosters set `ARKYC_API_URL` when\n * building the widget; integrators on the published package override per call\n * with `baseUrl`. Referenced through `typeof` so running the source directly\n * (tests) without the define is safe.\n */\ndeclare const __ARKYC_API_BASE__: string | undefined\nexport const DEFAULT_CLIENT_BASE_URL =\n (typeof __ARKYC_API_BASE__ === 'string' ? __ARKYC_API_BASE__ : '') || 'https://api.arkyc.toneflix.net/api/v1/client'\n\n/**\n * Resolve the request base: an explicit `baseUrl` overrides the build-time\n * {@link DEFAULT_CLIENT_BASE_URL}. The chosen value is used as-is when absolute\n * (cross-origin, CORS); a relative path resolves against the current origin\n * (same-origin proxy, no CORS).\n *\n * @param baseUrl\n * @returns\n */\nexport function resolveClientBaseUrl(baseUrl?: string): string {\n const raw = (baseUrl ?? '').trim().replace(/\\/$/, '')\n if (!raw) return DEFAULT_CLIENT_BASE_URL\n if (/^https?:\\/\\//i.test(raw)) return raw\n const origin = (globalThis.location?.origin ?? '').replace(/\\/$/, '')\n return `${origin}${raw.startsWith('/') ? '' : '/'}${raw}`\n}\n\ntype EnvelopeNumber = number | boolean\n\nfunction appendSignals(form: FormData, signals?: ProviderSignalHints): void {\n if (!signals) return\n for (const [key, value] of Object.entries(signals)) {\n if (value == null) continue\n form.append(key, String(value as EnvelopeNumber))\n }\n}\n\n/**\n * A thin, framework-agnostic client for the Arkyc Client/Widget API. Drives a\n * single verification session with a short-lived client token. All responses\n * are unwrapped from the standard `{ status, message, data }` envelope.\n */\nexport class ArkycClient {\n private readonly token: string\n private readonly baseUrl: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: ArkycClientOptions) {\n if (!options.token) throw new Error('ArkycClient requires a client `token`.')\n this.token = options.token\n this.baseUrl = resolveClientBaseUrl(options.baseUrl)\n const f = options.fetch ?? globalThis.fetch\n if (!f) throw new Error('No `fetch` implementation available.')\n this.fetchImpl = f.bind(globalThis)\n }\n\n /**\n * The resolved Client API base (always origin-qualified: the absolute/proxy\n * value, or the hosted default when `baseUrl` was omitted). Callers that need\n * a sibling endpoint — e.g. the realtime auth URL — must build it from this,\n * not the raw option, so a zero-config widget doesn't fall back to the page\n * origin.\n */\n get base(): string {\n return this.baseUrl\n }\n\n /**\n * Load the current session (marks it `started` on first call).\n *\n * @returns\n */\n getSession(): Promise<ClientSession> {\n return this.request('GET', '/session')\n }\n\n /**\n * Submit the document front image (triggers OCR + portrait extraction).\n *\n * @param input\n * @returns\n */\n submitDocumentFront(input: DocumentFrontInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.image) form.append('image', input.image, 'document-front.jpg')\n if (input.country) form.append('country', input.country)\n if (input.documentType) form.append('document_type', input.documentType)\n appendSignals(form, input.signals)\n return this.request('POST', '/document/front', form)\n }\n\n /**\n * Submit the document back image.\n *\n * @param input\n * @returns\n */\n submitDocumentBack(input: DocumentBackInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.image) form.append('image', input.image, 'document-back.jpg')\n if (input.country) form.append('country', input.country)\n if (input.documentType) form.append('document_type', input.documentType)\n return this.request('POST', '/document/back', form)\n }\n\n /**\n * Submit the selfie for the liveness check.\n *\n * @param input\n * @returns\n */\n submitLiveness(input: LivenessInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.selfie) form.append('selfie', input.selfie, 'selfie.jpg')\n if (input.video) form.append('video', input.video, 'liveness.webm')\n if (input.mode) form.append('mode', input.mode)\n if (input.challenges) form.append('challenges', JSON.stringify(input.challenges))\n appendSignals(form, input.signals)\n return this.request('POST', '/liveness', form)\n }\n\n /**\n * Submit address verification (opt-in stage). Sends the claimed address plus\n * whichever evidence was gathered (proof-of-address image, device coordinates).\n *\n * @param input\n * @returns\n */\n submitAddress(input: AddressInput): Promise<ClientSession> {\n const form = new FormData()\n const fields: Record<string, string | null | undefined> = {\n line1: input.line1,\n line2: input.line2,\n city: input.city,\n region: input.region,\n postal_code: input.postalCode,\n country: input.country,\n }\n for (const [key, value] of Object.entries(fields)) {\n if (value) form.append(key, value)\n }\n if (input.poa) form.append('poa', input.poa, 'proof-of-address.jpg')\n if (input.latitude != null) form.append('latitude', String(input.latitude))\n if (input.longitude != null) form.append('longitude', String(input.longitude))\n appendSignals(form, input.signals)\n return this.request('POST', '/address', form)\n }\n\n /**\n * Finalise the session (runs face-match + the decision engine).\n *\n * @param input\n * @returns\n */\n complete(input: CompleteInput = {}): Promise<ClientSession> {\n return this.request('POST', '/complete', input.signals ?? {})\n }\n\n private async request(method: string, path: string, body?: FormData | object): Promise<ClientSession> {\n const headers: Record<string, string> = { 'X-Client-Token': this.token }\n let payload: BodyInit | undefined\n\n if (body instanceof FormData) {\n payload = body\n } else if (body) {\n headers['Content-Type'] = 'application/json'\n payload = JSON.stringify(body)\n }\n\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, { method, headers, body: payload })\n const text = await res.text()\n const json = text ? (JSON.parse(text) as { message?: string; data?: ClientSession; error?: ApiErrorKey }) : {}\n\n if (!res.ok) {\n throw new WidgetApiError(json.message ?? `Request failed (${res.status})`, res.status, json.error)\n }\n return json.data as ClientSession\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n ApiErrorKey,\n CaptureModel,\n DocumentType,\n LivenessChallenge,\n LivenessMode,\n ProjectBranding,\n VerificationStatus,\n WorkflowConfig,\n} from '@arkyc/types'\n\n/** The session view exposed by the Client/Widget API (`GET /v1/client/session`). */\nexport interface ClientSession {\n id: string\n status: VerificationStatus\n expires_at: string\n /** The capture flow this session runs (Phase 17). */\n capture_model?: CaptureModel\n /** The active-liveness challenge sequence to prompt the user through. */\n liveness_challenges?: LivenessChallenge[]\n /** Cross-device handoff config resolved from the project setting. */\n handoff?: ClientHandoff\n /** How to watch this session live (push transport params or `polling`). */\n realtime?: ClientRealtime\n /** Project branding (colours/logo/name) so the widget themes itself at runtime. */\n branding?: ProjectBranding | null\n /** The applied workflow (ordered, toggleable stages + skip_ocr), or null for the default flow. */\n workflow?: WorkflowConfig | null\n}\n\n/**\n * How the widget watches this session live: the API's public realtime config\n * (`transport` plus that transport's settings) and the session's own channel.\n * Transports the widget does not stream over mean polling the session instead.\n */\nexport interface ClientRealtime {\n transport: 'pusher' | 'firebase' | 'firebase-database' | 'polling' | 'memory'\n /** The private channel this session's events publish to. */\n channel: string\n pusher?: Record<string, unknown>\n 'firebase-database'?: Record<string, unknown>\n [param: string]: unknown\n}\n\n/** Whether (and where) the widget may offer cross-device handoff. */\nexport interface ClientHandoff {\n enabled: boolean\n /** Whether a desktop user may continue on the desktop device instead of handing off. */\n allow_desktop: boolean\n /** First-party hosted page the QR points to (the phone resumes the session there). */\n url: string\n}\n\n/**\n * Mock-driver signal hints. Real provider drivers ignore these; the `mock`\n * drivers use them to make the demo/playground flow deterministic.\n */\nexport interface ProviderSignalHints {\n quality_score?: number\n ocr_confidence?: number\n expired?: boolean\n liveness_score?: number\n liveness_passed?: boolean\n multiple_faces?: boolean\n face_similarity?: number\n face_match_passed?: boolean\n address_score?: number\n address_passed?: boolean\n}\n\n/** Input for the address-verification submission (opt-in stage). */\nexport interface AddressInput {\n /** The claimed address the user entered. */\n line1?: string | null\n line2?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n /** ISO 3166-1 alpha-2 country code. */\n country?: string | null\n /** Proof-of-address document image. */\n poa?: Blob | null\n /** Captured device coordinates. */\n latitude?: number | null\n longitude?: number | null\n signals?: ProviderSignalHints\n}\n\n/** Input for the document-front submission. */\nexport interface DocumentFrontInput {\n image?: Blob | null\n country?: string | null\n documentType?: DocumentType | null\n signals?: ProviderSignalHints\n}\n\n/** Input for the document-back submission. */\nexport interface DocumentBackInput {\n image?: Blob | null\n country?: string | null\n documentType?: DocumentType | null\n}\n\n/** Input for the liveness submission (passive selfie or active challenge video). */\nexport interface LivenessInput {\n selfie?: Blob | null\n /** Recorded challenge video (active mode). */\n video?: Blob | null\n /** Passive (selfie) or active (challenge video). Defaults to passive. */\n mode?: LivenessMode\n /** The challenge sequence the user performed, in order (active mode). */\n challenges?: LivenessChallenge[]\n signals?: ProviderSignalHints\n}\n\n/** Input for completing the session (runs face-match + decision). */\nexport interface CompleteInput {\n signals?: ProviderSignalHints\n}\n\n/**\n * Raised when the Client API returns a non-2xx response.\n */\nexport class WidgetApiError extends Error {\n readonly status: number\n /**\n * Stable error key (e.g. `session_expired`) for errors Arkyc\n * deliberately raises. Undefined for unexpected errors.\n */\n readonly error?: ApiErrorKey\n\n constructor(message: string, status: number, error?: ApiErrorKey) {\n super(message)\n this.name = 'WidgetApiError'\n this.status = status\n this.error = error\n }\n}\n\n/** Where {@link ArkycClient} keeps this browser's device token between page loads. */\nexport type ClientTokenStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>\n\n/** Options for {@link ArkycClient}. */\nexport interface ArkycClientOptions {\n /**\n * The session's single-use launch token (from session creation) or a handoff\n * token (from a cross-device QR code). The client exchanges it for this\n * browser's device token on its first request.\n */\n token: string\n /**\n * The Client API **base** the widget appends each endpoint to — `{baseUrl}/session`,\n * `{baseUrl}/document/front`, etc. (no version prefix is added; `baseUrl` owns it).\n *\n * **Optional** — defaults to the hosted Arkyc API ({@link DEFAULT_CLIENT_BASE_URL}),\n * so the hosted product needs no config. Override only to self-host or proxy: a\n * relative path resolves against the current origin (same-origin, no CORS); an\n * absolute URL is used as-is (the API must allow your origin via CORS).\n */\n baseUrl?: string\n /** Injectable for testing; defaults to the global `fetch`. */\n fetch?: typeof fetch\n /**\n * Keeps the device token so a reload or second tab can continue the session.\n * Defaults to `localStorage`; pass `null` to keep it in memory only.\n */\n storage?: ClientTokenStorage | null\n}\n\n/** The device token a claim returned. */\ninterface StoredDeviceToken {\n token: string\n expires_at: string\n}\n\n/**\n * `localStorage` when the page may use it, otherwise null.\n *\n * @returns\n */\nfunction defaultStorage(): ClientTokenStorage | null {\n try {\n return globalThis.localStorage ?? null\n } catch {\n return null\n }\n}\n\n/**\n * The default Client API base, used when no `baseUrl` is passed — so the hosted\n * product is zero-config: the browser calls it cross-origin with the client\n * token (the API allows it via CORS).\n *\n * Baked at **build time** from the `ARKYC_API_URL` env (via the tsdown `define`),\n * falling back to the hosted Arkyc API. Self-hosters set `ARKYC_API_URL` when\n * building the widget; integrators on the published package override per call\n * with `baseUrl`. Referenced through `typeof` so running the source directly\n * (tests) without the define is safe.\n */\ndeclare const __ARKYC_API_BASE__: string | undefined\nexport const DEFAULT_CLIENT_BASE_URL =\n (typeof __ARKYC_API_BASE__ === 'string' ? __ARKYC_API_BASE__ : '') || 'https://api.arkyc.toneflix.net/api/v1/client'\n\n/**\n * Resolve the request base: an explicit `baseUrl` overrides the build-time\n * {@link DEFAULT_CLIENT_BASE_URL}. The chosen value is used as-is when absolute\n * (cross-origin, CORS); a relative path resolves against the current origin\n * (same-origin proxy, no CORS).\n *\n * @param baseUrl\n * @returns\n */\nexport function resolveClientBaseUrl(baseUrl?: string): string {\n const raw = (baseUrl ?? '').trim().replace(/\\/$/, '')\n if (!raw) return DEFAULT_CLIENT_BASE_URL\n if (/^https?:\\/\\//i.test(raw)) return raw\n const origin = (globalThis.location?.origin ?? '').replace(/\\/$/, '')\n return `${origin}${raw.startsWith('/') ? '' : '/'}${raw}`\n}\n\ntype EnvelopeNumber = number | boolean\n\nfunction appendSignals(form: FormData, signals?: ProviderSignalHints): void {\n if (!signals) return\n for (const [key, value] of Object.entries(signals)) {\n if (value == null) continue\n form.append(key, String(value as EnvelopeNumber))\n }\n}\n\n/**\n * A thin, framework-agnostic client for the Arkyc Client/Widget API. Drives a\n * single verification session with a short-lived client token. All responses\n * are unwrapped from the standard `{ status, message, data }` envelope.\n */\nexport class ArkycClient {\n private readonly token: string\n private readonly baseUrl: string\n private readonly fetchImpl: typeof fetch\n private readonly storage: ClientTokenStorage | null\n private deviceToken?: Promise<string>\n\n constructor(options: ArkycClientOptions) {\n if (!options.token) throw new Error('ArkycClient requires a client `token`.')\n this.token = options.token\n this.baseUrl = resolveClientBaseUrl(options.baseUrl)\n const f = options.fetch ?? globalThis.fetch\n if (!f) throw new Error('No `fetch` implementation available.')\n this.fetchImpl = f.bind(globalThis)\n this.storage = options.storage === undefined ? defaultStorage() : options.storage\n }\n\n /**\n * This browser's device token, claiming the launch or handoff token the first\n * time. Other Client API calls (and realtime channel auth) send this token.\n *\n * @returns\n */\n authToken(): Promise<string> {\n this.deviceToken ??= this.resolveDeviceToken().catch((error) => {\n this.deviceToken = undefined\n throw error\n })\n\n return this.deviceToken\n }\n\n /**\n * Mint the single-use token a cross-device QR code carries.\n *\n * @returns\n */\n async createHandoffToken(): Promise<string> {\n const data = await this.send<StoredDeviceToken>('POST', '/handoff', await this.authToken(), {})\n\n return data.token\n }\n\n /**\n * The resolved Client API base (always origin-qualified: the absolute/proxy\n * value, or the hosted default when `baseUrl` was omitted). Callers that need\n * a sibling endpoint — e.g. the realtime auth URL — must build it from this,\n * not the raw option, so a zero-config widget doesn't fall back to the page\n * origin.\n */\n get base(): string {\n return this.baseUrl\n }\n\n /**\n * Load the current session (marks it `started` on first call).\n *\n * @returns\n */\n getSession(): Promise<ClientSession> {\n return this.request('GET', '/session')\n }\n\n /**\n * Submit the document front image (triggers OCR + portrait extraction).\n *\n * @param input\n * @returns\n */\n submitDocumentFront(input: DocumentFrontInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.image) form.append('image', input.image, 'document-front.jpg')\n if (input.country) form.append('country', input.country)\n if (input.documentType) form.append('document_type', input.documentType)\n appendSignals(form, input.signals)\n return this.request('POST', '/document/front', form)\n }\n\n /**\n * Submit the document back image.\n *\n * @param input\n * @returns\n */\n submitDocumentBack(input: DocumentBackInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.image) form.append('image', input.image, 'document-back.jpg')\n if (input.country) form.append('country', input.country)\n if (input.documentType) form.append('document_type', input.documentType)\n return this.request('POST', '/document/back', form)\n }\n\n /**\n * Submit the selfie for the liveness check.\n *\n * @param input\n * @returns\n */\n submitLiveness(input: LivenessInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.selfie) form.append('selfie', input.selfie, 'selfie.jpg')\n if (input.video) form.append('video', input.video, 'liveness.webm')\n if (input.mode) form.append('mode', input.mode)\n if (input.challenges) form.append('challenges', JSON.stringify(input.challenges))\n appendSignals(form, input.signals)\n return this.request('POST', '/liveness', form)\n }\n\n /**\n * Submit address verification (opt-in stage). Sends the claimed address plus\n * whichever evidence was gathered (proof-of-address image, device coordinates).\n *\n * @param input\n * @returns\n */\n submitAddress(input: AddressInput): Promise<ClientSession> {\n const form = new FormData()\n const fields: Record<string, string | null | undefined> = {\n line1: input.line1,\n line2: input.line2,\n city: input.city,\n region: input.region,\n postal_code: input.postalCode,\n country: input.country,\n }\n for (const [key, value] of Object.entries(fields)) {\n if (value) form.append(key, value)\n }\n if (input.poa) form.append('poa', input.poa, 'proof-of-address.jpg')\n if (input.latitude != null) form.append('latitude', String(input.latitude))\n if (input.longitude != null) form.append('longitude', String(input.longitude))\n appendSignals(form, input.signals)\n return this.request('POST', '/address', form)\n }\n\n /**\n * Finalise the session (runs face-match + the decision engine).\n *\n * @param input\n * @returns\n */\n complete(input: CompleteInput = {}): Promise<ClientSession> {\n return this.request('POST', '/complete', input.signals ?? {})\n }\n\n private async request(method: string, path: string, body?: FormData | object): Promise<ClientSession> {\n return await this.send<ClientSession>(method, path, await this.authToken(), body)\n }\n\n /** The storage key for the device token claimed from this client's token. */\n private get storageKey(): string {\n return `arkyc:device:${this.token}`\n }\n\n private async resolveDeviceToken(): Promise<string> {\n const stored = this.readStoredToken()\n if (stored) return stored\n\n const claimed = await this.send<StoredDeviceToken>('POST', '/claim', this.token, {})\n this.writeStoredToken(claimed)\n\n return claimed.token\n }\n\n private readStoredToken(): string | null {\n try {\n const raw = this.storage?.getItem(this.storageKey)\n if (!raw) return null\n\n const stored = JSON.parse(raw) as StoredDeviceToken\n if (new Date(stored.expires_at).getTime() > Date.now()) return stored.token\n\n this.storage?.removeItem(this.storageKey)\n } catch {\n // Unreadable storage falls back to claiming.\n }\n\n return null\n }\n\n private writeStoredToken(token: StoredDeviceToken): void {\n try {\n this.storage?.setItem(this.storageKey, JSON.stringify(token))\n } catch {\n // Storage may be full or blocked; the token still works for this page.\n }\n }\n\n private async send<T>(method: string, path: string, token: string, body?: FormData | object): Promise<T> {\n const headers: Record<string, string> = { 'X-Client-Token': token }\n let payload: BodyInit | undefined\n\n if (body instanceof FormData) {\n payload = body\n } else if (body) {\n headers['Content-Type'] = 'application/json'\n payload = JSON.stringify(body)\n }\n\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, { method, headers, body: payload })\n const text = await res.text()\n const json = text ? (JSON.parse(text) as { message?: string; data?: unknown; error?: ApiErrorKey }) : {}\n\n if (!res.ok) {\n throw new WidgetApiError(json.message ?? `Request failed (${res.status})`, res.status, json.error)\n }\n return json.data as T\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { WidgetControllerConfig, WidgetEventListener } from "./types.mjs";
1
+ import { WidgetControllerConfig, WidgetEventListener, WidgetEventName } from "./types.mjs";
2
2
 
3
3
  //#region src/controller.d.ts
4
4
  declare class WidgetController {
@@ -24,7 +24,7 @@ declare class WidgetController {
24
24
  private rtResolved;
25
25
  /** The last status observed, so a transition only emits once per change. */
26
26
  private lastStatus;
27
- /** Per-name event listeners registered via {@link on}. */
27
+ /** Per-name event listeners registered via {@link on} (payload-erased for storage). */
28
28
  private listeners;
29
29
  /** Cancels the active session watch (e.g. continue-on-this-device). */
30
30
  private stopWatch;
@@ -62,7 +62,7 @@ declare class WidgetController {
62
62
  * Subscribe to a named widget event; returns an unsubscribe function. Having a
63
63
  * listener (here or via `onEvent`) is what activates the event stream.
64
64
  */
65
- on(event: string, listener: WidgetEventListener): () => void;
65
+ on<K extends WidgetEventName>(event: K, listener: WidgetEventListener<K>): () => void;
66
66
  /** Whether anyone is listening for `name` (the firehose or a named listener). */
67
67
  private hasListener;
68
68
  /**
@@ -77,8 +77,8 @@ declare class WidgetController {
77
77
  private observe;
78
78
  /**
79
79
  * Lazily connect the push transport for this session (once). Returns null for
80
- * `polling`/`off`/`memory` or when the transport SDK can't load — the caller
81
- * then polls instead.
80
+ * transports the widget does not stream over or when the transport SDK can't
81
+ * load, and the caller then polls instead.
82
82
  */
83
83
  private resolveRealtime;
84
84
  private handlers;
@@ -92,9 +92,12 @@ declare class WidgetController {
92
92
  private handoffTarget;
93
93
  /** Render the QR for this session and wait for the other device to finish. */
94
94
  private startHandoff;
95
- /** Build the hosted-page URL the QR encodes (carries the session token).
95
+ /**
96
+ * Build the hosted-page URL the QR encodes. It carries a single-use handoff
97
+ * token, never this browser's own token.
96
98
  *
97
99
  * @param target
100
+ * @param handoffToken
98
101
  * @returns
99
102
  */
100
103
  private buildHandoffUrl;
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.mts","names":[],"sources":["../src/controller.ts"],"mappings":";;;cAoEa,gBAAA;EAAA,iBAmDkB,MAAA;EAAA,iBAlDZ,MAAA;EAAA,iBACA,IAAA;EAAA,iBACA,GAAA;EAAA,iBACA,GAAA;EAAA,iBACA,YAAA;EAAA,iBACA,SAAA;EAAA,iBACA,WAAA;EAAA,iBACA,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,eAAA;EARA;EAAA,QAWT,aAAA;EAAA,QACA,YAAA;EAAA,QACA,aAAA;EATS;EAAA,QAYT,SAAA;EAAA,QACA,cAAA;EAAA,QACA,QAAA;EAAA,QACA,UAAA;EARA;EAAA,QAUA,UAAA;EARA;EAAA,QAUA,SAAA;EANA;EAAA,QAQA,SAAA;EAAA,QAEA,IAAA;EAAA,QACA,YAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,QACA,YAAA;EAHA;EAAA,QAKA,YAAA;EAAA,QACA,kBAAA;EAHA;EAAA,QAKA,WAAA;EAFA;EAAA,QAIA,aAAA;EAAA;EAAA,QAEA,QAAA;EAAA,QACA,MAAA;EAEA;EAAA,QAAA,cAAA;EAGA;EAAA,QADA,gBAAA;EAAA,QACA,YAAA;EAAA,QACA,OAAA;cAEqB,MAAA,EAAQ,sBAAA;EAkCjC;EAAA,IAAA,OAAA,IAAW,WAAA;EAKf;EAAA,KAAA;EAWA;EALA,OAAA;EAaG;EARH,KAAA;EAQkB;;;;EAAlB,EAAA,CAAG,KAAA,UAAe,QAAA,EAAU,mBAAA;EAiEd;EAAA,QAxDN,WAAA;EAuHM;;;;;EAAA,QA3GN,QAAA;EAkQA;EAAA,QA1PA,IAAA;EAmSM;EAAA,QA/QN,OAAA;EAyUA;;;;;EAAA,QAzTM,eAAA;EAAA,QAuBN,QAAA;EAyYA;;;;;EAAA,QAjWM,SAAA;EAuaN;EAAA,QA5YA,aAAA;EA+ZA;EAAA,QA1ZM,YAAA;EA0ZF;;;;;EAAA,QA5YJ,eAAA;;;;;;;UAWM,WAAA;;;;;;;;;;;;UA8CN,SAAA;;;;;;UAgDA,eAAA;EAAA,QAWM,OAAA;;;;;;;UA8BA,KAAA;;;;;UAwCA,IAAA;EAAA,QAkBN,UAAA;;;;;;UAWA,YAAA;EAAA,QAOA,MAAA;;;;;;;UAwBM,SAAA;;UAYA,aAAA;;;;;;UAWA,aAAA;;;;;;;;UAyBN,SAAA;EAAA,QAaA,IAAA;EAAA,QAQA,KAAA;;UAKM,GAAA;EAAA,QAQN,IAAA;;UAwBA,SAAA;EAAA,QAMA,YAAA;EAAA,QAmBA,WAAA;;UAaA,gBAAA;EAAA,QAMA,IAAA;AAAA"}
1
+ {"version":3,"file":"controller.d.mts","names":[],"sources":["../src/controller.ts"],"mappings":";;;cAgFa,gBAAA;EAAA,iBAmDkB,MAAA;EAAA,iBAlDZ,MAAA;EAAA,iBACA,IAAA;EAAA,iBACA,GAAA;EAAA,iBACA,GAAA;EAAA,iBACA,YAAA;EAAA,iBACA,SAAA;EAAA,iBACA,WAAA;EAAA,iBACA,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,eAAA;EAmGoD;EAAA,QAhG7D,aAAA;EAAA,QACA,YAAA;EAAA,QACA,aAAA;EAZS;EAAA,QAeT,SAAA;EAAA,QACA,cAAA;EAAA,QACA,QAAA;EAAA,QACA,UAAA;EAbS;EAAA,QAeT,UAAA;EAbS;EAAA,QAeT,SAAA;EAXA;EAAA,QAaA,SAAA;EAAA,QAEA,IAAA;EAAA,QACA,YAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,QACA,YAAA;EARA;EAAA,QAUA,YAAA;EAAA,QACA,kBAAA;EANA;EAAA,QAQA,WAAA;EANA;EAAA,QAQA,aAAA;EALA;EAAA,QAOA,QAAA;EAAA,QACA,MAAA;EAHA;EAAA,QAKA,cAAA;EAFA;EAAA,QAIA,gBAAA;EAAA,QACA,YAAA;EAAA,QACA,OAAA;cAEqB,MAAA,EAAQ,sBAAA;;MAkCjC,OAAA,IAAW,WAAA;EAlCc;EAuC7B,KAAA;EALe;EAWf,OAAA;EAAA;EAKA,KAAA;EAQA;;;;EAAA,EAAA,WAAa,eAAA,EAAiB,KAAA,EAAO,CAAA,EAAG,QAAA,EAAU,mBAAA,CAAoB,CAAA;EAApB;EAAA,QAS1C,WAAA;EATgC;;;;;EAAA,QAqBhC,QAAA;EAmEA;EAAA,QA3DA,IAAA;EA8HA;EAAA,QA1GA,OAAA;EAiIA;;;;;EAAA,QAjHM,eAAA;EAAA,QAuBN,QAAA;EAsSA;;;;;EAAA,QA9PM,SAAA;EAqWN;EAAA,QA1UA,aAAA;EA+VA;EAAA,QA1VM,YAAA;EAuWN;;;;;;;AAoEI;EApEJ,QArVA,eAAA;;;;;;;UAWM,WAAA;;;;;;;;;;;;UA8CN,SAAA;;;;;;UAgDA,eAAA;EAAA,QAWM,OAAA;;;;;;;UA8BA,KAAA;;;;;UAwCA,IAAA;EAAA,QAkBN,UAAA;;;;;;UAWA,YAAA;EAAA,QAoBA,MAAA;;;;;;;UAwBM,SAAA;;UAYA,aAAA;;;;;;UAWA,aAAA;;;;;;;;UAyBN,SAAA;EAAA,QAaA,IAAA;EAAA,QAQA,KAAA;;UAKM,GAAA;EAAA,QAQN,IAAA;;UAwBA,SAAA;EAAA,QAMA,YAAA;EAAA,QAmBA,WAAA;;UAaA,gBAAA;EAAA,QAMA,IAAA;AAAA"}
@@ -39,7 +39,9 @@ const STEP_INSTRUCTIONS = {
39
39
  * handed-off phone cannot reach — it reaches the API through the absolute
40
40
  * `baseUrl` passed in the handoff link. Rewriting the origin (keeping the path)
41
41
  * makes the logo load on whatever device runs the widget. Only applied when
42
- * `baseUrl` is absolute; the integrator's own branding is never re-based.
42
+ * `baseUrl` is absolute; the integrator's own branding is never re-based, and
43
+ * neither is a presigned object-storage URL (S3/R2/GCS), whose signature is only
44
+ * valid on its own host.
43
45
  */
44
46
  function rebaseBrandingLogo(branding, baseUrl) {
45
47
  const logo = branding.logo_url;
@@ -48,6 +50,7 @@ function rebaseBrandingLogo(branding, baseUrl) {
48
50
  const base = new URL(baseUrl);
49
51
  const resolved = new URL(logo, base);
50
52
  if (resolved.origin === base.origin) return branding;
53
+ if (/[?&]x-(amz|goog)-signature=/i.test(resolved.search)) return branding;
51
54
  return {
52
55
  ...branding,
53
56
  logo_url: `${base.origin}${resolved.pathname}${resolved.search}${resolved.hash}`
@@ -83,7 +86,7 @@ var WidgetController = class {
83
86
  rtResolved = false;
84
87
  /** The last status observed, so a transition only emits once per change. */
85
88
  lastStatus = null;
86
- /** Per-name event listeners registered via {@link on}. */
89
+ /** Per-name event listeners registered via {@link on} (payload-erased for storage). */
87
90
  listeners = /* @__PURE__ */ new Map();
88
91
  /** Cancels the active session watch (e.g. continue-on-this-device). */
89
92
  stopWatch;
@@ -201,20 +204,20 @@ var WidgetController = class {
201
204
  }
202
205
  /**
203
206
  * Lazily connect the push transport for this session (once). Returns null for
204
- * `polling`/`off`/`memory` or when the transport SDK can't load — the caller
205
- * then polls instead.
207
+ * transports the widget does not stream over or when the transport SDK can't
208
+ * load, and the caller then polls instead.
206
209
  */
207
210
  async resolveRealtime() {
208
211
  if (this.rtResolved) return this.rtClient;
209
212
  this.rtResolved = true;
210
213
  const cfg = this.realtimeConfig;
211
- if (!cfg || cfg.transport === "polling" || cfg.transport === "off" || cfg.transport === "memory") return null;
214
+ if (!cfg || cfg.transport === "polling" || cfg.transport === "memory") return null;
212
215
  const factory = this.config.realtimeFactory ?? createWidgetRealtimeClient;
213
216
  const apiBase = this.client.base;
214
217
  try {
215
218
  this.rtClient = await factory(cfg, {
216
219
  authEndpoint: `${apiBase}/realtime/auth`,
217
- token: this.config.token
220
+ token: await this.client.authToken()
218
221
  });
219
222
  } catch {
220
223
  this.rtClient = null;
@@ -287,17 +290,21 @@ var WidgetController = class {
287
290
  const target = this.handoffTarget();
288
291
  if (!target) return;
289
292
  this.handoffActive = true;
290
- this.view.renderHandoff(renderQrSvg(this.buildHandoffUrl(target)), this.handoffConfig.allow_desktop);
293
+ const url = this.buildHandoffUrl(target, await this.client.createHandoffToken());
294
+ this.view.renderHandoff(renderQrSvg(url), this.handoffConfig.allow_desktop);
291
295
  await this.pollHandoff();
292
296
  }
293
- /** Build the hosted-page URL the QR encodes (carries the session token).
297
+ /**
298
+ * Build the hosted-page URL the QR encodes. It carries a single-use handoff
299
+ * token, never this browser's own token.
294
300
  *
295
301
  * @param target
302
+ * @param handoffToken
296
303
  * @returns
297
304
  */
298
- buildHandoffUrl(target) {
305
+ buildHandoffUrl(target, handoffToken) {
299
306
  const pageOrigin = this.win.location?.origin ?? globalThis.location?.origin ?? "";
300
- return composeHandoffUrl(target, this.config.token, this.client.base, pageOrigin);
307
+ return composeHandoffUrl(target, handoffToken, this.client.base, pageOrigin);
301
308
  }
302
309
  /**
303
310
  * Watch the session while the user verifies on the other
@@ -492,6 +499,10 @@ var WidgetController = class {
492
499
  this.step = "result";
493
500
  this.terminalOnLoad = true;
494
501
  this.render();
502
+ if (this.result.decision != null) {
503
+ this.dispatch("complete", this.result);
504
+ this.config.onComplete?.(this.result);
505
+ }
495
506
  }
496
507
  render() {
497
508
  this.view.render({
@@ -625,7 +636,7 @@ var WidgetController = class {
625
636
  this.settled = true;
626
637
  this.teardownRealtime();
627
638
  this.post("arkyc:close", {});
628
- this.dispatch("close");
639
+ this.dispatch("close", void 0);
629
640
  this.config.onClose?.();
630
641
  this.destroy();
631
642
  this.config.onSettle?.();