@flonkid/kyc 1.8.1 → 1.9.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/README.md CHANGED
@@ -124,9 +124,16 @@ widget.destroy();
124
124
  ```typescript
125
125
  import { FlonkKYCServer } from '@flonkid/kyc/server';
126
126
 
127
- const flonk = new FlonkKYCServer({ secretKey: 'sk_live_...' });
127
+ const flonk = new FlonkKYCServer({
128
+ secretKey: 'sk_live_...',
129
+ // Transient failures (429 / 5xx / network) are retried with jittered
130
+ // exponential backoff. GETs always retry; writes retry only when idempotent.
131
+ maxRetries: 2, // default; set 0 to disable
132
+ });
128
133
 
129
- // Create session
134
+ // Create session — idempotent: a retry (same key) returns the original session,
135
+ // never a duplicate. A key is auto-generated per call; pass `idempotencyKey` to
136
+ // make a specific create idempotent across process restarts.
130
137
  const session = await flonk.createSession({
131
138
  clientMetadata: { email: 'user@example.com', userId: 'user_123' },
132
139
  expiryMinutes: 30,
@@ -162,6 +169,105 @@ switch (event.type) {
162
169
  }
163
170
  ```
164
171
 
172
+ ### Signature formats & replay protection
173
+
174
+ Flonk sends **two** signature headers, both HMAC-SHA256 with your webhook
175
+ secret; `constructEvent` verifies whichever you pass. Both prove authenticity +
176
+ integrity — the only difference is replay protection:
177
+
178
+ | Header | Format | Replay-protected? |
179
+ |--------|--------|-------------------|
180
+ | `X-Signature` | `t=<unix>, v1=<hex>` | ✅ Timestamp is signed; requests outside the skew window (default 300s) are rejected. |
181
+ | `X-Signature-256` | `sha256=<hex>` | ❌ No timestamp — a captured request stays valid (close the gap with event-id dedup, below). |
182
+
183
+ Both are fully valid signatures; prefer `X-Signature` when you want replay
184
+ protection at the signature layer. Verification is constant-time.
185
+
186
+ Since delivery is **at-least-once** (Flonk retries on non-200), also dedupe by
187
+ `event.id` so a retry is processed once. Do this in your own store — a unique
188
+ DB index, or Redis `SET id 1 NX EX <ttl>` — exactly as you would for Stripe:
189
+
190
+ ```typescript
191
+ app.post('/webhooks/flonk', async (req, res) => {
192
+ const event = flonk.webhooks.constructEvent(req.rawBody, req.headers['x-signature'], secret);
193
+
194
+ // Idempotency: SET NX returns null if the key already existed → it's a retry.
195
+ const fresh = await redis.set(`whk:${event.id}`, '1', 'PX', 6 * 60_000, 'NX');
196
+ if (fresh === null) return res.status(200).end(); // already handled
197
+
198
+ await process(event);
199
+ res.status(200).end();
200
+ });
201
+ ```
202
+
203
+ ## Content Security Policy & CORS
204
+
205
+ The widget runs in an iframe on `widget.flonk.id` and loads a small branded
206
+ loader script from the API. If your site sends a `Content-Security-Policy`
207
+ header, allow our origins:
208
+
209
+ ```
210
+ Content-Security-Policy:
211
+ frame-src https://widget.flonk.id;
212
+ script-src https://widget.flonk.id https://api.flonk.id;
213
+ connect-src https://api.flonk.id https://widget.flonk.id;
214
+ img-src https://widget.flonk.id data:;
215
+ ```
216
+
217
+ - `frame-src` — the widget iframe (`widget.flonk.id`).
218
+ - `script-src` — `widget.js` (`widget.flonk.id`) and the server-hosted loader
219
+ (`api.flonk.id/v1/public/loader.js`). If the loader script is blocked, the SDK
220
+ **falls back to its bundled loader** — it still works, just without
221
+ dashboard-driven branding/fixes.
222
+ - `connect-src` — session/token/design-token fetches.
223
+
224
+ You do **not** need any CORS or `Cross-Origin-Resource-Policy` configuration on
225
+ your side. Our public assets (`loader.js`, `loader-config`, `design-tokens`)
226
+ already send `Cross-Origin-Resource-Policy: cross-origin`, and the API handles
227
+ CORS for the SDK's `fetch`/`POST` calls.
228
+
229
+ ### Debugging blocked resources
230
+
231
+ Every degradation is observable — no silent failures. Pass `onDiagnostic`, or
232
+ flip the global debug flag to mirror events to the console:
233
+
234
+ ```typescript
235
+ const kyc = new FlonkKYC({
236
+ onDiagnostic: (e) => console.log(`[flonk:${e.code}] ${e.message}`, e.detail),
237
+ });
238
+
239
+ // Or, without code — anywhere before the widget opens:
240
+ window.__FLONK_DEBUG__ = true; // SDK + widget.js both honor this
241
+ // <script ... data-debug> also enables it for widget.js
242
+ ```
243
+
244
+ Codes you may see when something is blocked or degraded:
245
+
246
+ | Code | Meaning |
247
+ |------|---------|
248
+ | `LOADER_SCRIPT_BLOCKED` | Server loader script failed to load (CSP `script-src`, CORP, offline). Bundled loader used. |
249
+ | `LOADER_FALLBACK_BUNDLED` | Server loader wasn't ready at show time; bundled loader used. |
250
+ | `PREWARM_SKIPPED` | Prewarm disabled (`prewarm: 'none'`) or no DOM (SSR). |
251
+ | `READY_TIMEOUT_REVEAL` | No `READY` from the iframe within 4s; revealed on safety timeout (check `frame-src`). |
252
+ | `IFRAME_FRESH` | A fresh widget iframe was built. |
253
+
254
+ Console output is silent unless `onDiagnostic` is set or `__FLONK_DEBUG__` is
255
+ truthy — zero noise in production by default.
256
+
257
+ ## Versioning
258
+
259
+ Three versions, deliberately separate:
260
+
261
+ - **Package version** (npm) — changes every release.
262
+ - **SDK↔iframe wire protocol** — the widget iframe and SDK deploy independently,
263
+ so the wire is additive-only; a `PROTOCOL_VERSION_MISMATCH` diagnostic surfaces
264
+ a stale-cached peer. Details: [`CONTRACT.md`](./CONTRACT.md).
265
+ - **REST API version** — date-pinned, sent as the `Flonk-Version` header on every
266
+ server request. The API is additive-only within a version; a breaking change
267
+ would mint a new date and serve the old shape to SDKs pinned to the old one.
268
+ Override with `new FlonkKYCServer({ apiVersion: '2026-06-01' })`; the server
269
+ echoes the resolved version back in the `Flonk-Version` response header.
270
+
165
271
  ## Links
