@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 +108 -2
- package/dist/index.cjs +330 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -4
- package/dist/index.d.ts +128 -4
- package/dist/index.js +325 -88
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +102 -32
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +938 -36
- package/dist/server.d.ts +938 -36
- package/dist/server.js +103 -33
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +950 -34
- package/package.json +78 -49
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({
|
|
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.
|
|
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
|
-
|
|
251
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
1263
|
+
buildWidget(token, sessionId, config, loader, designTokens) {
|
|
1025
1264
|
const params = {
|
|
1026
1265
|
mode: "embedded",
|
|
1027
|
-
sessionId
|
|
1028
|
-
token
|
|
1029
|
-
allowManualUpload: String(session.allowManualUpload !== false)
|
|
1266
|
+
sessionId,
|
|
1267
|
+
token
|
|
1030
1268
|
};
|
|
1031
|
-
if (
|
|
1032
|
-
|
|
1033
|
-
|
|
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 (
|
|
1047
|
-
|
|
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 =
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|