@arkyc/widget 2.3.0 → 2.3.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"capture.d.mts","names":[],"sources":["../src/capture.ts"],"mappings":";;;;;;;;YAQY;qBAEC;mBAIQ;mBACA;UAJX;EAGW,YAAA,MAAK,UACL,MAAK;;MAIpB;;;;;;;;EAWE,MAAM,OAAO,kBAAkB,QAAQ,SAAS,QAAQ;;;;EAgB9D;;MAMI;;;;;;;;;EAYJ,YAAY,QAAQ;IAAgB,QAAQ,QAAQ;;;;;;;;;;EAyBpD,cAAc,OAAO;IAAqB;IAAoB;IAAkB;;;;;;;;;EA6BhF,UAAU,OAAO,kBAAkB,mBAAiB,QAAQ;;;;;;;SAuBrD,cAAc,OAAO,mBAAmB"}
1
+ {"version":3,"file":"capture.d.mts","names":[],"sources":["../src/capture.ts"],"mappings":";;;;;;;;YAQY;qBAoBC;mBAIQ;mBACA;UAJX;EAGW,YAAA,MAAK,UACL,MAAK;;MAIpB;;;;;;;;EAWE,MAAM,OAAO,kBAAkB,QAAQ,SAAS,QAAQ;;;;EAgB9D;;MAMI;;;;;;;;;EAYJ,YAAY,QAAQ;IAAgB,QAAQ,QAAQ;;;;;;;;;;EAyBpD,cAAc,OAAO;IAAqB;IAAoB;IAAkB;;;;;;;;;EA6BhF,UAAU,OAAO,kBAAkB,mBAAiB,QAAQ;;;;;;;SAuBrD,cAAc,OAAO,mBAAmB"}
package/dist/capture.mjs CHANGED
@@ -1,4 +1,19 @@
1
1
  //#region src/capture.ts
2
+ /**
3
+ * Video constraints per camera. With no resolution requested, browsers open the
4
+ * camera at 640×480, which leaves a card's names and numbers a few pixels tall,
5
+ * too small for OCR to read. Documents ask for 1080p (`ideal`, so a camera that
6
+ * can't deliver it still opens at its best). Selfies keep the default: the face
7
+ * fills the frame, and a larger stream would only inflate the liveness video.
8
+ */
9
+ const VIDEO_CONSTRAINTS = {
10
+ environment: {
11
+ facingMode: "environment",
12
+ width: { ideal: 1920 },
13
+ height: { ideal: 1080 }
14
+ },
15
+ user: { facingMode: "user" }
16
+ };
2
17
  var Camera = class {
3
18
  doc;
4
19
  nav;
@@ -20,7 +35,7 @@ var Camera = class {
20
35
  */
21
36
  async start(video, facing) {
22
37
  const stream = await this.nav.mediaDevices.getUserMedia({
23
- video: { facingMode: facing },
38
+ video: VIDEO_CONSTRAINTS[facing],
24
39
  audio: false
25
40
  });
26
41
  this.stream = stream;
@@ -121,6 +136,6 @@ var Camera = class {
121
136
  }
122
137
  };
123
138
  //#endregion
124
- export { Camera };
139
+ export { Camera, VIDEO_CONSTRAINTS };
125
140
 
126
141
  //# sourceMappingURL=capture.mjs.map
@@ -1 +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"}
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\n/**\n * Video constraints per camera. With no resolution requested, browsers open the\n * camera at 640×480, which leaves a card's names and numbers a few pixels tall,\n * too small for OCR to read. Documents ask for 1080p (`ideal`, so a camera that\n * can't deliver it still opens at its best). Selfies keep the default: the face\n * fills the frame, and a larger stream would only inflate the liveness video.\n */\nexport const VIDEO_CONSTRAINTS: Record<Facing, MediaTrackConstraints> = {\n environment: {\n facingMode: 'environment',\n width: { ideal: 1920 },\n height: { ideal: 1080 },\n },\n user: {\n facingMode: 'user',\n },\n}\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: VIDEO_CONSTRAINTS[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":";;;;;;;;AAiBA,MAAa,oBAA2D;CACtE,aAAa;EACX,YAAY;EACZ,OAAO,EAAE,OAAO,KAAK;EACrB,QAAQ,EAAE,OAAO,KAAK;CACxB;CACA,MAAM,EACJ,YAAY,OACd;AACF;AAEA,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,kBAAkB;GACzB,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkyc/widget",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Embeddable, framework-agnostic Arkyc identity-verification widget",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "@arkstack/realtime": "^0.19.4",
43
43
  "qrcode-generator": "^1.4.4",
44
- "@arkyc/types": "^2.3.0"
44
+ "@arkyc/types": "^2.3.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "firebase": "^11.0.0",