166
272
 
167
273
  - [Full Documentation](https://docs.flonk.id)
package/dist/index.cjs CHANGED
@@ -4,14 +4,31 @@ var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
 
6
6
  // src/shared/constants.ts
7
- var SDK_VERSION = "1.8.1";
7
+ var SDK_VERSION = "1.9.0";
8
8
  var DEFAULT_WIDGET_URL = "https://widget.flonk.id";
9
9
  var DEFAULT_API_BASE = "https://api.flonk.id/v1";
10
+ var API_VERSION = "2026-06-01";
11
+ var PROTOCOL_VERSION = 1;
10
12
  var WIDGET_EVENTS = {
11
13
  READY: "KYC_WIDGET_READY",
12
14
  COMPLETE: "KYC_COMPLETE",
13
15
  CANCEL: "KYC_CANCEL",
14
- ERROR: "KYC_ERROR"
16
+ ERROR: "KYC_ERROR",
17
+ CONFIG: "KYC_WIDGET_CONFIG"
18
+ };
19
+ var WIDGET_PARAMS = {
20
+ PROTOCOL_VERSION: "pv",
21
+ SESSION_ID: "sessionId",
22
+ EMBED_TOKEN: "embedToken",
23
+ TOKEN: "token",
24
+ PUBLISHABLE_KEY: "publishableKey",
25
+ CLIENT_ID: "clientId",
26
+ CLIENT_METADATA: "clientMetadata",
27
+ DESIGN_TOKENS: "designTokens",
28
+ ALLOW_MANUAL_UPLOAD: "allowManualUpload",
29
+ LANG: "lang",
30
+ OVERLAY_COLOR: "overlayColor",
31
+ MODE: "mode"
15
32
  };
16
33
 
17
34
  // src/shared/errors.ts
@@ -199,24 +216,6 @@ async function fetchSessionFromServer(serverUrl, clientMetadata, requestHeaders)
199
216
  sessionCreateInflight.set(key, promise);
200
217
  return promise;
201
218
  }
