@arkyc/widget 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/ArkycWidget.d.mts +50 -0
- package/dist/ArkycWidget.d.mts.map +1 -0
- package/dist/ArkycWidget.mjs +80 -0
- package/dist/ArkycWidget.mjs.map +1 -0
- package/dist/WidgetHandler.d.mts +24 -0
- package/dist/WidgetHandler.d.mts.map +1 -0
- package/dist/WidgetHandler.mjs +28 -0
- package/dist/WidgetHandler.mjs.map +1 -0
- package/dist/_virtual/_virtual_arkyc-theme-css.mjs +4 -0
- package/dist/arkyc-widget.iife.global.js +670 -0
- package/dist/arkyc-widget.iife.global.js.map +1 -0
- package/dist/capture.d.mts +73 -0
- package/dist/capture.d.mts.map +1 -0
- package/dist/capture.mjs +126 -0
- package/dist/capture.mjs.map +1 -0
- package/dist/client.d.mts +152 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +120 -0
- package/dist/client.mjs.map +1 -0
- package/dist/controller.d.mts +126 -0
- package/dist/controller.d.mts.map +1 -0
- package/dist/controller.mjs +582 -0
- package/dist/controller.mjs.map +1 -0
- package/dist/countries.mjs +967 -0
- package/dist/countries.mjs.map +1 -0
- package/dist/device.mjs +17 -0
- package/dist/device.mjs.map +1 -0
- package/dist/document.d.mts +108 -0
- package/dist/document.d.mts.map +1 -0
- package/dist/document.mjs +227 -0
- package/dist/document.mjs.map +1 -0
- package/dist/face.d.mts +82 -0
- package/dist/face.d.mts.map +1 -0
- package/dist/face.mjs +230 -0
- package/dist/face.mjs.map +1 -0
- package/dist/flow.d.mts +74 -0
- package/dist/flow.d.mts.map +1 -0
- package/dist/flow.mjs +132 -0
- package/dist/flow.mjs.map +1 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +16 -0
- package/dist/index.mjs.map +1 -0
- package/dist/qr.mjs +22 -0
- package/dist/qr.mjs.map +1 -0
- package/dist/realtime.d.mts +29 -0
- package/dist/realtime.d.mts.map +1 -0
- package/dist/realtime.mjs +107 -0
- package/dist/realtime.mjs.map +1 -0
- package/dist/theme.d.mts +42 -0
- package/dist/theme.d.mts.map +1 -0
- package/dist/theme.mjs +77 -0
- package/dist/theme.mjs.map +1 -0
- package/dist/types.d.mts +153 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/ui.mjs +931 -0
- package/dist/ui.mjs.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/capture.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Camera capture, scoped to one class. DOM-injectable so the widget core stays
|
|
4
|
+
* testable without a real browser. A live `getUserMedia` preview is used when
|
|
5
|
+
* available; the view always offers a file input as a fallback. An instance
|
|
6
|
+
* owns the active stream so callers don't track it themselves.
|
|
7
|
+
*/
|
|
8
|
+
/** Which camera to request: `environment` for documents, `user` for selfies. */
|
|
9
|
+
type Facing = 'environment' | 'user';
|
|
10
|
+
declare class Camera {
|
|
11
|
+
private readonly doc;
|
|
12
|
+
private readonly nav;
|
|
13
|
+
private stream;
|
|
14
|
+
constructor(doc?: Document, nav?: Navigator);
|
|
15
|
+
/** Whether live camera capture is available in this environment. */
|
|
16
|
+
get supported(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Start a camera stream and bind it to a `<video>` element for preview.
|
|
19
|
+
*
|
|
20
|
+
* @param video
|
|
21
|
+
* @param facing
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
start(video: HTMLVideoElement, facing: Facing): Promise<MediaStream>;
|
|
25
|
+
/**
|
|
26
|
+
* Stop the active stream and release the camera.
|
|
27
|
+
*/
|
|
28
|
+
stop(): void;
|
|
29
|
+
/** Whether this environment can record video (active liveness). */
|
|
30
|
+
get canRecord(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Start recording the active stream. Returns a handle whose `stop()` resolves
|
|
33
|
+
* to the recorded `video/webm` blob — used by the active-liveness flow to record
|
|
34
|
+
* the user performing the challenge sequence.
|
|
35
|
+
*
|
|
36
|
+
* @param stream
|
|
37
|
+
* @returns
|
|
38
|
+
*/
|
|
39
|
+
recordStart(stream: MediaStream): {
|
|
40
|
+
stop(): Promise<Blob>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Sample the current frame's average luminance for basic quality hints (too
|
|
44
|
+
* dark / glare). A cheap proxy with no model — enough to guide the user and
|
|
45
|
+
* gate auto-capture.
|
|
46
|
+
*
|
|
47
|
+
* @param video
|
|
48
|
+
* @returns A 0–1 brightness plus `tooDark`/`glare` flags, or `null` if unreadable.
|
|
49
|
+
*/
|
|
50
|
+
sampleQuality(video: HTMLVideoElement): {
|
|
51
|
+
brightness: number;
|
|
52
|
+
tooDark: boolean;
|
|
53
|
+
glare: boolean;
|
|
54
|
+
} | null;
|
|
55
|
+
/**
|
|
56
|
+
* Grab the current video frame as a JPEG `Blob` via an offscreen canvas.
|
|
57
|
+
*
|
|
58
|
+
* @param video
|
|
59
|
+
* @param quality
|
|
60
|
+
* @returns
|
|
61
|
+
*/
|
|
62
|
+
grabFrame(video: HTMLVideoElement, quality?: number): Promise<Blob>;
|
|
63
|
+
/**
|
|
64
|
+
* Read a selected file from an `<input type="file">` as a `Blob`.
|
|
65
|
+
*
|
|
66
|
+
* @param input
|
|
67
|
+
* @returns
|
|
68
|
+
*/
|
|
69
|
+
static fileFromInput(input: HTMLInputElement): Blob | null;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { Camera, Facing };
|
|
73
|
+
//# sourceMappingURL=capture.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.mts","names":[],"sources":["../src/capture.ts"],"mappings":";;AAQA;;;;AAAkB;AAElB;AAAA,KAFY,MAAA;AAAA,cAEC,MAAA;EAAA,iBAIQ,GAAA;EAAA,iBACA,GAAA;EAAA,QAJX,MAAA;cAGW,GAAA,GAAK,QAAA,EACL,GAAA,GAAK,SAAA;EAesC;EAAA,IAX1D,SAAA;EA6CgB;;;;;;;EAlCd,KAAA,CAAM,KAAA,EAAO,gBAAA,EAAkB,MAAA,EAAQ,MAAA,GAAS,OAAA,CAAQ,WAAA;EA+Gf;;;EA/F/C,IAAA;EA/BmB;EAAA,IAqCf,SAAA;;;;;;;;;EAYJ,WAAA,CAAY,MAAA,EAAQ,WAAA;IAAgB,IAAA,IAAQ,OAAA,CAAQ,IAAA;EAAA;EAlCE;;;;;;;;EA2DtD,aAAA,CAAc,KAAA,EAAO,gBAAA;IAAqB,UAAA;IAAoB,OAAA;IAAkB,KAAA;EAAA;EAAtC;;;;;;;EA6B1C,SAAA,CAAU,KAAA,EAAO,gBAAA,EAAkB,OAAA,YAAiB,OAAA,CAAQ,IAAA;EAAA;;;;;;EAAA,OAuBrD,aAAA,CAAc,KAAA,EAAO,gBAAA,GAAmB,IAAA;AAAA"}
|
package/dist/capture.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//#region src/capture.ts
|
|
2
|
+
var Camera = class {
|
|
3
|
+
doc;
|
|
4
|
+
nav;
|
|
5
|
+
stream = null;
|
|
6
|
+
constructor(doc = globalThis.document, nav = globalThis.navigator) {
|
|
7
|
+
this.doc = doc;
|
|
8
|
+
this.nav = nav;
|
|
9
|
+
}
|
|
10
|
+
/** Whether live camera capture is available in this environment. */
|
|
11
|
+
get supported() {
|
|
12
|
+
return !!this.nav?.mediaDevices?.getUserMedia;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Start a camera stream and bind it to a `<video>` element for preview.
|
|
16
|
+
*
|
|
17
|
+
* @param video
|
|
18
|
+
* @param facing
|
|
19
|
+
* @returns
|
|
20
|
+
*/
|
|
21
|
+
async start(video, facing) {
|
|
22
|
+
const stream = await this.nav.mediaDevices.getUserMedia({
|
|
23
|
+
video: { facingMode: facing },
|
|
24
|
+
audio: false
|
|
25
|
+
});
|
|
26
|
+
this.stream = stream;
|
|
27
|
+
video.srcObject = stream;
|
|
28
|
+
video.setAttribute("playsinline", "true");
|
|
29
|
+
video.muted = true;
|
|
30
|
+
await video.play().catch(() => void 0);
|
|
31
|
+
return stream;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Stop the active stream and release the camera.
|
|
35
|
+
*/
|
|
36
|
+
stop() {
|
|
37
|
+
this.stream?.getTracks().forEach((track) => track.stop());
|
|
38
|
+
this.stream = null;
|
|
39
|
+
}
|
|
40
|
+
/** Whether this environment can record video (active liveness). */
|
|
41
|
+
get canRecord() {
|
|
42
|
+
return typeof globalThis.MediaRecorder !== "undefined";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Start recording the active stream. Returns a handle whose `stop()` resolves
|
|
46
|
+
* to the recorded `video/webm` blob — used by the active-liveness flow to record
|
|
47
|
+
* the user performing the challenge sequence.
|
|
48
|
+
*
|
|
49
|
+
* @param stream
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
recordStart(stream) {
|
|
53
|
+
const chunks = [];
|
|
54
|
+
const recorder = new MediaRecorder(stream, { mimeType: "video/webm" });
|
|
55
|
+
recorder.ondataavailable = (event) => {
|
|
56
|
+
if (event.data && event.data.size > 0) chunks.push(event.data);
|
|
57
|
+
};
|
|
58
|
+
recorder.start();
|
|
59
|
+
return { stop: () => new Promise((resolve) => {
|
|
60
|
+
recorder.onstop = () => resolve(new Blob(chunks, { type: "video/webm" }));
|
|
61
|
+
recorder.stop();
|
|
62
|
+
}) };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sample the current frame's average luminance for basic quality hints (too
|
|
66
|
+
* dark / glare). A cheap proxy with no model — enough to guide the user and
|
|
67
|
+
* gate auto-capture.
|
|
68
|
+
*
|
|
69
|
+
* @param video
|
|
70
|
+
* @returns A 0–1 brightness plus `tooDark`/`glare` flags, or `null` if unreadable.
|
|
71
|
+
*/
|
|
72
|
+
sampleQuality(video) {
|
|
73
|
+
const canvas = this.doc.createElement("canvas");
|
|
74
|
+
canvas.width = 32;
|
|
75
|
+
canvas.height = 32;
|
|
76
|
+
const ctx = canvas.getContext("2d");
|
|
77
|
+
if (!ctx || !video.videoWidth) return null;
|
|
78
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
79
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
80
|
+
let sum = 0;
|
|
81
|
+
let bright = 0;
|
|
82
|
+
const pixels = data.length / 4;
|
|
83
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
84
|
+
const lum = (.299 * data[i] + .587 * data[i + 1] + .114 * data[i + 2]) / 255;
|
|
85
|
+
sum += lum;
|
|
86
|
+
if (lum > .92) bright += 1;
|
|
87
|
+
}
|
|
88
|
+
const brightness = sum / pixels;
|
|
89
|
+
return {
|
|
90
|
+
brightness,
|
|
91
|
+
tooDark: brightness < .25,
|
|
92
|
+
glare: bright / pixels > .15
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Grab the current video frame as a JPEG `Blob` via an offscreen canvas.
|
|
97
|
+
*
|
|
98
|
+
* @param video
|
|
99
|
+
* @param quality
|
|
100
|
+
* @returns
|
|
101
|
+
*/
|
|
102
|
+
grabFrame(video, quality = .92) {
|
|
103
|
+
const canvas = this.doc.createElement("canvas");
|
|
104
|
+
canvas.width = video.videoWidth || 1280;
|
|
105
|
+
canvas.height = video.videoHeight || 720;
|
|
106
|
+
const ctx = canvas.getContext("2d");
|
|
107
|
+
if (!ctx) return Promise.reject(/* @__PURE__ */ new Error("Canvas 2D context unavailable."));
|
|
108
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
canvas.toBlob((blob) => blob ? resolve(blob) : reject(/* @__PURE__ */ new Error("Failed to capture frame.")), "image/jpeg", quality);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read a selected file from an `<input type="file">` as a `Blob`.
|
|
115
|
+
*
|
|
116
|
+
* @param input
|
|
117
|
+
* @returns
|
|
118
|
+
*/
|
|
119
|
+
static fileFromInput(input) {
|
|
120
|
+
return input.files?.[0] ?? null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
export { Camera };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=capture.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.mjs","names":[],"sources":["../src/capture.ts"],"sourcesContent":["/**\n * Camera capture, scoped to one class. DOM-injectable so the widget core stays\n * testable without a real browser. A live `getUserMedia` preview is used when\n * available; the view always offers a file input as a fallback. An instance\n * owns the active stream so callers don't track it themselves.\n */\n\n/** Which camera to request: `environment` for documents, `user` for selfies. */\nexport type Facing = 'environment' | 'user'\n\nexport class Camera {\n private stream: MediaStream | null = null\n\n constructor(\n private readonly doc: Document = globalThis.document,\n private readonly nav: Navigator = globalThis.navigator,\n ) {}\n\n /** Whether live camera capture is available in this environment. */\n get supported(): boolean {\n return !!this.nav?.mediaDevices?.getUserMedia\n }\n\n /**\n * Start a camera stream and bind it to a `<video>` element for preview.\n *\n * @param video\n * @param facing\n * @returns\n */\n async start(video: HTMLVideoElement, facing: Facing): Promise<MediaStream> {\n const stream = await this.nav.mediaDevices.getUserMedia({\n video: { facingMode: facing },\n audio: false,\n })\n this.stream = stream\n video.srcObject = stream\n video.setAttribute('playsinline', 'true')\n video.muted = true\n await video.play().catch(() => undefined)\n return stream\n }\n\n /**\n * Stop the active stream and release the camera.\n */\n stop(): void {\n this.stream?.getTracks().forEach((track) => track.stop())\n this.stream = null\n }\n\n /** Whether this environment can record video (active liveness). */\n get canRecord(): boolean {\n return typeof (globalThis as { MediaRecorder?: unknown }).MediaRecorder !== 'undefined'\n }\n\n /**\n * Start recording the active stream. Returns a handle whose `stop()` resolves\n * to the recorded `video/webm` blob — used by the active-liveness flow to record\n * the user performing the challenge sequence.\n *\n * @param stream\n * @returns\n */\n recordStart(stream: MediaStream): { stop(): Promise<Blob> } {\n const chunks: BlobPart[] = []\n const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' })\n recorder.ondataavailable = (event) => {\n if (event.data && event.data.size > 0) chunks.push(event.data)\n }\n recorder.start()\n\n return {\n stop: () =>\n new Promise<Blob>((resolve) => {\n recorder.onstop = () => resolve(new Blob(chunks, { type: 'video/webm' }))\n recorder.stop()\n }),\n }\n }\n\n /**\n * Sample the current frame's average luminance for basic quality hints (too\n * dark / glare). A cheap proxy with no model — enough to guide the user and\n * gate auto-capture.\n *\n * @param video\n * @returns A 0–1 brightness plus `tooDark`/`glare` flags, or `null` if unreadable.\n */\n sampleQuality(video: HTMLVideoElement): { brightness: number; tooDark: boolean; glare: boolean } | null {\n const canvas = this.doc.createElement('canvas')\n // Downscale heavily — we only need an average, not detail.\n canvas.width = 32\n canvas.height = 32\n const ctx = canvas.getContext('2d')\n if (!ctx || !video.videoWidth) return null\n ctx.drawImage(video, 0, 0, canvas.width, canvas.height)\n\n const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height)\n let sum = 0\n let bright = 0\n const pixels = data.length / 4\n for (let i = 0; i < data.length; i += 4) {\n const lum = (0.299 * data[i]! + 0.587 * data[i + 1]! + 0.114 * data[i + 2]!) / 255\n sum += lum\n if (lum > 0.92) bright += 1\n }\n const brightness = sum / pixels\n return { brightness, tooDark: brightness < 0.25, glare: bright / pixels > 0.15 }\n }\n\n /**\n * Grab the current video frame as a JPEG `Blob` via an offscreen canvas.\n *\n * @param video\n * @param quality\n * @returns\n */\n grabFrame(video: HTMLVideoElement, quality = 0.92): Promise<Blob> {\n const canvas = this.doc.createElement('canvas')\n canvas.width = video.videoWidth || 1280\n canvas.height = video.videoHeight || 720\n const ctx = canvas.getContext('2d')\n if (!ctx) return Promise.reject(new Error('Canvas 2D context unavailable.'))\n ctx.drawImage(video, 0, 0, canvas.width, canvas.height)\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Failed to capture frame.'))),\n 'image/jpeg',\n quality,\n )\n })\n }\n\n /**\n * Read a selected file from an `<input type=\"file\">` as a `Blob`.\n *\n * @param input\n * @returns\n */\n static fileFromInput(input: HTMLInputElement): Blob | null {\n return input.files?.[0] ?? null\n }\n}\n"],"mappings":";AAUA,IAAa,SAAb,MAAoB;CAIC;CACA;CAJnB,SAAqC;CAErC,YACE,MAAiC,WAAW,UAC5C,MAAkC,WAAW,WAC7C;EAFiB,KAAA,MAAA;EACA,KAAA,MAAA;CAChB;;CAGH,IAAI,YAAqB;EACvB,OAAO,CAAC,CAAC,KAAK,KAAK,cAAc;CACnC;;;;;;;;CASA,MAAM,MAAM,OAAyB,QAAsC;EACzE,MAAM,SAAS,MAAM,KAAK,IAAI,aAAa,aAAa;GACtD,OAAO,EAAE,YAAY,OAAO;GAC5B,OAAO;EACT,CAAC;EACD,KAAK,SAAS;EACd,MAAM,YAAY;EAClB,MAAM,aAAa,eAAe,MAAM;EACxC,MAAM,QAAQ;EACd,MAAM,MAAM,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;EACxC,OAAO;CACT;;;;CAKA,OAAa;EACX,KAAK,QAAQ,UAAU,CAAC,CAAC,SAAS,UAAU,MAAM,KAAK,CAAC;EACxD,KAAK,SAAS;CAChB;;CAGA,IAAI,YAAqB;EACvB,OAAO,OAAQ,WAA2C,kBAAkB;CAC9E;;;;;;;;;CAUA,YAAY,QAAgD;EAC1D,MAAM,SAAqB,CAAC;EAC5B,MAAM,WAAW,IAAI,cAAc,QAAQ,EAAE,UAAU,aAAa,CAAC;EACrE,SAAS,mBAAmB,UAAU;GACpC,IAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,GAAG,OAAO,KAAK,MAAM,IAAI;EAC/D;EACA,SAAS,MAAM;EAEf,OAAO,EACL,YACE,IAAI,SAAe,YAAY;GAC7B,SAAS,eAAe,QAAQ,IAAI,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC,CAAC;GACxE,SAAS,KAAK;EAChB,CAAC,EACL;CACF;;;;;;;;;CAUA,cAAc,OAA0F;EACtG,MAAM,SAAS,KAAK,IAAI,cAAc,QAAQ;EAE9C,OAAO,QAAQ;EACf,OAAO,SAAS;EAChB,MAAM,MAAM,OAAO,WAAW,IAAI;EAClC,IAAI,CAAC,OAAO,CAAC,MAAM,YAAY,OAAO;EACtC,IAAI,UAAU,OAAO,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;EAEtD,MAAM,EAAE,SAAS,IAAI,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;EACnE,IAAI,MAAM;EACV,IAAI,SAAS;EACb,MAAM,SAAS,KAAK,SAAS;EAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;GACvC,MAAM,OAAO,OAAQ,KAAK,KAAM,OAAQ,KAAK,IAAI,KAAM,OAAQ,KAAK,IAAI,MAAO;GAC/E,OAAO;GACP,IAAI,MAAM,KAAM,UAAU;EAC5B;EACA,MAAM,aAAa,MAAM;EACzB,OAAO;GAAE;GAAY,SAAS,aAAa;GAAM,OAAO,SAAS,SAAS;EAAK;CACjF;;;;;;;;CASA,UAAU,OAAyB,UAAU,KAAqB;EAChE,MAAM,SAAS,KAAK,IAAI,cAAc,QAAQ;EAC9C,OAAO,QAAQ,MAAM,cAAc;EACnC,OAAO,SAAS,MAAM,eAAe;EACrC,MAAM,MAAM,OAAO,WAAW,IAAI;EAClC,IAAI,CAAC,KAAK,OAAO,QAAQ,uBAAO,IAAI,MAAM,gCAAgC,CAAC;EAC3E,IAAI,UAAU,OAAO,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;EAEtD,OAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,OAAO,QACJ,SAAU,OAAO,QAAQ,IAAI,IAAI,uBAAO,IAAI,MAAM,0BAA0B,CAAC,GAC9E,cACA,OACF;EACF,CAAC;CACH;;;;;;;CAQA,OAAO,cAAc,OAAsC;EACzD,OAAO,MAAM,QAAQ,MAAM;CAC7B;AACF"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { CaptureModel, DocumentType, LivenessChallenge, LivenessMode, ProjectBranding, VerificationStatus, WorkflowConfig } from "@arkyc/types";
|
|
2
|
+
|
|
3
|
+
//#region src/client.d.ts
|
|
4
|
+
/** The session view exposed by the Client/Widget API (`GET /v1/client/session`). */
|
|
5
|
+
interface ClientSession {
|
|
6
|
+
id: string;
|
|
7
|
+
status: VerificationStatus;
|
|
8
|
+
expires_at: string;
|
|
9
|
+
/** The capture flow this session runs (Phase 17). */
|
|
10
|
+
capture_model?: CaptureModel;
|
|
11
|
+
/** The active-liveness challenge sequence to prompt the user through. */
|
|
12
|
+
liveness_challenges?: LivenessChallenge[];
|
|
13
|
+
/** Cross-device handoff config resolved from the project setting. */
|
|
14
|
+
handoff?: ClientHandoff;
|
|
15
|
+
/** How to watch this session live (push transport params or `polling`). */
|
|
16
|
+
realtime?: ClientRealtime;
|
|
17
|
+
/** Project branding (colours/logo/name) so the widget themes itself at runtime. */
|
|
18
|
+
branding?: ProjectBranding | null;
|
|
19
|
+
/** The applied workflow (ordered, toggleable stages + skip_ocr), or null for the default flow. */
|
|
20
|
+
workflow?: WorkflowConfig | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Realtime connection info for this session (Phase 16). `transport` selects how
|
|
24
|
+
* the widget watches the session: `pusher`/`firebase` subscribe to `channel`;
|
|
25
|
+
* `polling` (and `off`/`memory`) mean poll the session endpoint instead. The
|
|
26
|
+
* remaining fields carry the active transport's public connection params; `token`
|
|
27
|
+
* is a per-session Firebase custom token when applicable.
|
|
28
|
+
*/
|
|
29
|
+
interface ClientRealtime {
|
|
30
|
+
transport: 'pusher' | 'firebase' | 'polling' | 'off' | 'memory';
|
|
31
|
+
/** The private channel this session's events publish to. */
|
|
32
|
+
channel: string;
|
|
33
|
+
/** Per-session Firebase custom token (firebase transport only). */
|
|
34
|
+
token?: string | null;
|
|
35
|
+
[param: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
/** Whether (and where) the widget may offer cross-device handoff. */
|
|
38
|
+
interface ClientHandoff {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
/** Whether a desktop user may continue on the desktop device instead of handing off. */
|
|
41
|
+
allow_desktop: boolean;
|
|
42
|
+
/** First-party hosted page the QR points to (the phone resumes the session there). */
|
|
43
|
+
url: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Mock-driver signal hints. Real provider drivers ignore these; the `mock`
|
|
47
|
+
* drivers use them to make the demo/playground flow deterministic.
|
|
48
|
+
*/
|
|
49
|
+
interface ProviderSignalHints {
|
|
50
|
+
quality_score?: number;
|
|
51
|
+
ocr_confidence?: number;
|
|
52
|
+
expired?: boolean;
|
|
53
|
+
liveness_score?: number;
|
|
54
|
+
liveness_passed?: boolean;
|
|
55
|
+
multiple_faces?: boolean;
|
|
56
|
+
face_similarity?: number;
|
|
57
|
+
face_match_passed?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Input for the document-front submission. */
|
|
60
|
+
interface DocumentFrontInput {
|
|
61
|
+
image?: Blob | null;
|
|
62
|
+
country?: string | null;
|
|
63
|
+
documentType?: DocumentType | null;
|
|
64
|
+
signals?: ProviderSignalHints;
|
|
65
|
+
}
|
|
66
|
+
/** Input for the document-back submission. */
|
|
67
|
+
interface DocumentBackInput {
|
|
68
|
+
image?: Blob | null;
|
|
69
|
+
country?: string | null;
|
|
70
|
+
documentType?: DocumentType | null;
|
|
71
|
+
}
|
|
72
|
+
/** Input for the liveness submission (passive selfie or active challenge video). */
|
|
73
|
+
interface LivenessInput {
|
|
74
|
+
selfie?: Blob | null;
|
|
75
|
+
/** Recorded challenge video (active mode). */
|
|
76
|
+
video?: Blob | null;
|
|
77
|
+
/** Passive (selfie) or active (challenge video). Defaults to passive. */
|
|
78
|
+
mode?: LivenessMode;
|
|
79
|
+
/** The challenge sequence the user performed, in order (active mode). */
|
|
80
|
+
challenges?: LivenessChallenge[];
|
|
81
|
+
signals?: ProviderSignalHints;
|
|
82
|
+
}
|
|
83
|
+
/** Input for completing the session (runs face-match + decision). */
|
|
84
|
+
interface CompleteInput {
|
|
85
|
+
signals?: ProviderSignalHints;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Raised when the Client API returns a non-2xx response.
|
|
89
|
+
*/
|
|
90
|
+
declare class WidgetApiError extends Error {
|
|
91
|
+
readonly status: number;
|
|
92
|
+
readonly code?: string;
|
|
93
|
+
constructor(message: string, status: number, code?: string);
|
|
94
|
+
}
|
|
95
|
+
/** Options for {@link ArkycClient}. */
|
|
96
|
+
interface ArkycClientOptions {
|
|
97
|
+
/** Short-lived client token minted for this session. */
|
|
98
|
+
token: string;
|
|
99
|
+
/** API origin (defaults to the same origin the widget is served from). */
|
|
100
|
+
baseUrl?: string;
|
|
101
|
+
/** Injectable for testing; defaults to the global `fetch`. */
|
|
102
|
+
fetch?: typeof fetch;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A thin, framework-agnostic client for the Arkyc Client/Widget API. Drives a
|
|
106
|
+
* single verification session with a short-lived client token. All responses
|
|
107
|
+
* are unwrapped from the standard `{ status, message, data }` envelope.
|
|
108
|
+
*/
|
|
109
|
+
declare class ArkycClient {
|
|
110
|
+
private readonly token;
|
|
111
|
+
private readonly baseUrl;
|
|
112
|
+
private readonly fetchImpl;
|
|
113
|
+
constructor(options: ArkycClientOptions);
|
|
114
|
+
/**
|
|
115
|
+
* Load the current session (marks it `started` on first call).
|
|
116
|
+
*
|
|
117
|
+
* @returns
|
|
118
|
+
*/
|
|
119
|
+
getSession(): Promise<ClientSession>;
|
|
120
|
+
/**
|
|
121
|
+
* Submit the document front image (triggers OCR + portrait extraction).
|
|
122
|
+
*
|
|
123
|
+
* @param input
|
|
124
|
+
* @returns
|
|
125
|
+
*/
|
|
126
|
+
submitDocumentFront(input: DocumentFrontInput): Promise<ClientSession>;
|
|
127
|
+
/**
|
|
128
|
+
* Submit the document back image.
|
|
129
|
+
*
|
|
130
|
+
* @param input
|
|
131
|
+
* @returns
|
|
132
|
+
*/
|
|
133
|
+
submitDocumentBack(input: DocumentBackInput): Promise<ClientSession>;
|
|
134
|
+
/**
|
|
135
|
+
* Submit the selfie for the liveness check.
|
|
136
|
+
*
|
|
137
|
+
* @param input
|
|
138
|
+
* @returns
|
|
139
|
+
*/
|
|
140
|
+
submitLiveness(input: LivenessInput): Promise<ClientSession>;
|
|
141
|
+
/**
|
|
142
|
+
* Finalise the session (runs face-match + the decision engine).
|
|
143
|
+
*
|
|
144
|
+
* @param input
|
|
145
|
+
* @returns
|
|
146
|
+
*/
|
|
147
|
+
complete(input?: CompleteInput): Promise<ClientSession>;
|
|
148
|
+
private request;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { ArkycClient, ArkycClientOptions, ClientRealtime, ClientSession, ProviderSignalHints, WidgetApiError };
|
|
152
|
+
//# sourceMappingURL=client.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;UAWiB,aAAA;EACf,EAAA;EACA,MAAA,EAAQ,kBAAA;EACR,UAAA;EADQ;EAGR,aAAA,GAAgB,YAAA;EAEM;EAAtB,mBAAA,GAAsB,iBAAA;EAIX;EAFX,OAAA,GAAU,aAAA;EAMC;EAJX,QAAA,GAAW,cAAA;EAIc;EAFzB,QAAA,GAAW,eAAA;EAXX;EAaA,QAAA,GAAW,cAAA;AAAA;;;;;;;;UAUI,cAAA;EACf,SAAA;EAbW;EAeX,OAAA;EAbW;EAeX,KAAA;EAAA,CACC,KAAA;AAAA;;UAIc,aAAA;EACf,OAAA;EAVA;EAYA,aAAA;EARA;EAUA,GAAA;AAAA;AATc;AAIhB;;;AAJgB,UAgBC,mBAAA;EACf,aAAA;EACA,cAAA;EACA,OAAA;EACA,cAAA;EACA,eAAA;EACA,cAAA;EACA,eAAA;EACA,iBAAA;AAAA;;UAIe,kBAAA;EACf,KAAA,GAAQ,IAAA;EACR,OAAA;EACA,YAAA,GAAe,YAAA;EACf,OAAA,GAAU,mBAAA;AAAA;;UAIK,iBAAA;EACf,KAAA,GAAQ,IAAA;EACR,OAAA;EACA,YAAA,GAAe,YAAY;AAAA;;UAIZ,aAAA;EACf,MAAA,GAAS,IAAA;EAZoB;EAc7B,KAAA,GAAQ,IAAA;EAjBR;EAmBA,IAAA,GAAO,YAAA;EAlBP;EAoBA,UAAA,GAAa,iBAAA;EACb,OAAA,GAAU,mBAAA;AAAA;;UAIK,aAAA;EACf,OAAA,GAAU,mBAAmB;AAAA;;;;cAMlB,cAAA,SAAuB,KAAK;EAAA,SAC9B,MAAA;EAAA,SACA,IAAA;cAEG,OAAA,UAAiB,MAAA,UAAgB,IAAA;AAAA;;UAS9B,kBAAA;EAhCA;EAkCf,KAAA;;EAEA,OAAA;EAjCQ;EAmCR,KAAA,UAAe,KAAK;AAAA;;;;;;cAkBT,WAAA;EAAA,iBACM,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,SAAA;cAEL,OAAA,EAAS,kBAAA;EAtDR;;;;AACgB;EAmE7B,UAAA,IAAc,OAAA,CAAQ,aAAA;EA/DM;;;AACC;AAM/B;;EAkEE,mBAAA,CAAoB,KAAA,EAAO,kBAAA,GAAqB,OAAA,CAAQ,aAAA;EAlEjB;;;;;;EAiFvC,kBAAA,CAAmB,KAAA,EAAO,iBAAA,GAAoB,OAAA,CAAQ,aAAA;EA7ET;;AAAa;AAS5D;;;EAkFE,cAAA,CAAe,KAAA,EAAO,aAAA,GAAgB,OAAA,CAAQ,aAAA;EAhF9C;;;;;AAIoB;EA4FpB,QAAA,CAAS,KAAA,GAAO,aAAA,GAAqB,OAAA,CAAQ,aAAA;EAAA,QAI/B,OAAA;AAAA"}
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
//#region src/client.ts
|
|
2
|
+
/**
|
|
3
|
+
* Raised when the Client API returns a non-2xx response.
|
|
4
|
+
*/
|
|
5
|
+
var WidgetApiError = class extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, status, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "WidgetApiError";
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function appendSignals(form, signals) {
|
|
16
|
+
if (!signals) return;
|
|
17
|
+
for (const [key, value] of Object.entries(signals)) {
|
|
18
|
+
if (value == null) continue;
|
|
19
|
+
form.append(key, String(value));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A thin, framework-agnostic client for the Arkyc Client/Widget API. Drives a
|
|
24
|
+
* single verification session with a short-lived client token. All responses
|
|
25
|
+
* are unwrapped from the standard `{ status, message, data }` envelope.
|
|
26
|
+
*/
|
|
27
|
+
var ArkycClient = class {
|
|
28
|
+
token;
|
|
29
|
+
baseUrl;
|
|
30
|
+
fetchImpl;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
if (!options.token) throw new Error("ArkycClient requires a client `token`.");
|
|
33
|
+
this.token = options.token;
|
|
34
|
+
this.baseUrl = (options.baseUrl ?? "").replace(/\/$/, "");
|
|
35
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
36
|
+
if (!f) throw new Error("No `fetch` implementation available.");
|
|
37
|
+
this.fetchImpl = f.bind(globalThis);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load the current session (marks it `started` on first call).
|
|
41
|
+
*
|
|
42
|
+
* @returns
|
|
43
|
+
*/
|
|
44
|
+
getSession() {
|
|
45
|
+
return this.request("GET", "/v1/client/session");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Submit the document front image (triggers OCR + portrait extraction).
|
|
49
|
+
*
|
|
50
|
+
* @param input
|
|
51
|
+
* @returns
|
|
52
|
+
*/
|
|
53
|
+
submitDocumentFront(input) {
|
|
54
|
+
const form = new FormData();
|
|
55
|
+
if (input.image) form.append("image", input.image, "document-front.jpg");
|
|
56
|
+
if (input.country) form.append("country", input.country);
|
|
57
|
+
if (input.documentType) form.append("document_type", input.documentType);
|
|
58
|
+
appendSignals(form, input.signals);
|
|
59
|
+
return this.request("POST", "/v1/client/document/front", form);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Submit the document back image.
|
|
63
|
+
*
|
|
64
|
+
* @param input
|
|
65
|
+
* @returns
|
|
66
|
+
*/
|
|
67
|
+
submitDocumentBack(input) {
|
|
68
|
+
const form = new FormData();
|
|
69
|
+
if (input.image) form.append("image", input.image, "document-back.jpg");
|
|
70
|
+
if (input.country) form.append("country", input.country);
|
|
71
|
+
if (input.documentType) form.append("document_type", input.documentType);
|
|
72
|
+
return this.request("POST", "/v1/client/document/back", form);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Submit the selfie for the liveness check.
|
|
76
|
+
*
|
|
77
|
+
* @param input
|
|
78
|
+
* @returns
|
|
79
|
+
*/
|
|
80
|
+
submitLiveness(input) {
|
|
81
|
+
const form = new FormData();
|
|
82
|
+
if (input.selfie) form.append("selfie", input.selfie, "selfie.jpg");
|
|
83
|
+
if (input.video) form.append("video", input.video, "liveness.webm");
|
|
84
|
+
if (input.mode) form.append("mode", input.mode);
|
|
85
|
+
if (input.challenges) form.append("challenges", JSON.stringify(input.challenges));
|
|
86
|
+
appendSignals(form, input.signals);
|
|
87
|
+
return this.request("POST", "/v1/client/liveness", form);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Finalise the session (runs face-match + the decision engine).
|
|
91
|
+
*
|
|
92
|
+
* @param input
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
complete(input = {}) {
|
|
96
|
+
return this.request("POST", "/v1/client/complete", input.signals ?? {});
|
|
97
|
+
}
|
|
98
|
+
async request(method, path, body) {
|
|
99
|
+
const headers = { "X-Client-Token": this.token };
|
|
100
|
+
let payload;
|
|
101
|
+
if (body instanceof FormData) payload = body;
|
|
102
|
+
else if (body) {
|
|
103
|
+
headers["Content-Type"] = "application/json";
|
|
104
|
+
payload = JSON.stringify(body);
|
|
105
|
+
}
|
|
106
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
107
|
+
method,
|
|
108
|
+
headers,
|
|
109
|
+
body: payload
|
|
110
|
+
});
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
const json = text ? JSON.parse(text) : {};
|
|
113
|
+
if (!res.ok) throw new WidgetApiError(json.message ?? `Request failed (${res.status})`, res.status);
|
|
114
|
+
return json.data;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
export { ArkycClient, WidgetApiError };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n CaptureModel,\n DocumentType,\n LivenessChallenge,\n LivenessMode,\n ProjectBranding,\n VerificationStatus,\n WorkflowConfig,\n} from '@arkyc/types'\n\n/** The session view exposed by the Client/Widget API (`GET /v1/client/session`). */\nexport interface ClientSession {\n id: string\n status: VerificationStatus\n expires_at: string\n /** The capture flow this session runs (Phase 17). */\n capture_model?: CaptureModel\n /** The active-liveness challenge sequence to prompt the user through. */\n liveness_challenges?: LivenessChallenge[]\n /** Cross-device handoff config resolved from the project setting. */\n handoff?: ClientHandoff\n /** How to watch this session live (push transport params or `polling`). */\n realtime?: ClientRealtime\n /** Project branding (colours/logo/name) so the widget themes itself at runtime. */\n branding?: ProjectBranding | null\n /** The applied workflow (ordered, toggleable stages + skip_ocr), or null for the default flow. */\n workflow?: WorkflowConfig | null\n}\n\n/**\n * Realtime connection info for this session (Phase 16). `transport` selects how\n * the widget watches the session: `pusher`/`firebase` subscribe to `channel`;\n * `polling` (and `off`/`memory`) mean poll the session endpoint instead. The\n * remaining fields carry the active transport's public connection params; `token`\n * is a per-session Firebase custom token when applicable.\n */\nexport interface ClientRealtime {\n transport: 'pusher' | 'firebase' | 'polling' | 'off' | 'memory'\n /** The private channel this session's events publish to. */\n channel: string\n /** Per-session Firebase custom token (firebase transport only). */\n token?: string | null\n [param: string]: unknown\n}\n\n/** Whether (and where) the widget may offer cross-device handoff. */\nexport interface ClientHandoff {\n enabled: boolean\n /** Whether a desktop user may continue on the desktop device instead of handing off. */\n allow_desktop: boolean\n /** First-party hosted page the QR points to (the phone resumes the session there). */\n url: string\n}\n\n/**\n * Mock-driver signal hints. Real provider drivers ignore these; the `mock`\n * drivers use them to make the demo/playground flow deterministic.\n */\nexport interface ProviderSignalHints {\n quality_score?: number\n ocr_confidence?: number\n expired?: boolean\n liveness_score?: number\n liveness_passed?: boolean\n multiple_faces?: boolean\n face_similarity?: number\n face_match_passed?: boolean\n}\n\n/** Input for the document-front submission. */\nexport interface DocumentFrontInput {\n image?: Blob | null\n country?: string | null\n documentType?: DocumentType | null\n signals?: ProviderSignalHints\n}\n\n/** Input for the document-back submission. */\nexport interface DocumentBackInput {\n image?: Blob | null\n country?: string | null\n documentType?: DocumentType | null\n}\n\n/** Input for the liveness submission (passive selfie or active challenge video). */\nexport interface LivenessInput {\n selfie?: Blob | null\n /** Recorded challenge video (active mode). */\n video?: Blob | null\n /** Passive (selfie) or active (challenge video). Defaults to passive. */\n mode?: LivenessMode\n /** The challenge sequence the user performed, in order (active mode). */\n challenges?: LivenessChallenge[]\n signals?: ProviderSignalHints\n}\n\n/** Input for completing the session (runs face-match + decision). */\nexport interface CompleteInput {\n signals?: ProviderSignalHints\n}\n\n/**\n * Raised when the Client API returns a non-2xx response.\n */\nexport class WidgetApiError extends Error {\n readonly status: number\n readonly code?: string\n\n constructor(message: string, status: number, code?: string) {\n super(message)\n this.name = 'WidgetApiError'\n this.status = status\n this.code = code\n }\n}\n\n/** Options for {@link ArkycClient}. */\nexport interface ArkycClientOptions {\n /** Short-lived client token minted for this session. */\n token: string\n /** API origin (defaults to the same origin the widget is served from). */\n baseUrl?: string\n /** Injectable for testing; defaults to the global `fetch`. */\n fetch?: typeof fetch\n}\n\ntype EnvelopeNumber = number | boolean\n\nfunction appendSignals(form: FormData, signals?: ProviderSignalHints): void {\n if (!signals) return\n for (const [key, value] of Object.entries(signals)) {\n if (value == null) continue\n form.append(key, String(value as EnvelopeNumber))\n }\n}\n\n/**\n * A thin, framework-agnostic client for the Arkyc Client/Widget API. Drives a\n * single verification session with a short-lived client token. All responses\n * are unwrapped from the standard `{ status, message, data }` envelope.\n */\nexport class ArkycClient {\n private readonly token: string\n private readonly baseUrl: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: ArkycClientOptions) {\n if (!options.token) throw new Error('ArkycClient requires a client `token`.')\n this.token = options.token\n this.baseUrl = (options.baseUrl ?? '').replace(/\\/$/, '')\n const f = options.fetch ?? globalThis.fetch\n if (!f) throw new Error('No `fetch` implementation available.')\n this.fetchImpl = f.bind(globalThis)\n }\n\n /**\n * Load the current session (marks it `started` on first call).\n *\n * @returns\n */\n getSession(): Promise<ClientSession> {\n return this.request('GET', '/v1/client/session')\n }\n\n /**\n * Submit the document front image (triggers OCR + portrait extraction).\n *\n * @param input\n * @returns\n */\n submitDocumentFront(input: DocumentFrontInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.image) form.append('image', input.image, 'document-front.jpg')\n if (input.country) form.append('country', input.country)\n if (input.documentType) form.append('document_type', input.documentType)\n appendSignals(form, input.signals)\n return this.request('POST', '/v1/client/document/front', form)\n }\n\n /**\n * Submit the document back image.\n *\n * @param input\n * @returns\n */\n submitDocumentBack(input: DocumentBackInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.image) form.append('image', input.image, 'document-back.jpg')\n if (input.country) form.append('country', input.country)\n if (input.documentType) form.append('document_type', input.documentType)\n return this.request('POST', '/v1/client/document/back', form)\n }\n\n /**\n * Submit the selfie for the liveness check.\n *\n * @param input\n * @returns\n */\n submitLiveness(input: LivenessInput): Promise<ClientSession> {\n const form = new FormData()\n if (input.selfie) form.append('selfie', input.selfie, 'selfie.jpg')\n if (input.video) form.append('video', input.video, 'liveness.webm')\n if (input.mode) form.append('mode', input.mode)\n if (input.challenges) form.append('challenges', JSON.stringify(input.challenges))\n appendSignals(form, input.signals)\n return this.request('POST', '/v1/client/liveness', form)\n }\n\n /**\n * Finalise the session (runs face-match + the decision engine).\n *\n * @param input\n * @returns\n */\n complete(input: CompleteInput = {}): Promise<ClientSession> {\n return this.request('POST', '/v1/client/complete', input.signals ?? {})\n }\n\n private async request(method: string, path: string, body?: FormData | object): Promise<ClientSession> {\n const headers: Record<string, string> = { 'X-Client-Token': this.token }\n let payload: BodyInit | undefined\n\n if (body instanceof FormData) {\n payload = body\n } else if (body) {\n headers['Content-Type'] = 'application/json'\n payload = JSON.stringify(body)\n }\n\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, { method, headers, body: payload })\n const text = await res.text()\n const json = text ? (JSON.parse(text) as { message?: string; data?: ClientSession }) : {}\n\n if (!res.ok) {\n throw new WidgetApiError(json.message ?? `Request failed (${res.status})`, res.status)\n }\n return json.data as ClientSession\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { WidgetControllerConfig, WidgetEventListener } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/controller.d.ts
|
|
4
|
+
declare class WidgetController {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly client;
|
|
7
|
+
private readonly view;
|
|
8
|
+
private readonly win;
|
|
9
|
+
private readonly nav;
|
|
10
|
+
private readonly postToParent;
|
|
11
|
+
private readonly scheduler;
|
|
12
|
+
private readonly transientMs;
|
|
13
|
+
private readonly pollMs;
|
|
14
|
+
private readonly maxPolls;
|
|
15
|
+
private readonly maxHandoffPolls;
|
|
16
|
+
/** Cross-device handoff: project config + whether the offer/QR is showing. */
|
|
17
|
+
private handoffConfig;
|
|
18
|
+
private handoffReady;
|
|
19
|
+
private handoffActive;
|
|
20
|
+
/** This session's id + how to watch it live (push transport vs. polling). */
|
|
21
|
+
private sessionId;
|
|
22
|
+
private realtimeConfig;
|
|
23
|
+
private rtClient;
|
|
24
|
+
private rtResolved;
|
|
25
|
+
/** The last status observed, so a transition only emits once per change. */
|
|
26
|
+
private lastStatus;
|
|
27
|
+
/** Per-name event listeners registered via {@link on}. */
|
|
28
|
+
private listeners;
|
|
29
|
+
/** Cancels the active session watch (e.g. continue-on-this-device). */
|
|
30
|
+
private stopWatch;
|
|
31
|
+
private step;
|
|
32
|
+
private documentType;
|
|
33
|
+
private country;
|
|
34
|
+
private selfie;
|
|
35
|
+
private livenessMode;
|
|
36
|
+
/** The session's capture model; `active` mandates a live camera (no fallback). */
|
|
37
|
+
private captureModel;
|
|
38
|
+
private livenessChallenges;
|
|
39
|
+
/** The applied workflow (orders/toggles stages, skip_ocr), or null for the default flow. */
|
|
40
|
+
private workflow;
|
|
41
|
+
private result;
|
|
42
|
+
/** The session was already terminal when the widget loaded (show a close-only notice). */
|
|
43
|
+
private terminalOnLoad;
|
|
44
|
+
/** Whether the current step's instruction interstitial has been acknowledged. */
|
|
45
|
+
private instructionAcked;
|
|
46
|
+
private pendingError;
|
|
47
|
+
private settled;
|
|
48
|
+
constructor(config: WidgetControllerConfig);
|
|
49
|
+
/** The widget's root element — append it to an overlay or inline container. */
|
|
50
|
+
get element(): HTMLElement;
|
|
51
|
+
/** Connect to the session, then route to the result, the QR, or the welcome flow. */
|
|
52
|
+
start(): void;
|
|
53
|
+
/** Tear down the view and release the camera (does not fire callbacks). */
|
|
54
|
+
destroy(): void;
|
|
55
|
+
/** Close the widget as if the user dismissed it (fires `onClose`/`onSettle`). */
|
|
56
|
+
close(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Subscribe to a named widget event; returns an unsubscribe function. Having a
|
|
59
|
+
* listener (here or via `onEvent`) is what activates the event stream.
|
|
60
|
+
*/
|
|
61
|
+
on(event: string, listener: WidgetEventListener): () => void;
|
|
62
|
+
/** Whether anyone is listening for `name` (the firehose or a named listener). */
|
|
63
|
+
private hasListener;
|
|
64
|
+
/**
|
|
65
|
+
* Surface an event: to local listeners (firehose + `on`), and — in hosted
|
|
66
|
+
* iframe mode — to the embedding parent as `arkyc:event` so the SDK's
|
|
67
|
+
* `handle.on(...)` works across the frame.
|
|
68
|
+
*/
|
|
69
|
+
private dispatch;
|
|
70
|
+
/** Emit an event to the firehose + named listeners — but only if one is active. */
|
|
71
|
+
private emit;
|
|
72
|
+
/** Record an observed status, emitting `session.transition` on a real change. */
|
|
73
|
+
private observe;
|
|
74
|
+
/**
|
|
75
|
+
* Lazily connect the push transport for this session (once). Returns null for
|
|
76
|
+
* `polling`/`off`/`memory` or when the transport SDK can't load — the caller
|
|
77
|
+
* then polls instead.
|
|
78
|
+
*/
|
|
79
|
+
private resolveRealtime;
|
|
80
|
+
private handlers;
|
|
81
|
+
/**
|
|
82
|
+
* Fetch the session and route: an already-terminal session (e.g. a stale handoff
|
|
83
|
+
* link, or one finished on another device) shows a close-only notice; on a
|
|
84
|
+
* desktop with handoff enabled we lead with the QR; otherwise the welcome flow.
|
|
85
|
+
*/
|
|
86
|
+
private bootstrap;
|
|
87
|
+
/** The hosted handoff page URL: a consumer override, else the server-provided one. */
|
|
88
|
+
private handoffTarget;
|
|
89
|
+
/** Render the QR for this session and wait for the other device to finish. */
|
|
90
|
+
private startHandoff;
|
|
91
|
+
/** Build the hosted-page URL the QR encodes (carries the session token). */
|
|
92
|
+
private buildHandoffUrl;
|
|
93
|
+
/** Watch the session while the user verifies on the other device; mirror the result. */
|
|
94
|
+
private pollHandoff;
|
|
95
|
+
/**
|
|
96
|
+
* Await a terminal status over the push transport. Resolves to the terminal
|
|
97
|
+
* status, or null if `active()` went false or the tick budget elapsed (push can
|
|
98
|
+
* miss events / disconnect; the budget bounds the wait). An initial fetch
|
|
99
|
+
* catches a session that finished before we subscribed.
|
|
100
|
+
*/
|
|
101
|
+
private pushWatch;
|
|
102
|
+
/** Resolve which liveness flow to run from the session's capture model. */
|
|
103
|
+
private resolveLiveness;
|
|
104
|
+
private onImage;
|
|
105
|
+
/** Enter a step: render it, and drive any automatic (processing) work. */
|
|
106
|
+
private enter;
|
|
107
|
+
/** Watch the session until it reaches a terminal status (then show the result). */
|
|
108
|
+
private poll;
|
|
109
|
+
private showResult;
|
|
110
|
+
/** A session that was already terminal on load: a notice with only a Close button. */
|
|
111
|
+
private showTerminal;
|
|
112
|
+
private render;
|
|
113
|
+
private next;
|
|
114
|
+
private delay;
|
|
115
|
+
/** Run an async step, routing any failure to the error screen. */
|
|
116
|
+
private run;
|
|
117
|
+
private fail;
|
|
118
|
+
private finishResult;
|
|
119
|
+
private finishClose;
|
|
120
|
+
/** Stop any active session watch + disconnect the push transport. */
|
|
121
|
+
private teardownRealtime;
|
|
122
|
+
private post;
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
export { WidgetController };
|
|
126
|
+
//# sourceMappingURL=controller.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.mts","names":[],"sources":["../src/controller.ts"],"mappings":";;;cAoEa,gBAAA;EAAA,iBA+CkB,MAAA;EAAA,iBA9CZ,MAAA;EAAA,iBACA,IAAA;EAAA,iBACA,GAAA;EAAA,iBACA,GAAA;EAAA,iBACA,YAAA;EAAA,iBACA,SAAA;EAAA,iBACA,WAAA;EAAA,iBACA,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,eAAA;EARA;EAAA,QAWT,aAAA;EAAA,QACA,YAAA;EAAA,QACA,aAAA;EATS;EAAA,QAYT,SAAA;EAAA,QACA,cAAA;EAAA,QACA,QAAA;EAAA,QACA,UAAA;EARA;EAAA,QAUA,UAAA;EARA;EAAA,QAUA,SAAA;EANA;EAAA,QAQA,SAAA;EAAA,QAEA,IAAA;EAAA,QACA,YAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,QACA,YAAA;EAHA;EAAA,QAKA,YAAA;EAAA,QACA,kBAAA;EAHA;EAAA,QAKA,QAAA;EAAA,QACA,MAAA;EADA;EAAA,QAGA,cAAA;EAAA;EAAA,QAEA,gBAAA;EAAA,QACA,YAAA;EAAA,QACA,OAAA;cAEqB,MAAA,EAAQ,sBAAA;EAAA;EAAA,IAkCjC,OAAA,IAAW,WAAA;EAAX;EAKJ,KAAA;EAAA;EAMA,OAAA;EAKA;EAAA,KAAA;EAQG;;;;EAAH,EAAA,CAAG,KAAA,UAAe,QAAA,EAAU,mBAAA;EA6BpB;EAAA,QApBA,WAAA;EAwDM;;;;;EAAA,QA5CN,QAAA;EA2JM;EAAA,QAnJN,IAAA;EAwOA;EAAA,QApNA,OAAA;EAwPM;;;;;EAAA,QAxOA,eAAA;EAAA,QAoBN,QAAA;EAoTM;;;;;EAAA,QA9QA,SAAA;EA0UF;EAAA,QA/SJ,aAAA;;UAKM,YAAA;;UAUN,eAAA;;UAWM,WAAA;;;;;;;UAyCN,SAAA;;UA4CA,eAAA;EAAA,QAWM,OAAA;;UAyBA,KAAA;;UAkCA,IAAA;EAAA,QAkBN,UAAA;;UAOA,YAAA;EAAA,QAOA,MAAA;EAAA,QAiBA,IAAA;EAAA,QAQA,KAAA;;UAKM,GAAA;EAAA,QAQN,IAAA;EAAA,QAcA,YAAA;EAAA,QAmBA,WAAA;;UAaA,gBAAA;EAAA,QAMA,IAAA;AAAA"}
|