@arkyc/widget 1.2.11 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/realtime.mjs CHANGED
@@ -1,107 +1,89 @@
1
+ import { createRealtime } from "@arkstack/realtime";
2
+ import { REALTIME_EVENT } from "@arkyc/types";
1
3
  //#region src/realtime.ts
2
4
  /** CDN that serves any npm package as ESM (`…/pkg@ver/+esm`). */
3
5
  const CDN = "https://cdn.jsdelivr.net/npm/";
6
+ /** CDN paths for the SDK modules the realtime transports load. */
7
+ const CDN_PATHS = {
8
+ "pusher-js": "pusher-js@8/+esm",
9
+ "firebase/app": "firebase@11/app/+esm",
10
+ "firebase/auth": "firebase@11/auth/+esm",
11
+ "firebase/database": "firebase@11/database/+esm",
12
+ "firebase/messaging": "firebase@11/messaging/+esm"
13
+ };
14
+ /** The domain events the widget watches its session for. */
15
+ const SESSION_EVENTS = [REALTIME_EVENT.sessionTransition];
4
16
  /**
5
- * Import an optional transport SDK. Tries the bare package specifier first (a
6
- * bundler, import map, or Node resolves it), then falls back to the jsdelivr CDN
7
- * so a plain browser embed still loads it — a bare specifier like `pusher-js`
8
- * isn't resolvable at runtime in the browser, which would otherwise leave the
9
- * widget silently polling. Non-literal specifiers also keep the bundler from
10
- * trying to resolve these (they aren't hard dependencies) and avoid needing their
11
- * type declarations.
17
+ * Load a transport SDK by its package name (resolved by a bundler, import map or
18
+ * Node), falling back to jsDelivr so a plain `<script>` embed still gets it.
19
+ *
20
+ * @param specifier
21
+ * @returns
12
22
  */
13
- async function loadOptional(pkg, cdnPath) {
23
+ const importWithCdnFallback = async (specifier) => {
14
24
  try {
15
25
  return await import(
16
26
  /* @vite-ignore */
17
27
  /* webpackIgnore: true */
18
- pkg
28
+ specifier
19
29
  );
20
- } catch {
30
+ } catch (error) {
31
+ const path = CDN_PATHS[specifier];
32
+ if (!path) throw error;
21
33
  return await import(
22
34
  /* @vite-ignore */
23
35
  /* webpackIgnore: true */
24
- `${CDN}${cdnPath}`
36
+ `${CDN}${path}`
25
37
  );
26
38
  }
27
- }
39
+ };
28
40
  /**
29
- * Connect to whichever push transport the API reported for this session. Returns
30
- * null for `polling`/`off`/`memory` (no push — the widget polls instead) and also
31
- * when the transport SDK can't be loaded (e.g. a plain `<script>` standalone embed
32
- * with no bundler), so realtime degrades gracefully to polling. The SDK is
33
- * dynamically imported so only the active transport is ever pulled in.
41
+ * Connect to the session's push transport. Returns null for transports the
42
+ * widget does not stream over (polling, and Firebase Cloud Messaging, which
43
+ * would prompt the end user for notification permission), and rejects when the
44
+ * transport SDK cannot load, so the caller polls instead.
45
+ *
46
+ * @param config
47
+ * @param options
48
+ * @returns
34
49
  */
