@flonkid/kyc 1.8.2 → 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.2";
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
@@ -229,10 +246,14 @@ function createIframe(src) {
229
246
  top: "0",
230
247
  left: "0",
231
248
  zIndex: "9999",
232
- background: "transparent",
233
- 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.
234
253
  opacity: "0",
235
254
  visibility: "hidden",
255
+ background: "transparent",
256
+ backgroundColor: "transparent",
236
257
  borderRadius: d ? "0" : "",
237
258
  boxShadow: d ? "none" : "",
238
259
  colorScheme: "normal"
@@ -267,15 +288,15 @@ function adjustZIndex(loader, iframe) {
267
288
  function transitionLoaderToIframe(loader, iframe, onDone) {
268
289
  const d = isDesktop();
269
290
  const dur = d ? 300 : 500;
270
- setStyles(iframe, { opacity: "0", visibility: "hidden" });
271
291
  const card = loader.querySelector("div");
272
292
  if (card) {
273
293
  setStyles(card, {
294
+ transition: "transform 300ms ease-out, opacity 300ms ease-out",
274
295
  transform: d ? "translateY(-10px) scale(0.98)" : "translateY(-15px) scale(0.96)",
275
296
  opacity: "0"
276
297
  });
277
298
  }
278
- loader.style.opacity = "0";
299
+ setStyles(loader, { transition: "opacity 300ms ease-out", opacity: "0" });
279
300
  setTimeout(() => {
280
301
  onDone();
281
302
  setStyles(iframe, {
@@ -286,6 +307,160 @@ function transitionLoaderToIframe(loader, iframe, onDone) {
286
307
  }, dur);
287
308
  }
288
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
+
289
464
  // src/browser/loader.ts
290
465
  var LOADER_I18N = {
291
466
  en: { title: "Initializing...", subtitle: "Loading KYC widget", errorTitle: "Something went wrong", close: "Close" },
@@ -535,8 +710,81 @@ var Loader = class {
535
710
  this.cleanup = null;
536
711
  }
537
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
+ }
538
775
 
539
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
+ }
540
788
  var MessageHandler = class {
541
789
  constructor(iframeSrc, iframe, callbacks) {
542
790
  this.iframeSrc = iframeSrc;
@@ -569,6 +817,7 @@ var MessageHandler = class {
569
817
  this.completionHandled = true;
570
818
  this.callbacks.onError?.(data.error || "Unknown error");
571
819
  } else if (type === WIDGET_EVENTS.READY) {
820
+ checkProtocol(data);
572
821
  this.callbacks.onReady?.();
573
822
  }
574
823
  };
@@ -720,14 +969,55 @@ async function showLoaderWithEarlyColor(tokensPromise, lang) {
720
969
  new Promise((resolve) => setTimeout(() => resolve(null), EARLY_COLOR_BUDGET_MS))
721
970
  ]);
722
971
  const primaryColor = primaryFrom(earlyTokens);
723
- const loader = new Loader();
972
+ const loader = makeLoader();
724
973
  loader.show(primaryColor, lang);
725
974
  return { loader, primaryColor };
726
975
  }
727
976
  var FlonkKYC = class {
728
977
  constructor(options = {}) {
978
+ this.disposeDiagnostics = null;
729
979
  this.widgetUrl = (options.widgetUrl || DEFAULT_WIDGET_URL).replace(/\/$/, "");
730
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
+ });
731
1021
  }
732
1022
  /**
733
1023
  * Warm the project's branding (colors) ahead of time so the widget paints the
@@ -1006,12 +1296,14 @@ var FlonkKYC = class {
1006
1296
  const filtered = Object.fromEntries(
1007
1297
  Object.entries(params).filter(([, v]) => v != null)
1008
1298
  );
1299
+ filtered[WIDGET_PARAMS.PROTOCOL_VERSION] = String(PROTOCOL_VERSION);
1009
1300
  const search = new URLSearchParams(filtered);
1010
1301
  const src = `${this.widgetUrl}/?${search.toString()}`;
1011
1302
  const iframe = createIframe(src);
1303
+ emitDiagnostic("IFRAME_FRESH", "info", "Built a fresh widget iframe");
1012
1304
  const mountTarget = opts.mount || document.body;
1013
1305
  const loader = opts.loader ?? (() => {
1014
- const l = new Loader();
1306
+ const l = makeLoader();
1015
1307
  l.show(opts.primaryColor, opts.lang);
1016
1308
  return l;
1017
1309
  })();
@@ -1047,11 +1339,18 @@ var FlonkKYC = class {
1047
1339
  onReady: opts.onReady
1048
1340
  });
1049
1341
  handler.listen();
1050
- handler.onReadyOnce(() => {
1051
- if (loader.element) {
1052
- transitionLoaderToIframe(loader.element, iframe, () => loader.destroy());
1053
- }
1054
- });
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);
1055
1354
  return {
1056
1355
  iframe,
1057
1356
  destroy: cleanupAll
@@ -1139,10 +1438,16 @@ function FlonkKYCBrandingPreloader({
1139
1438
  return null;
1140
1439
  }
1141
1440
 
1441
+ exports.API_VERSION = API_VERSION;
1142
1442
  exports.FlonkError = FlonkError;
1143
1443
  exports.FlonkKYC = FlonkKYC;
1144
1444
  exports.FlonkKYCBrandingPreloader = FlonkKYCBrandingPreloader;
1145
1445
  exports.FlonkKYCWidget = FlonkKYCWidget;
1146
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;
1147
1452
  //# sourceMappingURL=index.cjs.map
1148
1453
  //# sourceMappingURL=index.cjs.map