@seams/voiceid 0.53.0 → 0.55.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.
@@ -1 +1 @@
1
- import{s as e}from"./prompt-bUX1mWKl.js";import{c as t,r as n,s as r}from"./deviceBridge-Cqb5arWN.js";async function i(e,n,i,o){let s=await e.prepareUnlock(n),c=s.scope;if(!await r(c))throw Error(`Unlock with a passkey, then open Manage voice to import or record your enrollment.`);await t(i,n,o),n.throwIfAborted();let l=await s.challenge(n),u={challenge:l,initiation:e.mountCapture?`hosted_menu`:`sdk_request`};return n.throwIfAborted(),{kind:`recording`,expiresAtMs:l.expiresAtMs,action:a.bind(null,o,u)}}async function a(t,r,i,a,s){switch(r.initiation){case`sdk_request`:e();break;case`hosted_menu`:break;default:return o(r.initiation)}return r.challenge.complete(n.bind(null,t,{onCapture:a,phrase:s}),i)}function o(e){throw Error(`Unexpected VoiceID initiation: ${String(e)}`)}export{i as prepareUnlockSurface};
1
+ import{s as e}from"./prompt-BPtSdsWi.js";import{c as t,r as n,s as r}from"./deviceBridge-D8R6eVEE.js";async function i(e,n,i,o){let s=await e.prepareUnlock(n),c=s.scope;if(!await r(c))throw Error(`Unlock with a passkey, then open Manage voice to import or record your enrollment.`);await t(i,n,o),n.throwIfAborted();let l=await s.challenge(n),u={challenge:l,initiation:e.mountCapture?`hosted_menu`:`sdk_request`};return n.throwIfAborted(),{kind:`recording`,expiresAtMs:l.expiresAtMs,action:a.bind(null,o,u)}}async function a(t,r,i,a,s){switch(r.initiation){case`sdk_request`:e();break;case`hosted_menu`:break;default:return o(r.initiation)}return r.challenge.complete(n.bind(null,t,{onCapture:a,phrase:s}),i)}function o(e){throw Error(`Unexpected VoiceID initiation: ${String(e)}`)}export{i as prepareUnlockSurface};
@@ -0,0 +1,9 @@
1
+ import { type AuthMethodPhraseRecording, type WalletAuthMethodHostPlugin } from '@seams/wallet/voiceid-host';
2
+ type PhraseInput = Parameters<WalletAuthMethodHostPlugin['recordPhrase']>[0];
3
+ /**
4
+ * Shows the given sentence in the VoiceID prompt and records one clip with the quality
5
+ * gate. A too quiet or interrupted recording stays on the surface for another try.
6
+ */
7
+ export declare function recordPhrase(input: PhraseInput): Promise<AuthMethodPhraseRecording>;
8
+ export {};
9
+ //# sourceMappingURL=phrase.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { WalletAuthMethodHostPlugin } from '@seams/wallet/voiceid-host';
2
2
  import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
3
- export declare const hostApiVersion = 8;
3
+ export declare const hostApiVersion = 9;
4
4
  export declare function createVoiceIdHostPlugin(model: VoiceIdTranscriptionModel): WalletAuthMethodHostPlugin;
5
5
  //# sourceMappingURL=host.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seams/voiceid",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "type": "module",
5
5
  "description": "Optional browser VoiceID authentication for Seams Wallet",
6
6
  "license": "Apache-2.0",
@@ -50,7 +50,7 @@
50
50
  "voice-glow": "0.2.1"
51
51
  },
52
52
  "peerDependencies": {
53
- "@seams/wallet": "0.53.0",
53
+ "@seams/wallet": "0.55.0",
54
54
  "react": ">=19.0.0",
55
55
  "react-dom": ">=19.0.0"
56
56
  },