202
- async function fetchPublicSession(apiBase, sessionId, embedToken) {
203
- const res = await fetchWithTimeout(`${apiBase}/public/session/${sessionId}`, {
204
- headers: {
205
- "Content-Type": "application/json",
206
- "Authorization": `Bearer ${embedToken}`
207
- }
208
- });
209
- if (!res.ok) {
210
- let message = `Failed to fetch session (${res.status})`;
211
- try {
212
- const b = await res.json();
213
- message = b.error || b.message || message;
214
- } catch {
215
- }
216
- throw new Error(message);
217
- }
218
- return res.json();
219
- }
220
219
  async function exchangeSessionForToken(apiBase, sessionId) {
221
220
  const res = await fetchWithTimeout(`${apiBase}/public/session/${sessionId}/token`, {
222
221
  method: "POST",
@@ -247,10 +246,14 @@ function createIframe(src) {
247
246
  top: "0",
248
247
  left: "0",
249
248
  zIndex: "9999",
250
- background: "transparent",
251
- backgroundColor: "transparent",
249
+ // Hidden until reveal so the loader overlay doesn't show the iframe's own
250
+ // loading state through its translucent backdrop (double loader). Reveal
251
+ // is gated on READY-OR-a-timeout (see index.ts), so a missing READY can't
252
+ // leave it permanently hidden — that timeout is what removed the deadlock.
252
253
  opacity: "0",
253
254
  visibility: "hidden",
255
+ background: "transparent",
256
+ backgroundColor: "transparent",
254
257
  borderRadius: d ? "0" : "",
255
258
  boxShadow: d ? "none" : "",
256
259
  colorScheme: "normal"
@@ -285,15 +288,15 @@ function adjustZIndex(loader, iframe) {
285
288
  function transitionLoaderToIframe(loader, iframe, onDone) {
286
289
  const d = isDesktop();
287
290
  const dur = d ? 300 : 500;
288
- setStyles(iframe, { opacity: "0", visibility: "hidden" });
289
291
  const card = loader.querySelector("div");
290
292
  if (card) {
291
293
  setStyles(card, {
294
+ transition: "transform 300ms ease-out, opacity 300ms ease-out",
292
295
  transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
293
296
  opacity: "0"
294
297
  });
295
298
  }
296
- loader.style.opacity = "0";
299
+ setStyles(loader, { transition: "opacity 300ms ease-out", opacity: "0" });
297
300
  setTimeout(() => {
298
301
  onDone();
299
302
  setStyles(iframe, {
@@ -304,6 +307,160 @@ function transitionLoaderToIframe(loader, iframe, onDone) {
304
307
  }, dur);
305
308
  }
306
309
 
310
+ // src/browser/diagnostics.ts
311
+ var handlers = /* @__PURE__ */ new Set();
312
+ function addDiagnosticHandler(handler) {
313
+ handlers.add(handler);
314
+ return () => {
315
+ handlers.delete(handler);
316
+ };
317
+ }
318
+ function debugEnabled() {
319
+ try {
320
+ return Boolean(globalThis.__FLONK_DEBUG__);
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
325
+ function emitDiagnostic(code, level, message, detail) {
326
+ const event = { code, level, message, detail };
327
+ if (debugEnabled()) {
328
+ try {
329
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
330
+ fn(`[flonk:${code}] ${message}`, detail ?? "");
331
+ } catch {
332
+ }
333
+ }
334
+ for (const handler of handlers) {
335
+ try {
336
+ handler(event);
337
+ } catch {
338
+ }
339
+ }
340
+ }
341
+
342
+ // src/browser/prewarm.ts
343
+ var noop = () => {
344
+ };
345
+ function originOf(url) {
346
+ try {
347
+ return new URL(url).origin;
348
+ } catch {
349
+ return url.replace(/\/+$/, "");
350
+ }
351
+ }
352
+ function addLinkHint(doc, rel, href, attrs) {
353
+ try {
354
+ const existing = doc.querySelector(`link[rel="${rel}"][href="${href}"]`);
355
+ if (existing) return;
356
+ const link = doc.createElement("link");
357
+ link.rel = rel;
358
+ link.href = href;
359
+ if (attrs) for (const k of Object.keys(attrs)) link.setAttribute(k, attrs[k]);
360
+ (doc.head || doc.documentElement).appendChild(link);
361
+ } catch {
362
+ }
363
+ }
364
+ function preconnect(widgetUrl, doc = document) {
365
+ const origin = originOf(widgetUrl);
366
+ addLinkHint(doc, "preconnect", origin, { crossorigin: "" });
367
+ addLinkHint(doc, "dns-prefetch", origin);
368
+ }
369
+ function defaultScheduleIdle(fn) {
370
+ if (typeof window === "undefined") {
371
+ fn();
372
+ return;
373
+ }
374
+ const w = window;
375
+ const run = () => {
376
+ if (typeof w.requestIdleCallback === "function") {
377
+ w.requestIdleCallback(fn, { timeout: 2e3 });
378
+ } else {
379
+ setTimeout(fn, 200);
380
+ }
381
+ };
382
+ if (document.readyState === "complete") run();
383
+ else window.addEventListener("load", run, { once: true });
384
+ }
385
+ function prewarm(options) {
386
+ const level = options.level ?? "connect";
387
+ const doc = options.doc ?? (typeof document !== "undefined" ? document : void 0);
388
+ if (level === "none" || !doc) {
389
+ emitDiagnostic("PREWARM_SKIPPED", "info", `Prewarm skipped (level=${level}${doc ? "" : ", no document"}).`);
390
+ return noop;
391
+ }
392
+ const scheduleIdle = options.scheduleIdle ?? defaultScheduleIdle;
393
+ const origin = originOf(options.widgetUrl);
394
+ preconnect(options.widgetUrl, doc);
395
+ const warmAssets = () => {
396
+ if (options.apiBase) {
397
+ const q = options.publishableKey ? `pk=${encodeURIComponent(options.publishableKey)}` : options.sessionId ? `sessionId=${encodeURIComponent(options.sessionId)}` : "";
398
+ addLinkHint(doc, "prefetch", `${options.apiBase}/v1/public/design-tokens${q ? `?${q}` : ""}`);
399
+ }
400
+ addLinkHint(doc, "prefetch", `${origin}/`, { as: "document" });
401
+ };
402
+ if (level === "intent") {
403
+ return attachIntent(doc, options.trigger ?? null, () => {
404
+ warmAssets();
405
+ mountHiddenIframe(doc, origin);
406
+ });
407
+ }
408
+ scheduleIdle(() => {
409
+ warmAssets();
410
+ if (level === "eager") mountHiddenIframe(doc, origin);
411
+ });
412
+ return noop;
413
+ }
414
+ function attachIntent(doc, trigger, warm) {
415
+ let fired = false;
416
+ const fire = () => {
417
+ if (fired) return;
418
+ fired = true;
419
+ cleanup();
420
+ warm();
421
+ };
422
+ const events = [
423
+ ["mouseenter", fire],
424
+ ["focusin", fire],
425
+ ["touchstart", fire]
426
+ ];
427
+ let io = null;
428
+ const cleanup = () => {
429
+ if (trigger) for (const [type, fn] of events) trigger.removeEventListener(type, fn);
430
+ if (io) {
431
+ io.disconnect();
432
+ io = null;
433
+ }
434
+ };
435
+ if (trigger) {
436
+ for (const [type, fn] of events) trigger.addEventListener(type, fn, { passive: true });
437
+ const w = doc.defaultView || (typeof window !== "undefined" ? window : void 0);
438
+ if (w && typeof w.IntersectionObserver === "function") {
439
+ const observer = new w.IntersectionObserver((entries) => {
440
+ if (entries.some((e) => e.isIntersecting)) fire();
441
+ });
442
+ observer.observe(trigger);
443
+ io = observer;
444
+ }
445
+ } else {
446
+ defaultScheduleIdle(fire);
447
+ }
448
+ return cleanup;
449
+ }
450
+ function mountHiddenIframe(doc, origin) {
451
+ try {
452
+ if (doc.querySelector("iframe[data-flonk-prewarm]")) return;
453
+ const iframe = doc.createElement("iframe");
454
+ iframe.src = `${origin}/?prewarm=1`;
455
+ iframe.setAttribute("data-flonk-prewarm", "1");
456
+ iframe.setAttribute("aria-hidden", "true");
457
+ iframe.tabIndex = -1;
458
+ iframe.style.cssText = "position:absolute;width:1px;height:1px;left:-9999px;top:-9999px;border:0;opacity:0;pointer-events:none";
459
+ (doc.body || doc.documentElement).appendChild(iframe);
460
+ } catch {
461
+ }
462
+ }
463
+
307
464
  // src/browser/loader.ts
308
465
  var LOADER_I18N = {
309
466
  en: { title: "Initializing...", subtitle: "Loading KYC widget", errorTitle: "Something went wrong", close: "Close" },
@@ -553,8 +710,81 @@ var Loader = class {
553
710
  this.cleanup = null;
554
711
  }
555
712
  };
713
+ function isServerLoaderReady() {
714
+ return typeof window !== "undefined" && !!window.FlonkWidgetLoader?.show;
715
+ }
716
+ var serverScriptRequested = false;
717
+ function loadServerLoaderScript(apiBase, pk, sessionId) {
718
+ if (typeof document === "undefined" || serverScriptRequested || isServerLoaderReady()) return;
719
+ serverScriptRequested = true;
720
+ try {
721
+ const q = pk ? `?publishableKey=${encodeURIComponent(pk)}` : sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
722
+ const s = document.createElement("script");
723
+ s.src = `${apiBase}/public/loader.js${q}`;
724
+ s.async = true;
725
+ s.onerror = () => {
726
+ serverScriptRequested = false;
727
+ emitDiagnostic(
728
+ "LOADER_SCRIPT_BLOCKED",
729
+ "warn",
730
+ `Failed to load the server loader (${s.src}). Likely a CSP script-src or CORP block \u2014 allow the API origin in script-src. Falling back to the bundled loader.`,
731
+ { src: s.src }
732
+ );
733
+ };
734
+ (document.head || document.documentElement).appendChild(s);
735
+ } catch {
736
+ }
737
+ }
738
+ var ServerLoader = class {
739
+ constructor() {
740
+ this.overlay = null;
741
+ }
742
+ show(primaryColor, lang) {
743
+ this.overlay = window.FlonkWidgetLoader.show({ primaryColor, lang });
744
+ return this.overlay;
745
+ }
746
+ get element() {
747
+ return this.overlay;
748
+ }
749
+ updateColor(primaryColor) {
750
+ this.overlay?.updateColor?.(primaryColor);
751
+ }
752
+ showError(message, lang) {
753
+ this.overlay?.showError?.(message, lang);
754
+ }
755
+ fadeOut() {
756
+ this.overlay?.fadeOut?.();
757
+ }
758
+ destroy() {
759
+ try {
760
+ this.overlay?.remove();
761
+ } catch {
762
+ }
763
+ this.overlay = null;
764
+ }
765
+ };
766
+ function makeLoader() {
767
+ if (isServerLoaderReady()) return new ServerLoader();
768
+ emitDiagnostic(
769
+ "LOADER_FALLBACK_BUNDLED",
770
+ "info",
771
+ "Server loader not ready at show time \u2014 using the bundled loader."
772
+ );
773
+ return new Loader();
774
+ }
556
775
 
557
776
  // src/browser/message-handler.ts
777
+ function checkProtocol(data) {
778
+ const remote = data.protocolVersion;
779
+ if (typeof remote === "number" && remote !== PROTOCOL_VERSION) {
780
+ emitDiagnostic(
781
+ "PROTOCOL_VERSION_MISMATCH",
782
+ "warn",
783
+ `SDK speaks protocol ${PROTOCOL_VERSION}, iframe speaks ${remote}. Additive-compatible, but check for a stale-cached widget if behavior is off.`,
784
+ { sdk: PROTOCOL_VERSION, iframe: remote }
785
+ );
786
+ }
787
+ }
558
788
  var MessageHandler = class {
559
789
  constructor(iframeSrc, iframe, callbacks) {
560
790
  this.iframeSrc = iframeSrc;
@@ -587,6 +817,7 @@ var MessageHandler = class {
587
817
  this.completionHandled = true;
588
818
  this.callbacks.onError?.(data.error || "Unknown error");
589
819
  } else if (type === WIDGET_EVENTS.READY) {
820
+ checkProtocol(data);
590
821
  this.callbacks.onReady?.();
591
822
  }
592
823
  };
@@ -738,14 +969,55 @@ async function showLoaderWithEarlyColor(tokensPromise, lang) {
738
969
  new Promise((resolve) => setTimeout(() => resolve(null), EARLY_COLOR_BUDGET_MS))
739
970
  ]);
740
971
  const primaryColor = primaryFrom(earlyTokens);
741
- const loader = new Loader();
972
+ const loader = makeLoader();
742
973
  loader.show(primaryColor, lang);
743
974
  return { loader, primaryColor };
744
975
  }
745
976
  var FlonkKYC = class {
746
977
  constructor(options = {}) {
978
+ this.disposeDiagnostics = null;
747
979
  this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
748
980
  this.apiBase = (options.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
981
+ if (options.onDiagnostic) this.disposeDiagnostics = addDiagnosticHandler(options.onDiagnostic);
982
+ if (typeof document !== "undefined") {
983
+ try {
984
+ preconnect(this.widgetUrl);
985
+ } catch {
986
+ }
987
+ try {
988
+ loadServerLoaderScript(this.apiBase);
989
+ } catch {
990
+ }
991
+ }
992
+ }
993
+ /** Unregister this instance's `onDiagnostic` handler. */
994
+ dispose() {
995
+ this.disposeDiagnostics?.();
996
+ this.disposeDiagnostics = null;
997
+ }
998
+ /**
999
+ * Prewarm the widget ahead of the user's click — preconnect + idle prefetch
1000
+ * of branding/assets, and (with `level:'eager'`) a hidden background iframe so
1001
+ * the full bundle is loaded before the click. Call on page mount / route
1002
+ * enter. Returns a cleanup (removes `intent` listeners). Never pre-creates a
1003
+ * session. SSR-safe.
1004
+ *
1005
+ * @example
1006
+ * FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'eager' });
1007
+ * // or, warm only when the user shows intent:
1008
+ * FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'intent', trigger: btn });
1009
+ */
1010
+ static prewarm(opts = {}) {
1011
+ if (typeof document === "undefined") return () => {
1012
+ };
1013
+ return prewarm({
1014
+ widgetUrl: (opts.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, ""),
1015
+ apiBase: (opts.apiBase || DEFAULT_API_BASE).replace(/\/$/, ""),
1016
+ publishableKey: opts.publishableKey,
1017
+ sessionId: opts.sessionId,
1018
+ level: opts.level,
1019
+ trigger: opts.trigger ?? null
1020
+ });
749
1021
  }
750
1022
  /**
751
1023
  * Warm the project's branding (colors) ahead of time so the widget paints the
@@ -896,20 +1168,7 @@ var FlonkKYC = class {
896
1168
  const finalTokens = designTokens ?? await fetchDesignTokens(this.apiBase, { sessionId });
897
1169
  const finalColor = primaryFrom(finalTokens);
898
1170
  if (finalColor !== primaryColor) loader.updateColor(finalColor);
899
- const sessionData = await fetchPublicSession(this.apiBase, sessionId, embedToken);
900
- const session = {
901
- id: sessionData.id,
902
- allowManualUpload: sessionData.allowManualUpload ?? config.allowManualUpload ?? true,
903
- clientMetadata: sessionData.clientMetadata || config.clientMetadata,
904
- qrCodeUrl: sessionData.qrCodeUrl,
905
- testMode: sessionData.testMode || false,
906
- poaEnabled: sessionData.poaEnabled || false,
907
- poaRequired: sessionData.poaRequired || false,
908
- mlAutoCaptureEnabled: sessionData.mlAutoCaptureEnabled || false,
909
- mlCropEnabled: sessionData.mlCropEnabled ?? true,
910
- mlVerifyEnabled: sessionData.mlVerifyEnabled || false
911
- };
912
- return this.buildWidget(embedToken, session, config, loader, finalTokens);
1171
+ return this.buildWidget(embedToken, sessionId, config, loader, finalTokens);
913
1172
  } catch (err) {
914
1173
  const msg = err.message || "Failed to create session";
915
1174
  loader.showError(msg, config.lang);
@@ -923,32 +1182,12 @@ var FlonkKYC = class {
923
1182
  async initWithEmbedToken(config) {
924
1183
  const pk = config.publishableKey;
925
1184
  const designTokensPromise = pk ? fetchDesignTokens(this.apiBase, { pk }) : fetchDesignTokens(this.apiBase, { sessionId: config.sessionId });
926
- const sessionPromise = fetchPublicSession(
927
- this.apiBase,
928
- config.sessionId,
929
- config.embedToken
930
- );
931
1185
  const { loader, primaryColor } = await showLoaderWithEarlyColor(designTokensPromise, config.lang);
932
1186
  try {
933
- const [sessionData, designTokens] = await Promise.all([
934
- sessionPromise,
935
- designTokensPromise
936
- ]);
1187
+ const designTokens = await designTokensPromise;
937
1188
  const finalColor = primaryFrom(designTokens);
938
1189
  if (finalColor !== primaryColor) loader.updateColor(finalColor);
939
- const session = {
940
- id: sessionData.id,
941
- allowManualUpload: sessionData.allowManualUpload ?? config.allowManualUpload ?? true,
942
- clientMetadata: sessionData.clientMetadata || config.clientMetadata,
943
- qrCodeUrl: sessionData.qrCodeUrl,
944
- testMode: sessionData.testMode || false,
945
- poaEnabled: sessionData.poaEnabled || false,
946
- poaRequired: sessionData.poaRequired || false,
947
- mlAutoCaptureEnabled: sessionData.mlAutoCaptureEnabled || false,
948
- mlCropEnabled: sessionData.mlCropEnabled ?? true,
949
- mlVerifyEnabled: sessionData.mlVerifyEnabled || false
950
- };
951
- return this.buildWidget(config.embedToken, session, config, loader, designTokens);
1190
+ return this.buildWidget(config.embedToken, config.sessionId, config, loader, designTokens);
952
1191
  } catch (err) {
953
1192
  const msg = err.message || "Failed to initialize verification";
954
1193
  loader.showError(msg, config.lang);
@@ -974,7 +1213,7 @@ var FlonkKYC = class {
974
1213
  exchangePromise,
975
1214
  designTokensPromise
976
1215
  ]);
977
- return this.buildWidget(embedToken, session, config, loader, designTokens);
1216
+ return this.buildWidget(embedToken, config.sessionId || session.id, config, loader, designTokens);
978
1217
  } catch (err) {
979
1218
  const msg = err.message || "Failed to initialize verification";
980
1219
  loader.showError(msg, config.lang);
@@ -1021,32 +1260,21 @@ var FlonkKYC = class {
1021
1260
  }
1022
1261
  }
1023
1262
  // ── Core widget builder ──────────────────────────────
1024
- buildWidget(token, session, config, loader, designTokens) {
1263
+ buildWidget(token, sessionId, config, loader, designTokens) {
1025
1264
  const params = {
1026
1265
  mode: "embedded",
1027
- sessionId: config.sessionId || session.id,
1028
- token,
1029
- allowManualUpload: String(session.allowManualUpload !== false)
1266
+ sessionId,
1267
+ token
1030
1268
  };
1031
- if (session.testMode) params.testMode = "true";
1032
- if (session.poaEnabled) params.poaEnabled = "true";
1033
- if (session.poaRequired) params.poaRequired = "true";
1034
- if (session.mlAutoCaptureEnabled) params.mlAutoCaptureEnabled = "true";
1035
- if (session.mlCropEnabled !== false) params.mlCropEnabled = "true";
1036
- if (session.mlVerifyEnabled) params.mlVerifyEnabled = "true";
1037
- if (session.vaultReuseEnabled) params.vaultReuseEnabled = "true";
1038
- if (session.vaultReuseChallenge) params.vaultReuseChallenge = session.vaultReuseChallenge;
1039
- if (session.faceAutoCapture) params.faceAutoCapture = JSON.stringify(session.faceAutoCapture);
1040
- if (session.project) params.project = JSON.stringify(session.project);
1041
- params.policyReady = "1";
1269
+ if (config.allowManualUpload !== void 0) {
1270
+ params.allowManualUpload = String(config.allowManualUpload !== false);
1271
+ }
1042
1272
  if (designTokens?.colors) {
1043
1273
  params.designTokens = JSON.stringify(designTokens);
1044
1274
  }
1045
1275
  if (config.embedToken) params.embedToken = config.embedToken;
1046
- if (session.qrCodeUrl) params.qrCodeUrl = session.qrCodeUrl;
1047
- const clientMetadata = session.clientMetadata || config.clientMetadata;
1048
- if (clientMetadata) {
1049
- params.clientMetadata = JSON.stringify(clientMetadata);
1276
+ if (config.clientMetadata) {
1277
+ params.clientMetadata = JSON.stringify(config.clientMetadata);
1050
1278
  }
1051
1279
  if (config.lang) params.lang = config.lang;
1052
1280
  if (config.overlayColor) params.overlayColor = config.overlayColor;
@@ -1068,12 +1296,14 @@ var FlonkKYC = class {
1068
1296
  const filtered = Object.fromEntries(
1069
1297
  Object.entries(params).filter(([, v]) => v != null)
1070
1298
  );
1299
+ filtered[WIDGET_PARAMS.PROTOCOL_VERSION] = String(PROTOCOL_VERSION);
1071
1300
  const search = new URLSearchParams(filtered);
1072
1301
  const src = `${this.widgetUrl}/?${search.toString()}`;
1073
1302
  const iframe = createIframe(src);
1303
+ emitDiagnostic("IFRAME_FRESH", "info", "Built a fresh widget iframe");
1074
1304
  const mountTarget = opts.mount || document.body;
1075
1305
  const loader = opts.loader ?? (() => {
1076
- const l = new Loader();
1306
+ const l = makeLoader();
1077
1307
  l.show(opts.primaryColor, opts.lang);
1078
1308
  return l;
1079
1309
  })();
@@ -1109,11 +1339,18 @@ var FlonkKYC = class {
1109
1339
  onReady: opts.onReady
1110
1340
  });
1111
1341
  handler.listen();
1112
- handler.onReadyOnce(() => {
1113
- if (loader.element) {
1114
- transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1115
- }
1116
- });
1342
+ let revealed = false;
1343
+ const revealOnce = () => {
1344
+ if (revealed) return;
1345
+ revealed = true;
1346
+ clearTimeout(revealTimer);
1347
+ if (loader.element) transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1348
+ };
1349
+ const revealTimer = setTimeout(() => {
1350
+ emitDiagnostic("READY_TIMEOUT_REVEAL", "warn", "Widget did not signal READY in time; revealing anyway");
1351
+ revealOnce();
1352
+ }, 4e3);
1353
+ handler.onReadyOnce(revealOnce);
1117
1354
  return {
1118
1355
  iframe,
1119
1356
  destroy: cleanupAll
@@ -1201,10 +1438,16 @@ function FlonkKYCBrandingPreloader({
1201
1438
  return null;
1202
1439
  }
1203
1440
 
1441
+ exports.API_VERSION = API_VERSION;
1204
1442
  exports.FlonkError = FlonkError;
1205
1443
  exports.FlonkKYC = FlonkKYC;
1206
1444
  exports.FlonkKYCBrandingPreloader = FlonkKYCBrandingPreloader;
1207
1445
  exports.FlonkKYCWidget = FlonkKYCWidget;
1208
1446
  exports.FlonkValidationError = FlonkValidationError;
1447
+ exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
1448
+ exports.SDK_VERSION = SDK_VERSION;
1449
+ exports.WIDGET_EVENTS = WIDGET_EVENTS;
1450
+ exports.WIDGET_PARAMS = WIDGET_PARAMS;
1451
+ exports.addDiagnosticHandler = addDiagnosticHandler;
1209
1452
  //# sourceMappingURL=index.cjs.map
1210
1453
  //# sourceMappingURL=index.cjs.map