35
50
  async function createWidgetRealtimeClient(config, options) {
36
- try {
37
- if (config.transport === "pusher") return await createPusherClient(config, options);
38
- if (config.transport === "firebase") return await createFirebaseClient(config);
39
- } catch {
40
- return null;
41
- }
42
- return null;
43
- }
44
- async function createPusherClient(config, options) {
45
- const Pusher = (await loadOptional("pusher-js", "pusher-js@8/+esm")).default;
46
- const pusher = new Pusher(String(config.key ?? ""), {
47
- cluster: String(config.cluster ?? "mt1"),
48
- forceTLS: Boolean(config.forceTLS ?? false),
49
- ...config.wsHost ? {
50
- wsHost: String(config.wsHost),
51
- wsPort: Number(config.wsPort),
52
- wssPort: Number(config.wssPort ?? config.wsPort),
53
- enabledTransports: config.enabledTransports ?? ["ws", "wss"]
54
- } : {},
55
- channelAuthorization: {
56
- transport: "ajax",
57
- endpoint: options.authEndpoint,
58
- headers: { "X-Client-Token": options.token }
59
- }
51
+ if (config.transport !== "pusher" && config.transport !== "firebase-database") return null;
52
+ const authEndpoint = options.authEndpoint;
53
+ const auth = { headers: { "X-Client-Token": options.token } };
54
+ const pusher = config.pusher;
55
+ const firebaseDatabase = config["firebase-database"];
56
+ const client = createRealtime({
57
+ transport: config.transport,
58
+ pusher: pusher && {
59
+ ...pusher,
60
+ authEndpoint,
61
+ auth
62
+ },
63
+ "firebase-database": firebaseDatabase && {
64
+ ...firebaseDatabase,
65
+ authEndpoint,
66
+ auth
67
+ },
68
+ importModule: importWithCdnFallback
60
69
  });
70
+ await client.onPoll(() => {});
61
71
  return {
62
72
  subscribe(channel, handler) {
63
- const ch = pusher.subscribe(channel);
64
- const cb = (event, data) => handler(event, data);
65
- ch.bind_global(cb);
73
+ let active = true;
74
+ const stops = [];
75
+ for (const event of SESSION_EVENTS) client.listen(channel, event, (data) => handler(event, data)).then((stop) => active ? stops.push(stop) : stop()).catch(() => {});
66
76
  return () => {
67
- ch.unbind_global(cb);
68
- pusher.unsubscribe(channel);
77
+ active = false;
78
+ stops.forEach((stop) => stop());
69
79
  };
70
80
  },
71
81
  disconnect() {
72
- pusher.disconnect();
82
+ client.disconnect();
73
83
  }
74
84
  };
75
85
  }
76
- async function createFirebaseClient(config) {
77
- const [app, auth, database] = await Promise.all([
78
- loadOptional("firebase/app", "firebase@11/app/+esm"),
79
- loadOptional("firebase/auth", "firebase@11/auth/+esm"),
80
- loadOptional("firebase/database", "firebase@11/database/+esm")
81
- ]);
82
- const name = "arkyc-widget-realtime";
83
- const fbApp = app.getApps().some((a) => a.name === name) ? app.getApp(name) : app.initializeApp({
84
- apiKey: String(config.apiKey ?? ""),
85
- authDomain: String(config.authDomain ?? ""),
86
- projectId: String(config.projectId ?? ""),
87
- databaseURL: String(config.databaseURL ?? "")
88
- }, name);
89
- if (config.token) await auth.signInWithCustomToken(auth.getAuth(fbApp), config.token);
90
- const db = database.getDatabase(fbApp);
91
- const since = Date.now();
92
- return {
93
- subscribe(channel, handler) {
94
- const q = database.query(database.ref(db, `realtime/${channel}`), database.orderByChild("at"), database.startAt(since));
95
- const unsubscribe = database.onChildAdded(q, (snap) => {
96
- const value = snap.val();
97
- if (value) handler(value.event, value.payload);
98
- });
99
- return () => unsubscribe();
100
- },
101
- disconnect() {}
102
- };
103
- }
104
86
  //#endregion
105
- export { createWidgetRealtimeClient };
87
+ export { createWidgetRealtimeClient, importWithCdnFallback };
106
88
 
107
89
  //# sourceMappingURL=realtime.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"realtime.mjs","names":[],"sources":["../src/realtime.ts"],"sourcesContent":["import type { ClientRealtime } from './client'\n\n/** Fires for every event received on a subscribed channel. */\nexport type WidgetRealtimeHandler = (event: string, data: unknown) => void\n\n/** A connected push transport. `subscribe` returns an unsubscribe function. */\nexport interface WidgetRealtimeClient {\n subscribe(channel: string, handler: WidgetRealtimeHandler): () => void\n disconnect(): void\n}\n\n/** A factory that builds a push client from the session's realtime config. */\nexport type WidgetRealtimeFactory = (\n config: ClientRealtime,\n options: WidgetRealtimeOptions,\n) => Promise<WidgetRealtimeClient | null>\n\nexport interface WidgetRealtimeOptions {\n /** The client-token channel-auth endpoint (pusher). */\n authEndpoint: string\n /** The session's client token (sent as `X-Client-Token` to the authorizer). */\n token: string\n}\n\n/** CDN that serves any npm package as ESM (`…/pkg@ver/+esm`). */\nconst CDN = 'https://cdn.jsdelivr.net/npm/'\n\n/**\n * Import an optional transport SDK. Tries the bare package specifier first (a\n * bundler, import map, or Node resolves it), then falls back to the jsdelivr CDN\n * so a plain browser embed still loads it — a bare specifier like `pusher-js`\n * isn't resolvable at runtime in the browser, which would otherwise leave the\n * widget silently polling. Non-literal specifiers also keep the bundler from\n * trying to resolve these (they aren't hard dependencies) and avoid needing their\n * type declarations.\n */\nasync function loadOptional(pkg: string, cdnPath: string): Promise<unknown> {\n try {\n return await import(/* @vite-ignore */ /* webpackIgnore: true */ pkg)\n } catch {\n return await import(/* @vite-ignore */ /* webpackIgnore: true */ `${CDN}${cdnPath}`)\n }\n}\n\n/**\n * Connect to whichever push transport the API reported for this session. Returns\n * null for `polling`/`off`/`memory` (no push — the widget polls instead) and also\n * when the transport SDK can't be loaded (e.g. a plain `<script>` standalone embed\n * with no bundler), so realtime degrades gracefully to polling. The SDK is\n * dynamically imported so only the active transport is ever pulled in.\n */\nexport async function createWidgetRealtimeClient(\n config: ClientRealtime,\n options: WidgetRealtimeOptions,\n): Promise<WidgetRealtimeClient | null> {\n try {\n if (config.transport === 'pusher') return await createPusherClient(config, options)\n if (config.transport === 'firebase') return await createFirebaseClient(config)\n } catch {\n // Transport SDK unavailable / failed to connect — fall back to polling.\n return null\n }\n\n return null\n}\n\n// --- pusher-js (minimal surface we depend on) ---\n\ninterface PusherChannel {\n bind_global(cb: (event: string, data: unknown) => void): void\n unbind_global(cb: (event: string, data: unknown) => void): void\n}\ninterface PusherInstance {\n subscribe(channel: string): PusherChannel\n unsubscribe(channel: string): void\n disconnect(): void\n}\ntype PusherCtor = new (key: string, options: Record<string, unknown>) => PusherInstance\n\nasync function createPusherClient(\n config: ClientRealtime,\n options: WidgetRealtimeOptions,\n): Promise<WidgetRealtimeClient> {\n const mod = (await loadOptional('pusher-js', 'pusher-js@8/+esm')) as { default: PusherCtor }\n const Pusher = mod.default\n const pusher = new Pusher(String(config.key ?? ''), {\n cluster: String(config.cluster ?? 'mt1'),\n forceTLS: Boolean(config.forceTLS ?? false),\n ...(config.wsHost\n ? {\n wsHost: String(config.wsHost),\n wsPort: Number(config.wsPort),\n wssPort: Number(config.wssPort ?? config.wsPort),\n enabledTransports: (config.enabledTransports as string[]) ?? ['ws', 'wss'],\n }\n : {}),\n // The widget authorizes private channels with its short-lived client token,\n // which the API scopes to this session's own channel only.\n channelAuthorization: {\n transport: 'ajax',\n endpoint: options.authEndpoint,\n headers: { 'X-Client-Token': options.token },\n },\n })\n\n return {\n subscribe(channel, handler) {\n const ch = pusher.subscribe(channel)\n const cb = (event: string, data: unknown) => handler(event, data)\n ch.bind_global(cb)\n\n return () => {\n ch.unbind_global(cb)\n pusher.unsubscribe(channel)\n }\n },\n disconnect() {\n pusher.disconnect()\n },\n }\n}\n\n// --- firebase (minimal surface we depend on) ---\n\ninterface FirebaseApp {\n name: string\n}\ninterface FirebaseAppModule {\n initializeApp(options: Record<string, unknown>, name: string): FirebaseApp\n getApps(): FirebaseApp[]\n getApp(name: string): FirebaseApp\n}\ninterface FirebaseAuthModule {\n getAuth(app: FirebaseApp): unknown\n signInWithCustomToken(auth: unknown, token: string): Promise<unknown>\n}\ninterface FirebaseSnapshot {\n val(): { event: string; payload: unknown } | null\n}\ninterface FirebaseDatabaseModule {\n getDatabase(app: FirebaseApp): unknown\n ref(db: unknown, path: string): unknown\n query(ref: unknown, ...constraints: unknown[]): unknown\n orderByChild(path: string): unknown\n startAt(value: number): unknown\n onChildAdded(query: unknown, cb: (snap: FirebaseSnapshot) => void): () => void\n}\n\nasync function createFirebaseClient(config: ClientRealtime): Promise<WidgetRealtimeClient> {\n const [app, auth, database] = (await Promise.all([\n loadOptional('firebase/app', 'firebase@11/app/+esm'),\n loadOptional('firebase/auth', 'firebase@11/auth/+esm'),\n loadOptional('firebase/database', 'firebase@11/database/+esm'),\n ])) as [FirebaseAppModule, FirebaseAuthModule, FirebaseDatabaseModule]\n\n const name = 'arkyc-widget-realtime'\n const fbApp = app.getApps().some((a) => a.name === name)\n ? app.getApp(name)\n : app.initializeApp(\n {\n apiKey: String(config.apiKey ?? ''),\n authDomain: String(config.authDomain ?? ''),\n projectId: String(config.projectId ?? ''),\n databaseURL: String(config.databaseURL ?? ''),\n },\n name,\n )\n\n // The per-session custom token authorizes reads of this session's channel only.\n if (config.token) await auth.signInWithCustomToken(auth.getAuth(fbApp), config.token)\n\n const db = database.getDatabase(fbApp)\n const since = Date.now()\n\n return {\n subscribe(channel, handler) {\n const q = database.query(\n database.ref(db, `realtime/${channel}`),\n database.orderByChild('at'),\n database.startAt(since),\n )\n const unsubscribe = database.onChildAdded(q, (snap) => {\n const value = snap.val()\n if (value) handler(value.event, value.payload)\n })\n\n return () => unsubscribe()\n },\n disconnect() {\n // Firebase listeners are torn down per-subscription via the returned unsub.\n },\n }\n}\n"],"mappings":";;AAyBA,MAAM,MAAM;;;;;;;;;;AAWZ,eAAe,aAAa,KAAa,SAAmC;CAC1E,IAAI;EACF,OAAO,MAAM;;;GAAoD;;CACnE,QAAQ;EACN,OAAO,MAAM;;;GAAoD,GAAG,MAAM;;CAC5E;AACF;;;;;;;;AASA,eAAsB,2BACpB,QACA,SACsC;CACtC,IAAI;EACF,IAAI,OAAO,cAAc,UAAU,OAAO,MAAM,mBAAmB,QAAQ,OAAO;EAClF,IAAI,OAAO,cAAc,YAAY,OAAO,MAAM,qBAAqB,MAAM;CAC/E,QAAQ;EAEN,OAAO;CACT;CAEA,OAAO;AACT;AAeA,eAAe,mBACb,QACA,SAC+B;CAE/B,MAAM,UAAS,MADI,aAAa,aAAa,kBAAkB,EAAA,CAC5C;CACnB,MAAM,SAAS,IAAI,OAAO,OAAO,OAAO,OAAO,EAAE,GAAG;EAClD,SAAS,OAAO,OAAO,WAAW,KAAK;EACvC,UAAU,QAAQ,OAAO,YAAY,KAAK;EAC1C,GAAI,OAAO,SACP;GACE,QAAQ,OAAO,OAAO,MAAM;GAC5B,QAAQ,OAAO,OAAO,MAAM;GAC5B,SAAS,OAAO,OAAO,WAAW,OAAO,MAAM;GAC/C,mBAAoB,OAAO,qBAAkC,CAAC,MAAM,KAAK;EAC3E,IACA,CAAC;EAGL,sBAAsB;GACpB,WAAW;GACX,UAAU,QAAQ;GAClB,SAAS,EAAE,kBAAkB,QAAQ,MAAM;EAC7C;CACF,CAAC;CAED,OAAO;EACL,UAAU,SAAS,SAAS;GAC1B,MAAM,KAAK,OAAO,UAAU,OAAO;GACnC,MAAM,MAAM,OAAe,SAAkB,QAAQ,OAAO,IAAI;GAChE,GAAG,YAAY,EAAE;GAEjB,aAAa;IACX,GAAG,cAAc,EAAE;IACnB,OAAO,YAAY,OAAO;GAC5B;EACF;EACA,aAAa;GACX,OAAO,WAAW;EACpB;CACF;AACF;AA4BA,eAAe,qBAAqB,QAAuD;CACzF,MAAM,CAAC,KAAK,MAAM,YAAa,MAAM,QAAQ,IAAI;EAC/C,aAAa,gBAAgB,sBAAsB;EACnD,aAAa,iBAAiB,uBAAuB;EACrD,aAAa,qBAAqB,2BAA2B;CAC/D,CAAC;CAED,MAAM,OAAO;CACb,MAAM,QAAQ,IAAI,QAAQ,CAAC,CAAC,MAAM,MAAM,EAAE,SAAS,IAAI,IACnD,IAAI,OAAO,IAAI,IACf,IAAI,cACF;EACE,QAAQ,OAAO,OAAO,UAAU,EAAE;EAClC,YAAY,OAAO,OAAO,cAAc,EAAE;EAC1C,WAAW,OAAO,OAAO,aAAa,EAAE;EACxC,aAAa,OAAO,OAAO,eAAe,EAAE;CAC9C,GACA,IACF;CAGJ,IAAI,OAAO,OAAO,MAAM,KAAK,sBAAsB,KAAK,QAAQ,KAAK,GAAG,OAAO,KAAK;CAEpF,MAAM,KAAK,SAAS,YAAY,KAAK;CACrC,MAAM,QAAQ,KAAK,IAAI;CAEvB,OAAO;EACL,UAAU,SAAS,SAAS;GAC1B,MAAM,IAAI,SAAS,MACjB,SAAS,IAAI,IAAI,YAAY,SAAS,GACtC,SAAS,aAAa,IAAI,GAC1B,SAAS,QAAQ,KAAK,CACxB;GACA,MAAM,cAAc,SAAS,aAAa,IAAI,SAAS;IACrD,MAAM,QAAQ,KAAK,IAAI;IACvB,IAAI,OAAO,QAAQ,MAAM,OAAO,MAAM,OAAO;GAC/C,CAAC;GAED,aAAa,YAAY;EAC3B;EACA,aAAa,CAEb;CACF;AACF"}
1
+ {"version":3,"file":"realtime.mjs","names":[],"sources":["../src/realtime.ts"],"sourcesContent":["import { createRealtime, type ModuleImporter, type RealtimeConfig } from '@arkstack/realtime'\nimport { REALTIME_EVENT } from '@arkyc/types'\nimport type { ClientRealtime } from './client'\n\n/** Fires for every event received on a subscribed channel. */\nexport type WidgetRealtimeHandler = (event: string, data: unknown) => void\n\n/** A connected push transport. `subscribe` returns an unsubscribe function. */\nexport interface WidgetRealtimeClient {\n subscribe(channel: string, handler: WidgetRealtimeHandler): () => void\n disconnect(): void\n}\n\n/** A factory that builds a push client from the session's realtime config. */\nexport type WidgetRealtimeFactory = (\n config: ClientRealtime,\n options: WidgetRealtimeOptions,\n) => Promise<WidgetRealtimeClient | null>\n\nexport interface WidgetRealtimeOptions {\n /** The client-token channel-auth endpoint. */\n authEndpoint: string\n /** The session's client token (sent as `X-Client-Token` to the authorizer). */\n token: string\n}\n\n/** CDN that serves any npm package as ESM (`…/pkg@ver/+esm`). */\nconst CDN = 'https://cdn.jsdelivr.net/npm/'\n\n/** CDN paths for the SDK modules the realtime transports load. */\nconst CDN_PATHS: Record<string, string> = {\n 'pusher-js': 'pusher-js@8/+esm',\n 'firebase/app': 'firebase@11/app/+esm',\n 'firebase/auth': 'firebase@11/auth/+esm',\n 'firebase/database': 'firebase@11/database/+esm',\n 'firebase/messaging': 'firebase@11/messaging/+esm',\n}\n\n/** The domain events the widget watches its session for. */\nconst SESSION_EVENTS = [REALTIME_EVENT.sessionTransition]\n\n/**\n * Load a transport SDK by its package name (resolved by a bundler, import map or\n * Node), falling back to jsDelivr so a plain `<script>` embed still gets it.\n *\n * @param specifier\n * @returns\n */\nexport const importWithCdnFallback: ModuleImporter = async (specifier) => {\n try {\n return await import(/* @vite-ignore */ /* webpackIgnore: true */ specifier)\n } catch (error) {\n const path = CDN_PATHS[specifier]\n if (!path) throw error\n\n return await import(/* @vite-ignore */ /* webpackIgnore: true */ `${CDN}${path}`)\n }\n}\n\n/**\n * Connect to the session's push transport. Returns null for transports the\n * widget does not stream over (polling, and Firebase Cloud Messaging, which\n * would prompt the end user for notification permission), and rejects when the\n * transport SDK cannot load, so the caller polls instead.\n *\n * @param config\n * @param options\n * @returns\n */\nexport async function createWidgetRealtimeClient(\n config: ClientRealtime,\n options: WidgetRealtimeOptions,\n): Promise<WidgetRealtimeClient | null> {\n if (config.transport !== 'pusher' && config.transport !== 'firebase-database') return null\n\n const authEndpoint = options.authEndpoint\n const auth = { headers: { 'X-Client-Token': options.token } }\n const pusher = config.pusher as RealtimeConfig['pusher']\n const firebaseDatabase = config['firebase-database'] as RealtimeConfig['firebase-database']\n\n const client = createRealtime({\n transport: config.transport,\n pusher: pusher && { ...pusher, authEndpoint, auth },\n 'firebase-database': firebaseDatabase && { ...firebaseDatabase, authEndpoint, auth },\n importModule: importWithCdnFallback,\n })\n\n // Connect now, so a transport that cannot load falls back to polling.\n await client.onPoll(() => {})\n\n return {\n subscribe(channel, handler) {\n let active = true\n const stops: Array<() => void> = []\n\n for (const event of SESSION_EVENTS) {\n client\n .listen(channel, event, (data) => handler(event, data))\n .then((stop) => (active ? stops.push(stop) : stop()))\n .catch(() => {\n // An unauthorized or unavailable channel leaves the session to the poll budget.\n })\n }\n\n return () => {\n active = false\n stops.forEach((stop) => stop())\n }\n },\n disconnect() {\n void client.disconnect()\n },\n }\n}\n"],"mappings":";;;;AA2BA,MAAM,MAAM;;AAGZ,MAAM,YAAoC;CACxC,aAAa;CACb,gBAAgB;CAChB,iBAAiB;CACjB,qBAAqB;CACrB,sBAAsB;AACxB;;AAGA,MAAM,iBAAiB,CAAC,eAAe,iBAAiB;;;;;;;;AASxD,MAAa,wBAAwC,OAAO,cAAc;CACxE,IAAI;EACF,OAAO,MAAM;;;GAAoD;;CACnE,SAAS,OAAO;EACd,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,MAAM,MAAM;EAEjB,OAAO,MAAM;;;GAAoD,GAAG,MAAM;;CAC5E;AACF;;;;;;;;;;;AAYA,eAAsB,2BACpB,QACA,SACsC;CACtC,IAAI,OAAO,cAAc,YAAY,OAAO,cAAc,qBAAqB,OAAO;CAEtF,MAAM,eAAe,QAAQ;CAC7B,MAAM,OAAO,EAAE,SAAS,EAAE,kBAAkB,QAAQ,MAAM,EAAE;CAC5D,MAAM,SAAS,OAAO;CACtB,MAAM,mBAAmB,OAAO;CAEhC,MAAM,SAAS,eAAe;EAC5B,WAAW,OAAO;EAClB,QAAQ,UAAU;GAAE,GAAG;GAAQ;GAAc;EAAK;EAClD,qBAAqB,oBAAoB;GAAE,GAAG;GAAkB;GAAc;EAAK;EACnF,cAAc;CAChB,CAAC;CAGD,MAAM,OAAO,aAAa,CAAC,CAAC;CAE5B,OAAO;EACL,UAAU,SAAS,SAAS;GAC1B,IAAI,SAAS;GACb,MAAM,QAA2B,CAAC;GAElC,KAAK,MAAM,SAAS,gBAClB,OACG,OAAO,SAAS,QAAQ,SAAS,QAAQ,OAAO,IAAI,CAAC,CAAC,CACtD,MAAM,SAAU,SAAS,MAAM,KAAK,IAAI,IAAI,KAAK,CAAE,CAAC,CACpD,YAAY,CAEb,CAAC;GAGL,aAAa;IACX,SAAS;IACT,MAAM,SAAS,SAAS,KAAK,CAAC;GAChC;EACF;EACA,aAAa;GACX,OAAY,WAAW;EACzB;CACF;AACF"}
package/dist/theme.d.mts CHANGED
@@ -1,13 +1,12 @@
1
1
  import { RgbData } from "./types.mjs";
2
2
  import { ProjectBranding } from "@arkyc/types";
3
-
4
3
  //#region src/theme.d.ts
5
4
  /**
6
5
  * Resolves project branding into a concrete theme and renders its CSS. One class
7
6
  * owns the whole "theme" concern: defaulted colour/logo/radius values plus the
8
7
  * CSS-variable block and stylesheet derived from them.
9
8
  */
10
- declare class Theme {
9
+ export declare class Theme {
11
10
  readonly primaryColor: string;
12
11
  readonly borderRadius: number;
13
12
  readonly mode: 'light' | 'dark';
@@ -45,5 +44,4 @@ declare class Theme {
45
44
  }): RgbData;
46
45
  }
47
46
  //#endregion
48
- export { Theme };
49
47
  //# sourceMappingURL=theme.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.mts","names":[],"sources":["../src/theme.ts"],"mappings":";;;;;;AAuBA;;;cAAa,KAAA;EAAA,SACF,YAAA;EAAA,SACA,YAAA;EAAA,SACA,IAAA;EAAA,SACA,OAAA;EAHA;EAAA,SAKA,IAAA;EAHA;EAAA,SAKA,YAAA;EAFA;EAAA,SAIA,UAAA;EAAA;EAAA,SAEA,UAAA;EAEA;EAAA,SAAA,KAAA;;WAEA,MAAA;cAEG,QAAA,GAAW,eAAA;EAmBvB;;;;;EAAA,SAAA;EA+BA;;;;;EAfA,UAAA;EAAA,OAIO,GAAA,CAAI,CAAA;IAAK,YAAA;EAAA,IAAyB,OAAA;EAWzC,GAAA,CAAI,CAAA;IAAK,YAAA;EAAA,IAAyB,OAAA;AAAA"}
1
+ {"version":3,"file":"theme.d.mts","names":[],"sources":["../src/theme.ts"],"mappings":";;;;;;;;qBAuBa;WACF;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;EAEG,YAAA,WAAW;;;;;;EAmBvB;;;;;;EAgBA;SAIO,IAAI;IAAK;MAAyB;EAWzC,IAAI;IAAK;MAAyB"}
package/dist/types.d.mts CHANGED
@@ -3,10 +3,9 @@ import { FaceAnalyzer, FaceTuning } from "./face.mjs";
3
3
  import { AddressFormData } from "./ui.mjs";
4
4
  import { ProviderSignalHints } from "./client.mjs";
5
5
  import { WidgetRealtimeFactory } from "./realtime.mjs";
6
- import { DocumentType, LivenessChallenge, ProjectBranding, WidgetResult } from "@arkyc/types";
7
-
6
+ import { DocumentType, LivenessChallenge, ProjectBranding, SessionTransitionEvent, WidgetResult } from "@arkyc/types";
8
7
  //#region src/types.d.ts
9
- interface RgbData {
8
+ export interface RgbData {
10
9
  obj: {
11
10
  r: number;
12
11
  g: number;
@@ -15,20 +14,62 @@ interface RgbData {
15
14
  str: string;
16
15
  }
17
16
  /**
18
- * An event surfaced to consumers through `onEvent` / `handle.on(name, cb)`. The
19
- * stream is a firehose: `session.transition` (live status changes, from the
20
- * configured transport) plus the lifecycle events `complete` / `error` / `close`.
17
+ * Payload for `session.transition` — a live verification status change relayed
18
+ * from the configured transport (pusher/firebase/polling). A widget-scoped
19
+ * subset of the server's {@link SessionTransitionEvent} (no org/project ids).
21
20
  */
22
- interface WidgetEvent {
23
- /** Event name, e.g. `session.transition`, `complete`, `error`, `close`. */
24
- name: string;
25
- /** Event payload (shape depends on `name`). */
26
- data?: unknown;
21
+ export type WidgetSessionTransitionData = Pick<SessionTransitionEvent, 'session_id' | 'status' | 'previous_status'>;
22
+ /**
23
+ * Payload for `error` — a non-terminal failure surfaced as it happens (an
24
+ * expired session/token becomes knowable without waiting for the user to
25
+ * dismiss the screen). `status`/`error` are present only for typed API errors.
26
+ */
27
+ export interface WidgetErrorData {
28
+ /** Human-readable failure message. */
29
+ message: string;
30
+ /** HTTP status code, when the failure came from a typed API error. */
31
+ status?: number;
32
+ /** Stable machine-readable error key, when the failure came from a typed API error. */
33
+ error?: string;
34
+ }
35
+ /**
36
+ * The set of widget events and the payload each delivers. This is the single
37
+ * source of truth for event names and their `data` shapes: `onEvent`,
38
+ * {@link WidgetEvent}, {@link WidgetEventListener}, and {@link WidgetHandle.on}
39
+ * are all derived from it, so a listener's `data` is typed from the name.
40
+ */
41
+ export interface WidgetEventMap {
42
+ /** Live status change (fires on every real transition, once per change). */
43
+ 'session.transition': WidgetSessionTransitionData;
44
+ /** The flow settled successfully; carries the final verification result. */
45
+ complete: WidgetResult;
46
+ /** A failure occurred (may be recoverable; the flow has not necessarily ended). */
47
+ error: WidgetErrorData;
48
+ /** The widget was closed (camera released). No payload. */
49
+ close: undefined;
27
50
  }
28
- /** Subscribe to a named widget event; returns an unsubscribe function. */
29
- type WidgetEventListener = (data: unknown) => void;
51
+ /** A widget event name — one of the keys of {@link WidgetEventMap}. */
52
+ export type WidgetEventName = keyof WidgetEventMap;
53
+ /**
54
+ * An event surfaced to consumers through `onEvent`. Discriminated on `name`, so
55
+ * a `switch (event.name)` narrows `event.data` to the matching payload. The
56
+ * stream is a firehose: `session.transition` (live status changes) plus the
57
+ * lifecycle events `complete` / `error` / `close`.
58
+ */
59
+ export type WidgetEvent = { [K in WidgetEventName]: {
60
+ /** Event name (discriminant). */
61
+ name: K;
62
+ /** Event payload for this `name` (see {@link WidgetEventMap}). */
63
+ data: WidgetEventMap[K];
64
+ }; }[WidgetEventName];
65
+ /**
66
+ * Subscribe callback for a single named widget event; receives that event's
67
+ * payload (typed from the name via {@link WidgetEventMap}). Defaults to the
68
+ * union of all payloads when the name isn't fixed.
69
+ */
70
+ export type WidgetEventListener<K extends WidgetEventName = WidgetEventName> = (data: WidgetEventMap[K]) => void;
30
71
  /** Configuration for a {@link WidgetController}. */
31
- interface WidgetControllerConfig {
72
+ export interface WidgetControllerConfig {
32
73
  /** Short-lived client token for this session. */
33
74
  token: string;
34
75
  /** API origin (defaults to the widget's own origin). */
@@ -93,7 +134,7 @@ interface WidgetControllerConfig {
93
134
  maxHandoffPolls?: number;
94
135
  }
95
136
  /** Common options shared by every launch mode. */
96
- interface BaseWidgetOptions {
137
+ export interface BaseWidgetOptions {
97
138
  /** Short-lived client token minted by the integrator's backend. */
98
139
  token: string;
99
140
  /** API origin (defaults to the page's own origin). */
@@ -142,23 +183,25 @@ interface BaseWidgetOptions {
142
183
  documentTuning?: DocumentTuning;
143
184
  }
144
185
  /** Options for {@link ArkycWidget.mount} (inline mode). */
145
- interface MountWidgetOptions extends BaseWidgetOptions {
186
+ export interface MountWidgetOptions extends BaseWidgetOptions {
146
187
  /** Element (or selector) to mount the widget into. */
147
188
  container: string | HTMLElement;
148
189
  }
149
190
  /** A handle to an open/mounted widget. */
150
- interface WidgetHandle {
191
+ export interface WidgetHandle {
151
192
  /** Close the widget and release the camera (fires `onClose`). */
152
193
  close: () => void;
153
194
  /**
154
- * Subscribe to a named widget event (e.g. `session.transition`, `complete`).
155
- * Returns an unsubscribe function. Registering a listener activates the event
156
- * stream (events are only delivered while at least one listener is active).
195
+ * Subscribe to a named widget event (`session.transition`, `complete`,
196
+ * `error`, `close`). The `listener`'s `data` is typed from the `event` name
197
+ * via {@link WidgetEventMap}. Returns an unsubscribe function. Registering a
198
+ * listener activates the event stream (events are only delivered while at
199
+ * least one listener is active).
157
200
  */
158
- on: (event: string, listener: WidgetEventListener) => () => void;
201
+ on: <K extends WidgetEventName>(event: K, listener: WidgetEventListener<K>) => () => void;
159
202
  }
160
203
  /** High-level events the view raises back to the controller. */
161
- interface ViewHandlers {
204
+ export interface ViewHandlers {
162
205
  onClose(): void;
163
206
  onStart(): void;
164
207
  onDocumentSelected(type: DocumentType, country: string): void;
@@ -181,5 +224,4 @@ interface ViewHandlers {
181
224
  onShareLocation(): boolean | Promise<boolean>;
182
225
  }
183
226
  //#endregion
184
- export { BaseWidgetOptions, MountWidgetOptions, RgbData, ViewHandlers, WidgetControllerConfig, WidgetEvent, WidgetEventListener, WidgetHandle };
185
227
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;UAQiB,OAAA;EACf,GAAA;IAAO,CAAA;IAAW,CAAA;IAAW,CAAA;EAAA;EAC7B,GAAA;AAAA;;;;;AAAG;UAQY,WAAA;EAAW;EAE1B,IAAA;EAAA;EAEA,IAAI;AAAA;;KAIM,mBAAA,IAAuB,IAAa;;UAG/B,sBAAA;EAAA;EAEf,KAAA;;EAEA,OAAA;EAUU;EARV,OAAA;EAUkB;EARlB,UAAA;EACA,QAAA,GAAW,eAAA;EAsBL;;;;EAjBN,OAAA,GAAU,mBAAA;EACV,UAAA,IAAc,MAAA,EAAQ,YAAA;EACtB,OAAA,IAAW,KAAA,EAAO,KAAA;EAClB,OAAA;EAqCuC;;;;;EA/BvC,OAAA,IAAW,KAAA,EAAO,WAAA;EAdlB;EAgBA,QAAA;EAXA;EAaA,YAAA;EAGA,KAAA,UAAe,KAAA;EACf,GAAA,GAAM,QAAA;EACN,GAAA,GAAM,MAAA;EACN,GAAA,GAAM,SAAA;EAjBY;;;;;EAuBlB,YAAA,GAAe,YAAA;EAdf;EAgBA,UAAA,GAAa,UAAA;EAXb;;;;;EAiBA,gBAAA,GAAmB,gBAAA;EAdnB;EAgBA,cAAA,GAAiB,cAAA;EAVjB;;;;EAeA,eAAA,GAAkB,qBAAA;EAPC;EASnB,SAAA,IAAa,EAAA,cAAgB,EAAA;EAPZ;EASjB,WAAA;EAJkB;EAMlB,MAAA;EAJa;EAMb,QAAA;EAJA;EAMA,eAAA;AAAA;;UAIe,iBAAA;EAJA;EAMf,KAAA;EAFgC;EAIhC,OAAA;EAaW;;;;;EAPX,OAAA;EA4BM;;;;EAvBN,UAAA;EA+BmB;EA7BnB,QAAA,GAAW,eAAA;EA+BoB;;;;EA1B/B,UAAA;EAPA;EASA,OAAA,GAAU,mBAAA;EACV,UAAA,IAAc,MAAA,EAAQ,YAAA;EACtB,OAAA,IAAW,KAAA,EAAO,KAAA;EAClB,OAAA;EAHU;;;;;;EAUV,OAAA,IAAW,KAAA,EAAO,WAAA;EAGlB,KAAA,UAAe,KAAA;EACf,GAAA,GAAM,QAAA;EACN,GAAA,GAAM,MAAA;EACN,GAAA,GAAM,SAAA;EAHN;EAKA,YAAA,GAAe,YAAA;EAJf;EAMA,UAAA,GAAa,UAAA;EALb;EAOA,gBAAA,GAAmB,gBAAA;EANnB;EAQA,cAAA,GAAiB,cAAA;AAAA;;UAIF,kBAAA,SAA2B,iBAAiB;EAR9C;EAUb,SAAA,WAAoB,WAAA;AAAA;;UAIL,YAAA;EAVgB;EAY/B,KAAA;EARe;;;;;EAcf,EAAA,GAAK,KAAA,UAAe,QAAA,EAAU,mBAAmB;AAAA;;UAIlC,YAAA;EACf,OAAA;EACA,OAAA;EACA,kBAAA,CAAmB,IAAA,EAAM,YAAA,EAAc,OAAA;EAPU;EASjD,OAAA,CAAQ,IAAA,EAAM,IAAA;EATd;;;;EAcA,gBAAA,CAAiB,KAAA,EAAO,IAAA,SAAa,SAAA,EAAW,iBAAA,IAAqB,MAAA,GAAS,IAAA;EAd7B;EAgBjD,aAAA;EAZ2B;EAc3B,UAAA;EAXyB;EAazB,cAAA;EANwB;EAQxB,SAAA,CAAU,IAAA,EAAM,eAAA;EAR8D;EAU9E,eAAA,cAA6B,OAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;iBAciB;EACf;IAAO;IAAW;IAAW;;EAC7B;;;;;;;YAQU,8BAA8B,KAAK;;;;;;iBAO9B;;EAEf;;EAEA;;EAEA;;;;;;;;iBASe;;EAEf,sBAAsB;;EAEtB,UAAU;;EAEV,OAAO;;EAEP;;;YAIU,wBAAwB;;;;;;;YAQxB,iBACT,KAAK;;EAEJ,MAAM;;EAEN,MAAM,eAAe;KAEvB;;;;;;YAOU,oBAAoB,UAAU,kBAAkB,oBAAoB,MAAM,eAAe;;iBAGpF;;EAEf;;EAEA;;EAEA;;EAEA;EACA,WAAW;;;;;EAKX,UAAU;EACV,cAAc,QAAQ;EACtB,WAAW,OAAO;EAClB;;;;;;EAMA,WAAW,OAAO;;EAElB;;EAEA;EAGA,eAAe;EACf,MAAM;EACN,MAAM;EACN,MAAM;;;;;;EAMN,eAAe;;EAEf,aAAa;;;;;;EAMb,mBAAmB;;EAEnB,iBAAiB;;;;;EAKjB,kBAAkB;;EAElB,aAAa,gBAAgB;;EAE7B;;EAEA;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;;;;;EAMA;;;;;EAKA;;EAEA,WAAW;;;;;EAKX;;EAEA,UAAU;EACV,cAAc,QAAQ;EACtB,WAAW,OAAO;EAClB;;;;;;;EAOA,WAAW,OAAO;EAGlB,eAAe;EACf,MAAM;EACN,MAAM;EACN,MAAM;;EAEN,eAAe;;EAEf,aAAa;;EAEb,mBAAmB;;EAEnB,iBAAiB;;;iBAIF,2BAA2B;;EAE1C,oBAAoB;;;iBAIL;;EAEf;;;;;;;;EAQA,KAAK,UAAU,iBAAiB,OAAO,GAAG,UAAU,oBAAoB;;;iBAIzD;EACf;EACA;EACA,mBAAmB,MAAM,cAAc;;EAEvC,QAAQ,MAAM;;;;;EAKd,iBAAiB,OAAO,aAAa,WAAW,qBAAqB,SAAS;;EAE9E;;EAEA;;EAEA;;EAEA,UAAU,MAAM;;EAEhB,6BAA6B"}
package/dist/ui.d.mts CHANGED
@@ -1,8 +1,10 @@
1
+ import "./face.mjs";
2
+ import "./theme.mjs";
3
+ import "./types.mjs";
1
4
  import { DocumentType, LivenessChallenge, ProjectBranding, VerificationDecision, VerificationStatus, WidgetStep } from "@arkyc/types";
2
-
3
5
  //#region src/ui.d.ts
4
6
  /** The data the address-entry screen collects before submission. */
5
- interface AddressFormData {
7
+ export interface AddressFormData {
6
8
  line1: string | null;
7
9
  line2: string | null;
8
10
  city: string | null;
@@ -15,5 +17,4 @@ interface AddressFormData {
15
17
  useLocation: boolean;
16
18
  }
17
19
  //#endregion
18
- export { AddressFormData };
19
20
  //# sourceMappingURL=ui.d.mts.map
package/dist/ui.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.mts","names":[],"sources":["../src/ui.ts"],"mappings":";;;;UAWiB,eAAA;EACf,KAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;EACA,OAAA;EAES;EAAT,GAAA,EAAK,IAAI;EANT;EAQA,WAAA;AAAA"}
1
+ {"version":3,"file":"ui.d.mts","names":[],"sources":["../src/ui.ts"],"mappings":";;;;;;iBAWiB;EACf;EACA;EACA;EACA;EACA;EACA;;EAEA,KAAK;;EAEL"}
package/dist/ui.mjs CHANGED
@@ -137,7 +137,7 @@ var WidgetView = class {
137
137
  */
138
138
  fillBrand() {
139
139
  this.clear(this.brandEl);
140
- const lastTenMins = Date.now() - Math.floor(Math.random() * (600 * 1e3));
140
+ const lastTenMins = Date.now() - Math.floor(Math.random() * 6e5);
141
141
  const branded = this.theme.showBranding && (this.theme.logoUrl || this.theme.name);
142
142
  this.dom.append(this.brandEl, branded ? [this.theme.logoUrl && this.dom.h("img", {
143
143
  class: "arkyc-logo",
@@ -446,11 +446,12 @@ var WidgetView = class {
446
446
  if (!sample) return;
447
447
  fillEma = ema(fillEma, sample.fill);
448
448
  edgeEma = ema(edgeEma, sample.edgeStrength);
449
- const { ready, hint: message } = documentGuidance({
449
+ const smoothed = {
450
450
  ...sample,
451
451
  fill: fillEma,
452
452
  edgeStrength: edgeEma
453
- }, this.docTuning);
453
+ };
454
+ const { ready, hint: message } = documentGuidance(smoothed, this.docTuning);
454
455
  hint.textContent = message;
455
456
  doc.setFrame(sample.present ? sample.rect : null);
456
457
  doc.setQuality(ready ? "good" : sample.present ? "wait" : "bad");