@@ -1 +0,0 @@
1
- import{_ as e,a as t,c as n,d as r,f as i,g as a,h as o,i as s,m as c,o as ee,p as l,s as u,t as d,u as f,v as p}from"./prompt-bUX1mWKl.js";import{WorkerDeferred as m,assertVoiceOwner as h,base64UrlEncode as g,createBrowserVoiceDevice as _,isFiniteNumber as v,openBrowserVoiceFactor as y,parseVoiceIdDeviceAssertion as te,parseVoiceIdOperationAssertion as b,prepareVoiceOwner as x,readBrowserVoiceDevice as S,requireRecord as C,resolveActivatingVoiceEnrollmentScope as w,resolveVoiceEnrollmentScope as T,toError as E,voiceEnrollmentStorage as D,voiceIdOperationStatementBytes as O,voiceIdWalletSessionChallengeBytes as k}from"../voiceid-host.js";const A=`e04dd7a6a1e23fc7b89e47682ea9d0a3946cf7bc59d07922c3db3efc52ae4269`,j=`redimnet-b1:${A}`,M=new o({path:`/sdk/voiceid/redimnet-b1.onnx`,sha256:A,bytes:8859593,label:`Speaker verification`});async function N(e){let t=await M.prepare(),n=new(await(import(`./ort.wasm.bundle.min-7y-hlRN8.js`))).Tensor(`float32`,e,[1,e.length]);try{let e=await t.run({samples:n});try{let t=e.embedding;if(!t||!(t.data instanceof Float32Array))throw Error(`VoiceID model returned an invalid embedding.`);return P(Array.from(t.data))}finally{for(let t of Object.values(e))t.dispose()}}finally{n.dispose()}}function P(e){if(e.length!==192)throw Error(`VoiceID template dimensions are invalid.`);let t=0;for(let n of e){if(!Number.isFinite(n))throw Error(`VoiceID template is invalid.`);t+=n*n}if(t<1e-12)throw Error(`VoiceID template contains no signal.`);let n=Math.sqrt(t),r=[];for(let t of e)r.push(t/n);return r}function F(e,t){if(e.length!==192||t.length!==192)throw Error(`VoiceID templates are incompatible.`);let n=0;for(let r=0;r<e.length;r+=1)n+=e[r]*t[r];return n}function I(e){return e.asset}function L(e,t){return e+t.size}var R=class{element=document.createElement(`section`);status=document.createElement(`p`);meter=document.createElement(`progress`);retryButton=document.createElement(`button`);retry=null;cancelled=new m;reveal;received=new Map;models;assets;total;constructor(t,n,r){this.parent=t,this.signal=n,this.models=[M,l(r),f],this.assets=[e,...this.models.map(I)],this.total=this.assets.reduce(L,0),this.element.className=`seams-voiceid-model-download`,this.status.setAttribute(`role`,`status`),this.status.setAttribute(`aria-live`,`polite`),this.meter.setAttribute(`aria-label`,`VoiceID download`),this.meter.style.width=`100%`,this.retryButton.type=`button`,this.retryButton.className=`seams-voiceid-primary`,this.retryButton.textContent=`Retry loading`,this.retryButton.hidden=!0,this.retryButton.addEventListener(`click`,this.retryLoading),this.element.append(this.status,this.meter,this.retryButton),this.status.textContent=`Loading saved VoiceID…`,this.reveal=setTimeout(t.append.bind(t,this.element),300),n.addEventListener(`abort`,this.abort,{once:!0})}settle=()=>{clearTimeout(this.reveal),this.signal.removeEventListener(`abort`,this.abort),this.retryButton.removeEventListener(`click`,this.retryLoading)};abort=()=>{let e=new DOMException(`VoiceID preparation was cancelled.`,`AbortError`);this.cancelled.reject(e),this.retry?.reject(e)};retryLoading=()=>{this.retry?.resolve(),this.retry=null};async prepare(){this.signal.throwIfAborted(),await Promise.race([this.prepareUntilReady(),this.cancelled.promise])}async prepareUntilReady(){for(;;){this.signal.throwIfAborted(),this.retryButton.hidden=!0,this.meter.hidden=!1;try{for(let e of this.assets)await e.download(this.update.bind(this,e)),this.signal.throwIfAborted();this.preparing();for(let e of this.models)await e.prepare(),this.signal.throwIfAborted();this.ready();return}catch(e){if(this.signal.throwIfAborted(),!(e instanceof a))throw e;clearTimeout(this.reveal),this.parent.append(this.element),this.element.dataset.state=`failed`,this.meter.hidden=!0,this.status.textContent=e.message,this.retry=new m,this.retryButton.hidden=!1,this.retryButton.focus(),await this.retry.promise}}}update=(e,t)=>{switch(t.kind){case`checking_cache`:return;case`downloading`:this.received.set(e,t.received),this.element.dataset.state=`downloading`;break;case`ready`:this.received.set(e,e.size);break;default:}if(this.element.dataset.state!==`downloading`)return;let n=0;for(let e of this.received.values())n+=e;this.meter.max=this.total,this.meter.value=n,this.status.textContent=`Downloading VoiceID: ${(n/1e6).toFixed(1)} of ${(this.total/1e6).toFixed(1)} MB · ${Math.floor(n/this.total*100)}%`};preparing(){this.element.dataset.state=`preparing`,this.meter.removeAttribute(`value`),this.status.textContent=`Preparing VoiceID…`}ready(){this.element.dataset.state=`ready`,this.meter.hidden=!0,this.status.textContent=`VoiceID is ready.`}};async function z(e,t,n){let r=new R(e,t,n);try{await r.prepare()}finally{r.settle()}}const B=.2,V=D({model:j,parseEnrollment:U});function H(e){if(!Array.isArray(e)||!e.every(v))throw Error(`VoiceID template is invalid.`);let t=P(e);for(let n=0;n<e.length;n+=1)if(Math.abs(e[n]-t[n])>1e-6)throw Error(`VoiceID template is not normalized.`);return e.slice()}function U(e,t){if(e===void 0)return null;let n=C(e,`VoiceID enrollment`),r=C(n.scope,`VoiceID enrollment scope`);if(n.kind!==`browser_voiceid_enrollment_v2`||r.walletId!==t.walletId||r.authorityId!==t.authorityId||r.walletAuthMethodId!==t.walletAuthMethodId||r.deviceKeyId!==t.deviceKeyId||typeof n.generation!=`string`||!n.generation||typeof n.revision!=`number`||!Number.isSafeInteger(n.revision)||n.revision<0)throw Error(`VoiceID enrollment identity is invalid.`);if(n.model!==j)return null;let i=H(n.baseline),a={kind:`baseline`};try{let e=C(n.adaptation,`VoiceID improvements`);if(e.kind===`adapted`&&Array.isArray(e.samples)&&e.samples.length===3){let t=e.samples.map(H);G(i,t),a={kind:`adapted`,samples:t}}}catch{a={kind:`baseline`}}return{kind:`browser_voiceid_enrollment_v2`,scope:t,model:j,generation:n.generation,revision:n.revision,baseline:i,adaptation:a}}function W(e,t){let n=Array(192).fill(0);for(let e of t)for(let r=0;r<n.length;r+=1)n[r]+=e[r]/t.length;let r=P(n);for(let t=0;t<n.length;t+=1)n[t]=(1-B)*e[t]+B*r[t];return P(n)}function G(e,t){if(t.length!==3)throw Error(`Record three samples before saving voice improvements.`);for(let n of t)if(F(e,n)<.45)throw Error(`This sample differs from your original voice. Try again or re-record your enrollment.`);if(F(e,W(e,t))<.98)throw Error(`These samples differ too much from your original voice. Re-record your enrollment instead.`)}function K(e){return V.read(e)}async function q(e,t){let n=await V.readTemplate(e,t),r=n.adaptation;return{template:r.kind===`adapted`?W(n.baseline,r.samples):[...n.baseline],baseline:[...n.baseline],kind:r.kind}}async function J(){let e=await V.readImportable();if(e===void 0)return null;let t=C(e,`Previous VoiceID enrollment`);return t.kind!==`browser_voiceid_enrollment_v1`||t.model!==j?null:H(t.template)}async function Y(e){let{scope:t,signal:n}=e;switch(e.kind){case`activate`:return V.commit({kind:`activate`,scope:t,signal:n,guard:e.guard,snapshot:e.snapshot,baseline:H(e.baseline)});case`import`:return V.commit({kind:`import`,scope:t,signal:n,guard:e.guard,snapshot:e.snapshot,confirmation:e.confirmation,baseline:H(e.baseline)});case`replace`:return V.commit({kind:`replace`,scope:t,signal:n,guard:e.guard,snapshot:e.snapshot,confirmation:e.confirmation,baseline:H(e.baseline)});case`improve`:return G(e.snapshot.enrollment.baseline,e.samples),V.commit({kind:`improve`,scope:t,signal:n,guard:e.guard,snapshot:e.snapshot,confirmation:e.confirmation,samples:e.samples});case`reset`:return V.commit({kind:`reset`,scope:t,signal:n,guard:e.guard,snapshot:e.snapshot,confirmation:e.confirmation});default:return X(e)}}function X(e){throw Error(`Unsupported VoiceID enrollment change.`)}async function Z(e,t,n,r){n.throwIfAborted();let i=new ne(e,t,n,r);try{await Promise.race([i.prepare(),i.result]),await i.result}finally{i.dispose()}}var ne=class{completion=new m;result=this.completion.promise;status;prompt;phrase=c(null);record;cancel;samples=[];phase=`loading`;get closed(){return this.phase===`closed`}controller=new AbortController;guardTimer;checkingGuard=!1;constructor(e,t,n,r){this.plan=t,this.signal=n,this.model=r,this.prompt=new d(e.ownerDocument,`Enroll your voice in this browser`,this.phrase),this.prompt.title.textContent=`Read aloud · 0 of 3 accepted`,this.status=this.prompt.status,this.record=this.prompt.record,this.cancel=this.prompt.cancel,this.record.textContent=`Record sample 1`,this.record.addEventListener(`click`,this.capture),this.cancel.setAttribute(`aria-label`,`Cancel enrollment`),this.cancel.addEventListener(`click`,this.cancelled),this.prompt.ready(t.kind===`improve`?`Read naturally. Your original enrollment stays unchanged.`:`Read the full phrase at your normal pace.`),this.record.disabled=!0,e.replaceChildren(this.prompt.element),n.addEventListener(`abort`,this.cancelled,{once:!0}),this.guardTimer=setInterval(this.checkGuard,250),n.aborted?this.cancelled():this.cancel.focus()}async prepare(){await z(this.prompt.progress,this.controller.signal,this.model),this.phase===`loading`&&(this.prompt.progress.replaceChildren(),this.phase=`ready`,this.record.disabled=!1,this.record.focus())}capture=()=>{this.collect()};async collect(){if(this.phase!==`ready`)return;let e=this.samples.length+1;try{u(),this.phase=`recording`,this.record.disabled=!0,this.status.textContent=`Opening microphone…`;let e=await t(this.controller.signal,this.prompt.capture);if(this.signal.aborted||this.closed){e.kind===`captured`&&e.samples.fill(0);return}if(e.kind===`insufficient_audio`){this.status.textContent=n(e.quality);return}if(this.plan.kind===`improve`&&e.quality.noise!==`normal`){e.samples.fill(0),this.status.textContent=`Use a quiet place to improve your voice enrollment.`;return}let a;try{if(await h(this.plan.scope,this.plan.guard),this.status.textContent=`Checking your voice…`,(await i(e.samples,this.phrase,this.model)).kind===`mismatch`)throw new r;a=await N(e.samples)}finally{e.samples.fill(0)}if(this.signal.aborted||this.closed){a.fill(0);return}if(this.plan.kind===`improve`&&F(this.plan.snapshot.enrollment.baseline,a)<.45)throw a.fill(0),Error(`Your voice did not match the original enrollment. To replace it, return to Manage voice and choose Record voice again.`);for(let e of this.samples)if(F(e,a)<.45)throw a.fill(0),Error(`This sample differs from the earlier samples. Use the same microphone and try again.`);if(this.samples.push(a),this.samples.length<3){this.prompt.accepted(),this.status.textContent=`Sample ${this.samples.length} accepted. Record the next sample.`,this.record.textContent=`Record sample ${this.samples.length+1}`;return}let o=Array(192).fill(0);for(let e of this.samples)for(let t=0;t<o.length;t+=1)o[t]+=e[t]/3;this.phase=`committing`,this.prompt.title.textContent=`3 of 3 accepted`,this.status.textContent=`Saving voice enrollment…`;let s=this.plan;switch(s.kind){case`activate`:await Y({kind:`activate`,scope:s.scope,guard:s.guard,snapshot:s.snapshot,baseline:P(o),signal:this.controller.signal});break;case`replace`:await Y({kind:`replace`,scope:s.scope,guard:s.guard,snapshot:s.snapshot,baseline:P(o),confirmation:s.confirmation,signal:this.controller.signal});break;case`improve`:await Y({kind:`improve`,scope:s.scope,guard:s.guard,snapshot:s.snapshot,samples:this.samples,confirmation:s.confirmation,signal:this.controller.signal});break;default:return re(s)}if(this.signal.aborted||this.closed)return;this.phase=`closed`,this.prompt.accepted(),this.status.textContent=`Voice enrollment saved.`,this.completion.resolve()}catch(e){if(!this.closed&&this.phase===`committing`){this.phase=`closed`,this.controller.abort(),this.completion.reject(E(e));return}!this.closed&&this.samples.length===3&&this.samples.pop()?.fill(0),this.closed||(this.status.textContent=e instanceof Error?e.message:`VoiceID enrollment failed.`)}finally{this.closed||(this.samples.length<e&&(this.record.textContent=`Retry sample ${e}`,this.status.textContent=`Sample ${e} was not accepted. ${this.status.textContent??``}`),this.phrase=c(this.phrase),this.prompt.setPhrase(this.phrase),this.prompt.title.textContent=`Read aloud · ${this.samples.length} of 3 accepted`,this.prompt.ready(this.status.textContent??``),this.phase=`ready`,this.record.disabled=!1)}}cancelled=()=>{this.closed||(this.phase=`closed`,this.completion.reject(Error(`VoiceID enrollment was cancelled.`)),this.controller.abort())};checkGuard=()=>{this.closed||this.checkingGuard||(this.checkingGuard=!0,this.validateGuard())};async validateGuard(){try{await h(this.plan.scope,this.plan.guard)}catch(e){this.closed||(this.phase=`closed`,this.controller.abort(),this.completion.reject(E(e)))}finally{this.checkingGuard=!1}}dispose(){this.phase=`closed`,this.controller.abort(),clearInterval(this.guardTimer),this.signal.removeEventListener(`abort`,this.cancelled),this.prompt.dispose(),this.record.removeEventListener(`click`,this.capture),this.cancel.removeEventListener(`click`,this.cancelled);for(let e of this.samples)e.fill(0)}};function re(e){throw Error(`Unsupported VoiceID enrollment capture.`)}async function Q(e,n,r,a,o){let s=await t(n,r);if(s.kind===`insufficient_audio`)return n.throwIfAborted(),s;try{n.throwIfAborted();let t=await i(s.samples,a,o),r=await N(s.samples),c;try{n.throwIfAborted();let t=await q(e,n);try{c=F(r,t.template)}finally{t.template.fill(0),t.baseline.fill(0)}}finally{r.fill(0)}return c<.45?{kind:`mismatch`,quality:s.quality}:t.kind===`matched`?{kind:`matched`}:{kind:`phrase_mismatch`}}finally{s.samples.fill(0)}}async function ie(e,t,n){u();let r=await _(n.walletId,n.walletAuthMethodId),i=await w(r);if(await K(i))await $({kind:`activation`,scope:i,owner:await x(i,`activating`)},n.signal,t.onCapture,t.phrase,e);else{let r=await x(i,`activating`);await Z(t.parent,{kind:`activate`,scope:i,guard:r,snapshot:{kind:`missing`}},n.signal,e)}n.signal.throwIfAborted();let a=await y(r);if(n.signal.aborted)throw a.fill(0),Error(`VoiceID activation was cancelled.`);return{kind:`activated`,publicKeyB64u:r.publicKeyB64u,deviceKeyId:r.deviceKeyId,factorSecret32:a}}async function ae(e,t,n){ee();let{challenge:r}=n;if(r.requestOrigin!==location.origin||r.expiresAtMs<=Date.now())throw Error(`VoiceID challenge is expired or belongs to another Wallet origin.`);let i=await S(r.walletId,r.walletAuthMethodId);if(!i||i.deviceKeyId!==r.deviceKeyId)throw Error(`VoiceID device key is unavailable in this browser.`);if(await $({kind:`active`,scope:await T(r.walletId,r.walletAuthMethodId)},n.signal,t.onCapture,t.phrase,e),n.signal.throwIfAborted(),r.expiresAtMs<=Date.now())throw Error(`VoiceID challenge expired. Start unlock again.`);let a=await crypto.subtle.sign(`Ed25519`,i.privateKey,k(r)),o=await y(i);if(n.signal.aborted||r.expiresAtMs<=Date.now())throw o.fill(0),Error(`VoiceID unlock expired or was cancelled.`);return{kind:`unlocked`,assertion:te({kind:`voiceid_device_assertion_v1`,challenge:r,signatureB64u:g(new Uint8Array(a))}),factorSecret32:o}}async function oe(e,t,n){u();let{statement:r}=n;if(r.requestOrigin!==location.origin||r.expiresAtMs<=Date.now())throw Error(`VoiceID check is expired or belongs to another Wallet origin.`);let i=await S(r.walletId,r.walletAuthMethodId);if(!i||i.deviceKeyId!==r.deviceKeyId)throw Error(`VoiceID device key is unavailable in this browser.`);if(await $({kind:`active`,scope:await T(r.walletId,r.walletAuthMethodId)},n.signal,t.onCapture,t.phrase,e),n.signal.throwIfAborted(),r.expiresAtMs<=Date.now())throw Error(`VoiceID check expired. Start again.`);let a=await crypto.subtle.sign(`Ed25519`,i.privateKey,O(r)),o=await y(i);if(n.signal.aborted||r.expiresAtMs<=Date.now())throw o.fill(0),Error(`VoiceID check expired or was cancelled.`);return{kind:`confirmed`,assertion:b({kind:`voiceid_operation_assertion_v1`,statement:r,signatureB64u:g(new Uint8Array(a))}),factorSecret32:o}}async function $(e,t,n,i,a){let o=await Q(e,t,n,i,a);switch(o.kind){case`matched`:return;case`insufficient_audio`:throw new s(o.quality);case`mismatch`:throw new p(o.quality);case`phrase_mismatch`:throw new r;default:return se(o)}}function se(e){throw Error(`Unexpected voice result: ${String(e)}`)}export{Y as a,z as c,Z as i,oe as n,J as o,ae as r,K as s,ie as t};