@seams/voiceid 0.26.0 → 0.28.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 +37 -3
- package/dist/esm/browser/microphoneInput.js +13 -0
- package/dist/esm/capture/prompt.js +41 -2
- package/dist/esm/descriptor.js +5 -2
- package/dist/public/sdk/voiceid/CITRINET_LICENSE.txt +395 -0
- package/dist/public/sdk/voiceid/NOTICE.txt +19 -2
- package/dist/public/sdk/voiceid/addVoiceId-Dbd-PwWG.js +1 -0
- package/dist/public/sdk/voiceid/approval-C4KlEQYu.js +1 -0
- package/dist/public/sdk/voiceid/citrinet-int8.onnx +0 -0
- package/dist/public/sdk/voiceid/{deviceBridge-DDviNxL6.js → deviceBridge-DQLPNdC8.js} +1 -1
- package/dist/public/sdk/voiceid/enrollment-DPOYb36Z.js +1 -0
- package/dist/public/sdk/voiceid/host.js +2 -2
- package/dist/public/sdk/voiceid/prompt-vztg41jG.js +614 -0
- package/dist/public/sdk/voiceid/unlock-Da49yYxd.js +1 -0
- package/dist/types/activation/addVoiceId.d.ts +2 -1
- package/dist/types/activation/manageEnrollment.d.ts +2 -1
- package/dist/types/browser/enrollment.d.ts +3 -2
- package/dist/types/browser/inference.d.ts +5 -0
- package/dist/types/browser/microphone.d.ts +6 -6
- package/dist/types/browser/microphoneInput.d.ts +27 -0
- package/dist/types/browser/model.d.ts +1 -1
- package/dist/types/browser/phrase.d.ts +5 -2
- package/dist/types/browser/phraseTokens.d.ts +2 -0
- package/dist/types/browser/phraseVocabulary.d.ts +3 -3
- package/dist/types/browser/progress.d.ts +2 -1
- package/dist/types/capture/approval.d.ts +5 -2
- package/dist/types/capture/bridge.d.ts +2 -1
- package/dist/types/capture/deviceBridge.d.ts +7 -4
- package/dist/types/capture/prompt.d.ts +4 -0
- package/dist/types/descriptor.d.ts +5 -2
- package/dist/types/host.d.ts +3 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/session/activation.d.ts +2 -2
- package/dist/types/session/unlock.d.ts +2 -1
- package/dist/types/session/unlockFromDevice.d.ts +2 -2
- package/package.json +2 -2
- package/src/README.md +26 -16
- package/dist/public/sdk/voiceid/addVoiceId-Bz9BDPI4.js +0 -1
- package/dist/public/sdk/voiceid/approval-CME3DxNc.js +0 -1
- package/dist/public/sdk/voiceid/enrollment-D8oQnM_e.js +0 -1
- package/dist/public/sdk/voiceid/prompt-DEkI70F2.js +0 -606
- package/dist/public/sdk/voiceid/unlock-CGnhhdt4.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./prompt-vztg41jG.js";import{a as e,n as t}from"./deviceBridge-DQLPNdC8.js";import{f as n,o as r,s as i}from"./enrollment-DPOYb36Z.js";import{IndexedDBManager as a,fetchWithWalletHomeDiscovery as o,readAuthMethodEnabled as s}from"../voiceid-host.js";async function c(e){let{authMethod:t,challenge:n}=e;if(n.walletId!==t.walletId||n.walletAuthMethodId!==t.walletAuthMethodId||n.walletAuthorityId!==t.walletAuthorityId||n.deviceKeyId!==t.deviceKeyId)throw Error(`VoiceID challenge does not match the selected device method.`);let r=await e.capture({challenge:n,signal:e.signal,onCapture:e.onCapture,phrase:e.phrase});try{if(e.signal.aborted)throw Error(`VoiceID unlock was cancelled.`);let n=await e.http.verify({assertion:r.assertion,signal:e.signal});if(e.signal.aborted)throw Error(`VoiceID unlock was cancelled.`);if(n.walletSession.walletId!==t.walletId||n.walletSession.authMethodId!==t.walletAuthMethodId||n.walletSession.authorityId!==t.walletAuthorityId||n.custodyEnvelope.factor.deviceKeyId!==t.deviceKeyId)throw Error(`VoiceID server verified a different device method.`);return await e.install(n,r.factorSecret32)}finally{r.factorSecret32.fill(0)}}async function l(t,c,l,u){if(!await s(t.walletId,t.walletAuthMethodId))throw Error(`VoiceID is turned off in this browser. Enable it in Authentication methods.`);let f=await t.prepareSession(c),p=await a.getWalletAuthMethodV2(t.walletAuthMethodId);if(!p||p.kind!==`voiceid`||p.status!==`active`||p.walletId!==t.walletId||p.walletAuthMethodId!==t.walletAuthMethodId)throw Error(`Activate VoiceID on this device before unlocking this wallet.`);let m=new e(t.relayerUrl,location.origin,o);if(!await r(await i(t.walletId,t.walletAuthMethodId)))throw Error(`Unlock with a passkey, then open Manage voice to import or record your enrollment.`);await n(l,c,u),c.throwIfAborted(),t.assertActive();let h=await m.challenge({walletId:t.walletId,walletAuthMethodId:t.walletAuthMethodId,signal:AbortSignal.any([c,AbortSignal.timeout(15e3)])});t.assertActive();let g={authMethod:p,challenge:h,http:m,install:f};return c.throwIfAborted(),{kind:`recording`,expiresAtMs:h.expiresAtMs,action:d.bind(null,u,g)}}function u(e,t,n,r){return e({walletSession:n.walletSession,operationCredential:n.operationCredential,custodyEnvelope:n.custodyEnvelope,factorSecret32:r,signal:t,near:n.near})}async function d(e,n,r,i,a){return await c({authMethod:n.authMethod,challenge:n.challenge,http:n.http,capture:t.bind(null,e),install:u.bind(null,n.install,r),signal:r,onCapture:i,phrase:a})}export{l as prepareUnlockSurface};
|
|
@@ -2,10 +2,11 @@ import type { PreparedVoiceCapture } from '../capture/surface';
|
|
|
2
2
|
import type { RegistrationCapability, RegistrationWebContext } from '@seams/wallet/voiceid-host';
|
|
3
3
|
import { type WalletId } from '@seams/wallet/voiceid-host';
|
|
4
4
|
import type { Variant } from '@seams/wallet/voiceid-host';
|
|
5
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
5
6
|
type AddVoiceIdResult = Variant<Awaited<ReturnType<RegistrationCapability['activateAuthMethod']>>, 'kind', 'activated'>;
|
|
6
7
|
export declare function prepareActivationSurface(input: {
|
|
7
8
|
readonly context: RegistrationWebContext;
|
|
8
9
|
readonly walletId: WalletId;
|
|
9
|
-
}, signal: AbortSignal): Promise<PreparedVoiceCapture<AddVoiceIdResult>>;
|
|
10
|
+
}, signal: AbortSignal, model: VoiceIdTranscriptionModel): Promise<PreparedVoiceCapture<AddVoiceIdResult>>;
|
|
10
11
|
export {};
|
|
11
12
|
//# sourceMappingURL=addVoiceId.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RegistrationWebContext } from '@seams/wallet/voiceid-host';
|
|
2
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
2
3
|
import { type VoiceEnrollmentScope } from '../browser/enrollmentStore';
|
|
3
|
-
export declare function manageVoiceEnrollment(context: RegistrationWebContext, scope: VoiceEnrollmentScope, signal: AbortSignal): Promise<void>;
|
|
4
|
+
export declare function manageVoiceEnrollment(context: RegistrationWebContext, scope: VoiceEnrollmentScope, signal: AbortSignal, model: VoiceIdTranscriptionModel): Promise<void>;
|
|
4
5
|
//# sourceMappingURL=manageEnrollment.d.ts.map
|
|
@@ -3,6 +3,7 @@ import { type VoiceCaptureQuality, type VoiceCaptureObserver } from './microphon
|
|
|
3
3
|
import { type VoiceEnrollmentScope, type VoiceEnrollmentSnapshot, type VoiceOwnerGuard, type VoiceTemplateAccess } from './enrollmentStore';
|
|
4
4
|
import type { ConfirmedVoiceChange } from '../activation/ownerConfirmation';
|
|
5
5
|
import type { Variant } from '@seams/wallet/voiceid-host';
|
|
6
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
6
7
|
type EnrollmentCapturePlan = {
|
|
7
8
|
readonly scope: VoiceEnrollmentScope;
|
|
8
9
|
readonly guard: VoiceOwnerGuard;
|
|
@@ -21,8 +22,8 @@ type EnrollmentCapturePlan = {
|
|
|
21
22
|
readonly snapshot: Variant<VoiceEnrollmentSnapshot, 'kind', 'present'>;
|
|
22
23
|
readonly confirmation: ConfirmedVoiceChange;
|
|
23
24
|
});
|
|
24
|
-
export declare function enrollBrowserVoice(plan: EnrollmentCapturePlan, signal: AbortSignal): Promise<void>;
|
|
25
|
-
export declare function verifyBrowserVoice(access: VoiceTemplateAccess, signal: AbortSignal, onCapture: VoiceCaptureObserver, phrase: SpokenPhrase): Promise<{
|
|
25
|
+
export declare function enrollBrowserVoice(plan: EnrollmentCapturePlan, signal: AbortSignal, model: VoiceIdTranscriptionModel): Promise<void>;
|
|
26
|
+
export declare function verifyBrowserVoice(access: VoiceTemplateAccess, signal: AbortSignal, onCapture: VoiceCaptureObserver, phrase: SpokenPhrase, model: VoiceIdTranscriptionModel): Promise<{
|
|
26
27
|
kind: 'matched';
|
|
27
28
|
} | {
|
|
28
29
|
kind: 'mismatch';
|
|
@@ -13,6 +13,10 @@ export type VoiceAssetProgress = {
|
|
|
13
13
|
} | {
|
|
14
14
|
kind: 'ready';
|
|
15
15
|
};
|
|
16
|
+
export declare class VoiceIdLoadError extends Error {
|
|
17
|
+
readonly kind: 'connection' | 'unavailable' | 'integrity' | 'initialization';
|
|
18
|
+
constructor(kind: 'connection' | 'unavailable' | 'integrity' | 'initialization', message: string, options?: ErrorOptions);
|
|
19
|
+
}
|
|
16
20
|
declare class BrowserInferenceAsset {
|
|
17
21
|
private readonly asset;
|
|
18
22
|
private pending;
|
|
@@ -25,6 +29,7 @@ declare class BrowserInferenceAsset {
|
|
|
25
29
|
private publish;
|
|
26
30
|
private valid;
|
|
27
31
|
private bytes;
|
|
32
|
+
private fetchBytes;
|
|
28
33
|
}
|
|
29
34
|
export declare const voiceInferenceEngine: BrowserInferenceAsset;
|
|
30
35
|
export declare class BrowserInferenceModel {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type VoiceMicrophone } from './microphoneInput';
|
|
1
2
|
/** One 32 ms frame as the recorder judged it. No capture event carries the stream. */
|
|
2
3
|
export type VoiceActivityEvent = Readonly<{
|
|
3
4
|
kind: 'activity';
|
|
@@ -7,6 +8,9 @@ export type VoiceActivityEvent = Readonly<{
|
|
|
7
8
|
stream?: never;
|
|
8
9
|
}>;
|
|
9
10
|
export type VoiceCaptureEvent = {
|
|
11
|
+
readonly kind: 'opening';
|
|
12
|
+
readonly stream?: never;
|
|
13
|
+
} | {
|
|
10
14
|
readonly kind: 'listening';
|
|
11
15
|
readonly stream?: never;
|
|
12
16
|
} | VoiceActivityEvent | {
|
|
@@ -29,12 +33,8 @@ export type VoiceCaptureQuality = Readonly<{
|
|
|
29
33
|
levelThreshold: number;
|
|
30
34
|
requiredVoicedMs: number;
|
|
31
35
|
microphone: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
echoCancellation: boolean | string | null;
|
|
35
|
-
autoGainControl: boolean | null;
|
|
36
|
-
sampleRate: number | null;
|
|
37
|
-
}>;
|
|
36
|
+
microphoneSelection: VoiceMicrophone['selection'];
|
|
37
|
+
processing: VoiceMicrophone['processing'];
|
|
38
38
|
backgroundRms: number;
|
|
39
39
|
speechToBackgroundDb: number | null;
|
|
40
40
|
noise: 'high' | 'normal' | 'unknown';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type MicrophonePreference = {
|
|
2
|
+
readonly kind: 'system_default';
|
|
3
|
+
readonly deviceId?: never;
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'device';
|
|
6
|
+
readonly deviceId: string;
|
|
7
|
+
};
|
|
8
|
+
export type VoiceMicrophone = Readonly<{
|
|
9
|
+
stream: MediaStream;
|
|
10
|
+
preference: MicrophonePreference;
|
|
11
|
+
selection: 'default' | 'preferred' | 'replacement';
|
|
12
|
+
label: string;
|
|
13
|
+
processing: Readonly<{
|
|
14
|
+
noiseSuppression: boolean | null;
|
|
15
|
+
echoCancellation: boolean | string | null;
|
|
16
|
+
autoGainControl: boolean | null;
|
|
17
|
+
sampleRate: number | null;
|
|
18
|
+
}>;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function openVoiceMicrophone(signal: AbortSignal): Promise<VoiceMicrophone>;
|
|
21
|
+
export declare function rememberVoiceMicrophone(microphone: VoiceMicrophone): Promise<void>;
|
|
22
|
+
export declare function useSystemVoiceMicrophone(): Promise<void>;
|
|
23
|
+
export declare class VoiceMicrophoneInterruptedError extends Error {
|
|
24
|
+
constructor();
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=microphoneInput.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BrowserInferenceModel } from './inference';
|
|
2
2
|
export declare const VOICE_MODEL_ID = "redimnet-b1:e04dd7a6a1e23fc7b89e47682ea9d0a3946cf7bc59d07922c3db3efc52ae4269";
|
|
3
|
-
export declare const VOICE_MATCH_THRESHOLD = 0.
|
|
3
|
+
export declare const VOICE_MATCH_THRESHOLD = 0.45;
|
|
4
4
|
export declare const voiceModel: BrowserInferenceModel;
|
|
5
5
|
export declare function voiceEmbedding(samples: Float32Array): Promise<number[]>;
|
|
6
6
|
export declare function normalizeVoiceVector(values: readonly number[]): number[];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BrowserInferenceModel } from './inference';
|
|
2
2
|
import { SPOKEN_PHRASES } from './phraseVocabulary';
|
|
3
3
|
import type { VoiceConfirmation } from './confirmationCommand';
|
|
4
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
4
5
|
export type SpokenPhrase = (typeof SPOKEN_PHRASES)[number];
|
|
5
|
-
export declare
|
|
6
|
+
export declare function getPhraseModel(model: VoiceIdTranscriptionModel): BrowserInferenceModel;
|
|
6
7
|
export declare function newSpokenPhrase(previous: SpokenPhrase | null): SpokenPhrase;
|
|
7
8
|
export declare function newConfirmationPhrase(confirmation: VoiceConfirmation, previous: SpokenPhrase | null): SpokenPhrase;
|
|
8
9
|
type PhraseRecognition = {
|
|
@@ -32,6 +33,8 @@ export type PhraseCheck = {
|
|
|
32
33
|
kind: 'mismatch';
|
|
33
34
|
recognition: PhraseRecognition;
|
|
34
35
|
};
|
|
35
|
-
|
|
36
|
+
/** Apply the same phrase checks to an independently decoded transcript. */
|
|
37
|
+
export declare function checkTranscribedPhrase(decodedText: string, expected: SpokenPhrase): PhraseCheck;
|
|
38
|
+
export declare function checkSpokenPhrase(samples: Float32Array, expected: SpokenPhrase, model: VoiceIdTranscriptionModel): Promise<PhraseCheck>;
|
|
36
39
|
export {};
|
|
37
40
|
//# sourceMappingURL=phrase.d.ts.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export declare const CHALLENGE_PHRASES: readonly ["please sign this trade now", "confirm my order for today", "
|
|
1
|
+
export declare const CHALLENGE_PHRASES: readonly ["please sign this trade now", "confirm my order for today", "please review my order", "dump on them slowly", "send this order to market", "i'm here for the tech", "keep this trade on track", "the market is open", "if you panic panic early", "i check the latest quote", "exit liquidity for all passengers", "no crying in the casino", "this trade fits my plan", "approve this transfer at once", "check the price before trading", "let this trade go through", "be retarded when others are fearful", "be fearful when others are retarded", "life is short go long to hedge", "sign this swap right now", "sign this swap with care", "sign this swap today", "check my order right now", "check my order with care", "check my order today", "confirm the trade right now", "confirm the trade with care", "confirm the trade today", "review this transfer right now", "review this transfer with care", "review this transfer today"];
|
|
2
2
|
export declare const CONFIRMATION_PHRASES: {
|
|
3
|
-
readonly sale: readonly ["
|
|
3
|
+
readonly sale: readonly ["please confirm this sale", "the sale is ready"];
|
|
4
4
|
readonly purchase: readonly ["this purchase has my approval", "please confirm my purchase"];
|
|
5
5
|
readonly swap: readonly ["confirm my swap today", "the swap is ready to proceed"];
|
|
6
6
|
readonly transfer: readonly ["confirm this transfer today", "please send this transfer now"];
|
|
7
7
|
};
|
|
8
|
-
export declare const SPOKEN_PHRASES: readonly ["please sign this trade now", "confirm my order for today", "
|
|
8
|
+
export declare const SPOKEN_PHRASES: readonly ["please sign this trade now", "confirm my order for today", "please review my order", "dump on them slowly", "send this order to market", "i'm here for the tech", "keep this trade on track", "the market is open", "if you panic panic early", "i check the latest quote", "exit liquidity for all passengers", "no crying in the casino", "this trade fits my plan", "approve this transfer at once", "check the price before trading", "let this trade go through", "be retarded when others are fearful", "be fearful when others are retarded", "life is short go long to hedge", "sign this swap right now", "sign this swap with care", "sign this swap today", "check my order right now", "check my order with care", "check my order today", "confirm the trade right now", "confirm the trade with care", "confirm the trade today", "review this transfer right now", "review this transfer with care", "review this transfer today", "please confirm this sale", "the sale is ready", "this purchase has my approval", "please confirm my purchase", "confirm my swap today", "the swap is ready to proceed", "confirm this transfer today", "please send this transfer now"];
|
|
9
9
|
export declare const PHRASE_TOKEN_PIECES: readonly string[];
|
|
10
10
|
//# sourceMappingURL=phraseVocabulary.d.ts.map
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
2
|
+
export declare function prepareVoiceModelWithProgress(parent: HTMLElement, signal: AbortSignal, model: VoiceIdTranscriptionModel): Promise<void>;
|
|
2
3
|
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type VoiceConfirmation } from '../browser/confirmationCommand';
|
|
2
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
2
3
|
import type { AuthMethodApprovalResult } from '@seams/wallet/voiceid-host';
|
|
3
4
|
import type { ParentToChildEnvelope } from '@seams/wallet/voiceid-host';
|
|
4
5
|
import { type ConfiguredWalletHost } from '@seams/wallet/voiceid-host';
|
|
@@ -11,12 +12,13 @@ export type VoiceIdPolicy = {
|
|
|
11
12
|
readonly kind: 'required';
|
|
12
13
|
readonly approval: VoiceIdApproval;
|
|
13
14
|
};
|
|
14
|
-
export declare function voiceIdPolicyForRequest(request: ParentToChildEnvelope, configuration: ConfiguredWalletHost): Promise<VoiceIdPolicy>;
|
|
15
|
+
export declare function voiceIdPolicyForRequest(request: ParentToChildEnvelope, configuration: ConfiguredWalletHost, model: VoiceIdTranscriptionModel): Promise<VoiceIdPolicy>;
|
|
15
16
|
export declare class VoiceIdApproval {
|
|
16
17
|
private readonly reviewExpiresAtMs;
|
|
17
18
|
private readonly operationHash;
|
|
18
19
|
private readonly walletId;
|
|
19
20
|
private readonly confirmation;
|
|
21
|
+
private readonly model;
|
|
20
22
|
private phrase;
|
|
21
23
|
private readonly prompt;
|
|
22
24
|
readonly surface: HTMLElement;
|
|
@@ -26,7 +28,8 @@ export declare class VoiceIdApproval {
|
|
|
26
28
|
};
|
|
27
29
|
private state;
|
|
28
30
|
private constructor();
|
|
29
|
-
static forReview(review: TransactionReviewWire, operationHash: string, walletId: WalletId, confirmation: VoiceConfirmation): VoiceIdApproval;
|
|
31
|
+
static forReview(review: TransactionReviewWire, operationHash: string, walletId: WalletId, confirmation: VoiceConfirmation, model: VoiceIdTranscriptionModel): VoiceIdApproval;
|
|
32
|
+
prepare(signal: AbortSignal): Promise<void>;
|
|
30
33
|
approve(signal: AbortSignal): Promise<AuthMethodApprovalResult>;
|
|
31
34
|
/** Binds one listening attempt to the operation and to what remains of the review. */
|
|
32
35
|
private beginAttempt;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
1
2
|
import { type VoiceCaptureObserver } from '../browser/microphone';
|
|
2
3
|
import { type SpokenPhrase } from '../browser/phrase';
|
|
3
4
|
import type { AuthMethodApprovalResult } from '@seams/wallet/voiceid-host';
|
|
@@ -8,5 +9,5 @@ export type VoiceBinding = Readonly<{
|
|
|
8
9
|
expiresAtMs: number;
|
|
9
10
|
}>;
|
|
10
11
|
export declare function sameVoiceBinding(left: VoiceBinding, right: VoiceBinding): boolean;
|
|
11
|
-
export declare function listenForVoiceApproval(scope: VoiceEnrollmentScope, binding: VoiceBinding, timeoutMs: number, signal: AbortSignal, onCapture: VoiceCaptureObserver, phrase: SpokenPhrase): Promise<AuthMethodApprovalResult>;
|
|
12
|
+
export declare function listenForVoiceApproval(scope: VoiceEnrollmentScope, binding: VoiceBinding, timeoutMs: number, signal: AbortSignal, onCapture: VoiceCaptureObserver, phrase: SpokenPhrase, model: VoiceIdTranscriptionModel): Promise<AuthMethodApprovalResult>;
|
|
12
13
|
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
1
2
|
import type { VoiceCaptureObserver } from '../browser/microphone';
|
|
2
3
|
import { type SpokenPhrase } from '../browser/phrase';
|
|
3
4
|
import { type VoiceIdWalletSessionChallenge } from '@seams/wallet/voiceid-host';
|
|
4
5
|
import type { WalletId, WalletAuthMethodId } from '@seams/wallet/voiceid-host';
|
|
5
6
|
import type { VoiceIdDeviceAssertion, VoiceIdDevicePublicKeyB64u } from '@seams/wallet/voiceid-host';
|
|
6
7
|
import type { DigestB64u } from '@seams/wallet/voiceid-host';
|
|
7
|
-
export
|
|
8
|
+
export type VoiceIdActivationCapture = (input: {
|
|
8
9
|
readonly walletId: WalletId;
|
|
9
10
|
readonly walletAuthMethodId: WalletAuthMethodId;
|
|
10
11
|
readonly signal: AbortSignal;
|
|
11
12
|
readonly onCapture: VoiceCaptureObserver;
|
|
12
13
|
readonly phrase: SpokenPhrase;
|
|
13
|
-
})
|
|
14
|
+
}) => Promise<{
|
|
14
15
|
kind: 'activated';
|
|
15
16
|
publicKeyB64u: VoiceIdDevicePublicKeyB64u;
|
|
16
17
|
deviceKeyId: DigestB64u;
|
|
17
18
|
factorSecret32: Uint8Array;
|
|
18
19
|
}>;
|
|
19
|
-
export declare function
|
|
20
|
+
export declare function captureVoiceIdActivation(model: VoiceIdTranscriptionModel, input: Parameters<VoiceIdActivationCapture>[0]): ReturnType<VoiceIdActivationCapture>;
|
|
21
|
+
export type VoiceIdUnlockCapture = (input: {
|
|
20
22
|
readonly challenge: VoiceIdWalletSessionChallenge;
|
|
21
23
|
readonly signal: AbortSignal;
|
|
22
24
|
readonly onCapture: VoiceCaptureObserver;
|
|
23
25
|
readonly phrase: SpokenPhrase;
|
|
24
|
-
})
|
|
26
|
+
}) => Promise<{
|
|
25
27
|
kind: 'unlocked';
|
|
26
28
|
assertion: VoiceIdDeviceAssertion;
|
|
27
29
|
factorSecret32: Uint8Array;
|
|
28
30
|
}>;
|
|
31
|
+
export declare function captureVoiceIdUnlock(model: VoiceIdTranscriptionModel, input: Parameters<VoiceIdUnlockCapture>[0]): ReturnType<VoiceIdUnlockCapture>;
|
|
29
32
|
//# sourceMappingURL=deviceBridge.d.ts.map
|
|
@@ -9,6 +9,8 @@ export declare class VoicePrompt {
|
|
|
9
9
|
readonly record: HTMLButtonElement;
|
|
10
10
|
readonly cancel: HTMLButtonElement;
|
|
11
11
|
readonly progress: HTMLElement;
|
|
12
|
+
private readonly microphone;
|
|
13
|
+
private microphoneRequested;
|
|
12
14
|
private readonly visual;
|
|
13
15
|
private disposed;
|
|
14
16
|
constructor(document: Document, label: string, phrase: SpokenPhrase, checking?: VoiceCheckingStyle);
|
|
@@ -22,6 +24,8 @@ export declare class VoicePrompt {
|
|
|
22
24
|
*/
|
|
23
25
|
succeed(headline: string, message: string, area?: SuccessArea): Promise<void>;
|
|
24
26
|
ready(message: string): void;
|
|
27
|
+
private readonly changeMicrophone;
|
|
28
|
+
private useSystemMicrophone;
|
|
25
29
|
dispose(): void;
|
|
26
30
|
/** The ribbon turns into the ring and the copy may change, so the card's rows hold their size. */
|
|
27
31
|
private lockLayout;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { VoiceIdAuthMethodPlugin } from '@seams/wallet/voiceid-host';
|
|
1
|
+
import type { VoiceIdAuthMethodPlugin, VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
2
|
+
export type VoiceIdOptions = Readonly<{
|
|
3
|
+
transcriptionModel?: VoiceIdTranscriptionModel;
|
|
4
|
+
}>;
|
|
2
5
|
/** A small opt-in descriptor with no capture, model, or host implementation. */
|
|
3
|
-
export declare function voiceIdAuthMethod(): VoiceIdAuthMethodPlugin;
|
|
6
|
+
export declare function voiceIdAuthMethod(options?: VoiceIdOptions): VoiceIdAuthMethodPlugin;
|
|
4
7
|
//# sourceMappingURL=descriptor.d.ts.map
|
package/dist/types/host.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WalletAuthMethodHostPlugin } from '@seams/wallet/voiceid-host';
|
|
2
|
-
|
|
3
|
-
export declare const
|
|
2
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
3
|
+
export declare const hostApiVersion = 3;
|
|
4
|
+
export declare function createVoiceIdHostPlugin(model: VoiceIdTranscriptionModel): WalletAuthMethodHostPlugin;
|
|
4
5
|
//# sourceMappingURL=host.d.ts.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { voiceIdAuthMethod } from './descriptor';
|
|
1
|
+
export { voiceIdAuthMethod, type VoiceIdOptions } from './descriptor';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -7,7 +7,7 @@ import { type PasskeyEnvelopeId, type WalletAuthMethodId, type WalletId } from '
|
|
|
7
7
|
import { type VoiceIdDevicePublicKeyB64u } from '@seams/wallet/voiceid-host';
|
|
8
8
|
import type { ActiveVoiceIdWalletAuthMethodRecordV2 } from '@seams/wallet/voiceid-host';
|
|
9
9
|
import type { UnlockedWalletEd25519ExportRootCapabilityV1 } from '@seams/wallet/voiceid-host';
|
|
10
|
-
import {
|
|
10
|
+
import { type VoiceIdActivationCapture } from '../capture/deviceBridge';
|
|
11
11
|
import type { VoiceIdHttpClient } from './httpClient';
|
|
12
12
|
export type PendingVoiceIdActivation = Readonly<{
|
|
13
13
|
kind: 'pending_voiceid_activation_v1';
|
|
@@ -53,7 +53,7 @@ export declare function activateVoiceIdForUnlockedWallet(input: {
|
|
|
53
53
|
readonly signal: AbortSignal;
|
|
54
54
|
readonly onCapture: VoiceCaptureObserver;
|
|
55
55
|
readonly phrase: SpokenPhrase;
|
|
56
|
-
readonly capture:
|
|
56
|
+
readonly capture: VoiceIdActivationCapture;
|
|
57
57
|
readonly persistPending: (pending: PendingVoiceIdActivation) => Promise<void>;
|
|
58
58
|
readonly persistActive: (authMethod: ActiveVoiceIdWalletAuthMethodRecordV2, pending: PendingVoiceIdActivation) => Promise<void>;
|
|
59
59
|
}): Promise<VoiceIdActivationOutcome>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import type { VoiceIdTranscriptionModel } from '@seams/wallet/voiceid-host';
|
|
1
2
|
import type { AuthMethodUnlockSuccess, WalletAuthMethodHostPlugin } from '@seams/wallet/voiceid-host';
|
|
2
3
|
import type { PreparedVoiceCapture } from '../capture/surface';
|
|
3
|
-
export declare function prepareUnlockSurface(input: Parameters<WalletAuthMethodHostPlugin['unlock']>[0], signal: AbortSignal, parent: HTMLElement): Promise<PreparedVoiceCapture<AuthMethodUnlockSuccess>>;
|
|
4
|
+
export declare function prepareUnlockSurface(input: Parameters<WalletAuthMethodHostPlugin['unlock']>[0], signal: AbortSignal, parent: HTMLElement, model: VoiceIdTranscriptionModel): Promise<PreparedVoiceCapture<AuthMethodUnlockSuccess>>;
|
|
4
5
|
//# sourceMappingURL=unlock.d.ts.map
|
|
@@ -2,14 +2,14 @@ import type { VoiceCaptureObserver } from '../browser/microphone';
|
|
|
2
2
|
import type { SpokenPhrase } from '../browser/phrase';
|
|
3
3
|
import type { VoiceIdWalletSessionChallenge } from '@seams/wallet/voiceid-host';
|
|
4
4
|
import type { ActiveVoiceIdWalletAuthMethodRecordV2 } from '@seams/wallet/voiceid-host';
|
|
5
|
-
import {
|
|
5
|
+
import type { VoiceIdUnlockCapture } from '../capture/deviceBridge';
|
|
6
6
|
import type { VoiceIdHttpClient, VoiceIdVerifiedSession } from './httpClient';
|
|
7
7
|
/** The installer must finish custody activation before the caller reports unlock. */
|
|
8
8
|
export declare function verifyVoiceIdChallengeWithDevice<T>(input: {
|
|
9
9
|
readonly authMethod: ActiveVoiceIdWalletAuthMethodRecordV2;
|
|
10
10
|
readonly challenge: VoiceIdWalletSessionChallenge;
|
|
11
11
|
readonly http: Pick<VoiceIdHttpClient, 'verify'>;
|
|
12
|
-
readonly capture:
|
|
12
|
+
readonly capture: VoiceIdUnlockCapture;
|
|
13
13
|
readonly install: (verified: VoiceIdVerifiedSession, factorSecret32: Uint8Array) => Promise<T>;
|
|
14
14
|
readonly signal: AbortSignal;
|
|
15
15
|
readonly onCapture: VoiceCaptureObserver;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seams/voiceid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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
|
+
"@seams/wallet": "0.28.0",
|
|
54
54
|
"react": "^18.0.0 || ^19.0.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
package/src/README.md
CHANGED
|
@@ -39,12 +39,11 @@ device-key possession, and the Wallet code enforces speaker and phrase matching.
|
|
|
39
39
|
|
|
40
40
|
## Hosted model asset
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
must provision both assets. It is not fetched from a third party during capture.
|
|
42
|
+
The approved exports live in `packages/voiceid/assets/`. The VoiceID build
|
|
43
|
+
includes ReDimNet B1, Aurora INT8, Citrinet INT8, Silero VAD, and ONNX Runtime
|
|
44
|
+
Web WASM. A VoiceID action loads the shared speaker model and speech detector,
|
|
45
|
+
then only the configured speech model. Assets come from the trusted Wallet
|
|
46
|
+
origin. Plain Wallet deployments remain independent of this package.
|
|
48
47
|
|
|
49
48
|
Current model: ReDimNet B1, VoxCeleb2 large-margin fine-tuned, MIT license.
|
|
50
49
|
Source revision: `ce039a624cb99fe127702ceb94c6080090e5032f`.
|
|
@@ -56,17 +55,27 @@ The deployment includes the model's MIT attribution and export details in NOTICE
|
|
|
56
55
|
Use the pinned exporter in `tests/scripts/voiceid-model-evaluation/prepare.py`
|
|
57
56
|
to reproduce the model. The browser evaluation evidence records parity checks.
|
|
58
57
|
|
|
59
|
-
The
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
The experimental B1 threshold is 0.45 for a human usability trial. In the separate
|
|
59
|
+
historical evaluation, it accepted 1/728 different-speaker comparisons at four
|
|
60
|
+
seconds and 2/728 at two seconds. The historical capture pipeline differs from
|
|
61
|
+
the current microphone path. These results do not qualify authentication accuracy.
|
|
63
62
|
|
|
64
63
|
AuroraASR-1M checks the spoken phrase. Its INT8 full-waveform ONNX export is
|
|
65
64
|
1,342,384 bytes and shares the existing WASM runtime. It accepts 16 kHz mono PCM.
|
|
66
65
|
Source revision: `cd307b54b4d666d6ad7dc982a7a58a96d6e7f942`.
|
|
67
66
|
SHA-256: `83ed6f3856458cd96d49a70d8c5175aeec18ba8c184e5367e089cd424eebefd6`.
|
|
68
67
|
The deployment includes its Apache-2.0 license and export details.
|
|
69
|
-
See [export and screening instructions](
|
|
68
|
+
See [export and screening instructions](../../../tests/scripts/voiceid-aurora/README.md).
|
|
69
|
+
|
|
70
|
+
Citrinet-256 LS is the default speech model. Its INT8-weight full-waveform ONNX
|
|
71
|
+
export is 10,483,704 bytes, approximately 8.84 MB with gzip compression.
|
|
72
|
+
Source revision: `c7cdacff0262bbb2a17771d20ec9f9f3b5cff6ed`.
|
|
73
|
+
SHA-256: `875f58df0e2a593017ac9550c8fab68c4343cd1fbb2a042290450aa125441ed5`.
|
|
74
|
+
The deployment includes its CC-BY-4.0 license and attribution. Developers choose
|
|
75
|
+
`transcriptionModel: 'aurora'` or `transcriptionModel: 'citrinet'` in the
|
|
76
|
+
`voiceIdAuthMethod()` options. Each host plugin binds the choice before its action starts.
|
|
77
|
+
Speaker templates and thresholds remain independent of that choice.
|
|
78
|
+
See [export and parity instructions](../../../tests/scripts/voiceid-citrinet/README.md).
|
|
70
79
|
|
|
71
80
|
## Download progress and browser cache
|
|
72
81
|
|
|
@@ -112,7 +121,7 @@ generation/revision. A competing update cannot be overwritten. Capture watches
|
|
|
112
121
|
owner lifecycle changes and releases the microphone if the action becomes invalid.
|
|
113
122
|
|
|
114
123
|
Three accepted supplements contribute 20% of the effective template; the immutable
|
|
115
|
-
baseline contributes 80%. Each supplement must match the baseline at 0.
|
|
124
|
+
baseline contributes 80%. Each supplement must match the baseline at 0.45. The final
|
|
116
125
|
template must stay within cosine 0.98 of the baseline. Supplements remain saved
|
|
117
126
|
without a time or usage limit. Reset removes them and preserves the baseline. Re-recording creates a
|
|
118
127
|
new baseline generation. These are bounded experimental settings, not calibrated
|
|
@@ -153,15 +162,16 @@ recordings where live analysis is unavailable. Both speaker and phrase checks us
|
|
|
153
162
|
Existing B1 enrollment remains valid. Aurora adds a separate phrase check.
|
|
154
163
|
|
|
155
164
|
Aurora decodes the audio independently of the displayed phrase. The matcher compares
|
|
156
|
-
that text with all
|
|
165
|
+
that text with all 39 phrases. Eight additional phrases form four action-only
|
|
157
166
|
confirmation families. It ignores case, punctuation, and articles, and maps
|
|
158
|
-
selected homophones to a common spelling. It
|
|
167
|
+
selected homophones, including “sail” and “sale,” to a common spelling. It joins
|
|
168
|
+
the decoded word break “to day” as “today.” It permits at most 30% normalized character
|
|
159
169
|
edit distance, requires a 10-point distance margin over the next candidate, and
|
|
160
170
|
limits decoded word count to within two words of the candidate. Negation must
|
|
161
171
|
match the candidate phrase; added, omitted, or changed negation rejects. These are development bounds for an approximate challenge. Some changed
|
|
162
172
|
content words can pass; this check does not interpret transaction intent.
|
|
163
173
|
A different recognized phrase, ambiguity, or an out-of-bounds result prevents
|
|
164
|
-
enrollment, device-key use, or signing. The speaker threshold
|
|
174
|
+
enrollment, device-key use, or signing. The speaker threshold is 0.45. A signing retry
|
|
165
175
|
keeps the original transaction review and deadline and requires another Confirm.
|
|
166
176
|
No audio or transcript leaves the browser or enters persistent storage.
|
|
167
177
|
|
|
@@ -271,7 +281,7 @@ control remain requested. High-noise recordings receive brief retry guidance.
|
|
|
271
281
|
Speech detection replaces volume alone for pause detection and clip qualification.
|
|
272
282
|
It retains the 1.5-second pause and eight-second recording cap. Speech probability
|
|
273
283
|
must reach 0.5 and the original minimum volume must also pass. The speaker
|
|
274
|
-
threshold
|
|
284
|
+
threshold is 0.45. Phrase matching retains the bounded text-difference and
|
|
275
285
|
candidate-margin checks described above.
|
|
276
286
|
|
|
277
287
|
Noise estimates compare frames classified as speech with other frames. They are
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./prompt-DEkI70F2.js";import{a as e,i as t,r as n,t as r}from"./deviceBridge-DDviNxL6.js";import{a as i,d as a,i as o,o as s,s as c,t as l}from"./enrollment-D8oQnM_e.js";import{IndexedDBManager as u,WorkerDeferred as d,allocateWalletAuthMethodId as f,alphabetizeStringify as p,base64UrlDecode as m,buildMethodBoundEnvelopeOwnership as h,buildPasskeyCustodyEnvelopeRecord as g,buildVoiceIdEnvelopeFactor as _,buildWalletCustodySeedBinding as v,custodyEnvelopeBindingJsonV1 as y,fetchWithWalletHomeDiscovery as b,isVoiceIdSeedEnvelope as x,parseDigestB64u as S,parseEnvelopeRevision as C,parsePasskeyCustodyEnvelopeRecord as w,parsePasskeyEnvelopeId as T,parseResealedEnvelopeResult as E,parseVoiceIdDevicePublicKeyB64u as D,parseWalletAuthMethodId as O,parseWalletAuthMethodRecordV2 as k,parseWalletId as A,readUnlockedWalletEd25519ExportRootCapabilityV1 as j,rejectUnknownFields as M,requireRecord as N,secureRandomId as P,sha256DigestB64u as F,toError as I,walletCustodyCeremonyTransportFromWorkerContextV1 as L,walletSessionAuthorizations as R}from"../voiceid-host.js";async function z(e){let t=N(e,`pending VoiceID activation`);if(M(t,[`kind`,`walletId`,`walletAuthMethodId`,`devicePublicKeyB64u`,`deviceKeyId`,`custodyEnvelope`],`pending VoiceID activation`),t.kind!==`pending_voiceid_activation_v1`)throw Error(`Pending VoiceID activation kind is invalid.`);let n=A(t.walletId),r=O(t.walletAuthMethodId);if(!n.ok||!r.ok)throw Error(`Pending VoiceID activation identity is invalid.`);let i=D(t.devicePublicKeyB64u),a=S(t.deviceKeyId),o=w(t.custodyEnvelope);if(!x(o)||o.walletId!==n.value||o.ownership.walletAuthMethodId!==r.value||o.factor.deviceKeyId!==a||Number(o.envelopeRevision)!==1||await F(m(i))!==a)throw Error(`Pending VoiceID activation binding is invalid.`);return{kind:`pending_voiceid_activation_v1`,walletId:n.value,walletAuthMethodId:r.value,devicePublicKeyB64u:i,deviceKeyId:a,custodyEnvelope:o}}function B(e){throw Error(`Unhandled VoiceID activation plan: ${String(e)}`)}function V(e,t){let{capability:n}=e;if(n.walletId!==e.walletId||n.walletSessionId!==t.walletSessionId||n.expiresAtMs<=Date.now())throw Error(`VoiceID activation requires the unlocked owner seed and Wallet Session.`)}async function H(e,t){let n=_({deviceKeyId:t.deviceKeyId}),r=v(),i=h(e.walletAuthMethodId),a=C(1),o=y({walletId:e.walletId,envelopeId:e.envelopeId,factor:n,envelopeRevision:a,binding:r,ownership:i}),s=t.factorSecret32.slice(),c;try{c=E(await e.reseal({capability:e.capability,bindingJson:o,factorSecret:s.buffer}))}finally{s.byteLength>0&&s.fill(0)}if(await F(m(c.sealedCustodySecretB64u))!==c.ciphertextDigestB64u)throw Error(`VoiceID worker returned a mismatched ciphertext digest.`);let l=Date.now(),u=w(g({envelopeId:e.envelopeId,walletId:e.walletId,ownership:i,binding:r,factor:n,envelopeRevision:a,nonceB64u:c.nonceB64u,sealedCustodySecretB64u:c.sealedCustodySecretB64u,ciphertextDigestB64u:c.ciphertextDigestB64u,aadHashB64u:c.aadHashB64u,lifecycle:{state:`active`,activatedAtMs:l},createdAtMs:l,updatedAtMs:l}));if(!x(u))throw Error(`VoiceID worker produced an invalid seed envelope.`);return{kind:`pending_voiceid_activation_v1`,walletId:e.walletId,walletAuthMethodId:e.walletAuthMethodId,devicePublicKeyB64u:t.publicKeyB64u,deviceKeyId:t.deviceKeyId,custodyEnvelope:u}}async function U(e){let t=e.plan;switch(t.kind){case`new`:V(t,e.ownerCredential);break;case`resume`:break;default:return B(t)}let n=t.kind===`new`?t.walletId:t.pending.walletId,r=t.kind===`new`?t.walletAuthMethodId:t.pending.walletAuthMethodId,i=await e.capture({walletId:n,walletAuthMethodId:r,signal:e.signal,onCapture:e.onCapture,phrase:e.phrase}),a;try{switch(t.kind){case`new`:a=await H(t,i),await e.persistPending(a);break;case`resume`:if(t.pending.devicePublicKeyB64u!==i.publicKeyB64u||t.pending.deviceKeyId!==i.deviceKeyId)throw Error(`VoiceID activation retry found a different device key.`);a=t.pending;break;default:return B(t)}}finally{i.factorSecret32.fill(0)}if(e.signal.aborted)throw Error(`VoiceID activation was cancelled.`);let o=await e.http.activate({ownerCredential:e.ownerCredential,walletId:a.walletId,walletAuthMethodId:a.walletAuthMethodId,devicePublicKeyB64u:a.devicePublicKeyB64u,deviceKeyId:a.deviceKeyId,custodyEnvelope:a.custodyEnvelope,signal:e.signal});return await e.persistActive(o,a),{authMethod:o,pending:a}}const W=`voiceid-activation-v1:`;function G(e,t){return`${W}${encodeURIComponent(e)}:${encodeURIComponent(t)}`}function K(e){return`${W}${encodeURIComponent(e)}:`}function q(e,t){return p(e)===p(t)}async function J(e){if(e==null)return null;let t=N(e,`stored VoiceID activation`);switch(t.kind){case`pending`:return M(t,[`kind`,`pending`],`stored VoiceID activation`),{kind:`pending`,pending:await z(t.pending)};case`active`:{M(t,[`kind`,`pending`,`authMethod`],`stored VoiceID activation`);let e=await z(t.pending),n=k(t.authMethod);if(!n||n.kind!==`voiceid`||n.status!==`active`||n.walletId!==e.walletId||n.walletAuthMethodId!==e.walletAuthMethodId||n.deviceKeyId!==e.deviceKeyId||n.devicePublicKeyB64u!==e.devicePublicKeyB64u)throw Error(`Stored VoiceID activation method is invalid.`);return{kind:`active`,pending:e,authMethod:n}}default:throw Error(`Stored VoiceID activation lifecycle is invalid.`)}}var Y=class{constructor(e){this.storage=e}async persistPending(e){let t=G(e.walletId,e.walletAuthMethodId),n=await J(await this.storage.getAppState(t));if(n){if(n.kind===`pending`&&q(n.pending,e))return;throw Error(`A different VoiceID activation already owns this method ID.`)}await this.storage.setAppState(t,{kind:`pending`,pending:e})}async persistActive(e,t){let n=G(t.walletId,t.walletAuthMethodId),r=await J(await this.storage.getAppState(n));if(!r||!q(r.pending,t))throw Error(`VoiceID activation has no matching pending record.`);if(e.walletId!==t.walletId||e.walletAuthMethodId!==t.walletAuthMethodId||e.deviceKeyId!==t.deviceKeyId||e.devicePublicKeyB64u!==t.devicePublicKeyB64u)throw Error(`VoiceID server method does not match pending activation.`);await this.storage.upsertWalletAuthMethodV2(e),await this.storage.setAppState(n,{kind:`active`,pending:t,authMethod:e})}async readPending(e,t){let n=await J(await this.storage.getAppState(G(e,t)));if(!n||n.kind===`active`)return null;if(n.pending.walletId!==e||n.pending.walletAuthMethodId!==t)throw Error(`Stored VoiceID activation key does not match its record.`);return n.pending}async listPending(e){let t=await this.storage.listAppStateEntriesByPrefix(K(e)),n=[];for(let r of t){let t=await J(r.value);if(t){if(t.pending.walletId!==e||G(e,t.pending.walletAuthMethodId)!==r.key)throw Error(`Stored VoiceID activation key does not match its record.`);t.kind===`pending`&&n.push(t.pending)}}return n}};const X=`http://www.w3.org/2000/svg`;async function Z(e,t,n){let r=document.querySelector(`.seams-voiceid-activation-surface`);if(!(r instanceof HTMLElement))throw Error(`Open VoiceID management in the Wallet.`);let i=new Q(r,e,t,n);try{await i.result}finally{i.dispose()}}var Q=class{completion=new d;result=this.completion.promise;element=document.createElement(`div`);enrollment=document.createElement(`div`);enrollmentValue=document.createElement(`span`);startOver=document.createElement(`p`);status=document.createElement(`p`);actions;busy=!1;closed=!1;constructor(e,t,n,r){this.parent=e,this.context=t,this.scope=n,this.signal=r,this.element.className=`seams-voiceid-prompt seams-voiceid-management`;let i=document.createElement(`div`);i.className=`seams-voiceid-management-header`;let a=document.createElement(`button`);a.type=`button`,a.className=`seams-voiceid-management-back`,a.setAttribute(`aria-label`,`Back`),a.append($(`M15 6l-6 6 6 6`,20)),a.addEventListener(`click`,this.cancel);let o=document.createElement(`h2`);o.textContent=`Manage voice`,i.append(a,o);let s=document.createElement(`p`);s.className=`seams-voiceid-management-intro`,s.textContent=`Voice unlock on this device. Each change asks for your passkey.`,this.element.append(i,s,this.enrollmentRow()),this.actions={reset:this.resetAction(),improve:this.action(`improve`,`Improve recognition`,!0),import:this.action(`import`,`Use saved enrollment`,!1),replace:this.startOverAction()},this.status.setAttribute(`role`,`status`),this.element.append(this.status),this.enrollmentValue.textContent=`Checking…`,this.actions.import.button.hidden=!0,this.render(null,!1,!1),this.parent.replaceChildren(this.element),this.signal.addEventListener(`abort`,this.cancel,{once:!0}),this.refresh(),a.focus()}enrollmentRow(){this.enrollment.className=`seams-voiceid-management-enrollment`;let e=document.createElement(`span`);e.textContent=`Enrollment`,this.enrollmentValue.className=`seams-voiceid-management-value`;let t=document.createElement(`span`);return t.className=`seams-voiceid-management-summary`,t.append(this.enrollmentValue),this.enrollment.append(e,t),this.enrollment}resetAction(){let e=document.createElement(`button`);e.type=`button`,e.className=`seams-voiceid-management-reset`,e.textContent=`Reset`,e.setAttribute(`aria-label`,`Reset improvements`),e.addEventListener(`click`,this.begin.bind(this,`reset`));let t=document.createElement(`span`);return t.className=`seams-voiceid-management-enrollment-detail`,this.enrollment.querySelector(`.seams-voiceid-management-summary`)?.append(t),this.enrollment.append(e),{button:e,title:e,detail:t,frame:this.enrollment}}startOverAction(){return this.startOver.className=`seams-voiceid-management-section`,this.startOver.textContent=`Start over`,this.element.append(this.startOver),this.action(`replace`,`Record voice again`,!1)}action(e,t,n){let r=document.createElement(`button`);r.type=`button`,r.className=`seams-voiceid-management-action`;let i=document.createElement(`span`),a=document.createElement(`span`);a.className=`seams-voiceid-management-action-heading`;let o=document.createElement(`span`);o.id=`seams-voiceid-management-${e}-title`,o.textContent=t,a.append(o);let s=[];if(n){let t=document.createElement(`span`);t.id=`seams-voiceid-management-${e}-chip`,t.className=`seams-voiceid-management-chip`,t.textContent=`Experimental`,a.append(t),s.push(t.id)}let c=document.createElement(`span`);return c.id=`seams-voiceid-management-${e}-detail`,c.className=`seams-voiceid-management-action-detail`,s.push(c.id),i.append(a,c),r.append(i,$(`M9 6l6 6-6 6`,16)),r.setAttribute(`aria-labelledby`,o.id),r.setAttribute(`aria-describedby`,s.join(` `)),r.disabled=!0,r.addEventListener(`click`,this.begin.bind(this,e)),this.element.append(r),{button:r,title:o,detail:c,frame:r}}async refresh(){try{let e=await s(this.scope),t=e?null:await i();if(this.closed)return;this.render(e,t!==null,!0)}catch(e){this.completion.reject(I(e))}}render(e,t,n){let r=e?.adaptation??null,{improve:i,reset:a,import:o,replace:s}=this.actions;n&&(this.enrollment.dataset.enrollment=r?.kind??`missing`,this.enrollmentValue.textContent=r===null?`Not on this device`:r.kind===`adapted`?`Improved`:`Original`,i.button.hidden=e===null,o.button.hidden=!t),a.button.hidden=r?.kind!==`adapted`,a.detail.textContent=r?.kind===`adapted`?`Saved on this device`:``,i.title.textContent=r?.kind===`adapted`?`Improve again`:`Improve recognition`,i.detail.textContent=r?.kind===`adapted`?`Replace the added samples. Your original enrollment stays.`:`Add three samples. They stay until you replace or reset them.`,o.detail.textContent=`Import the voice enrollment saved in this browser.`,s.detail.textContent=r?.kind===`adapted`?`Replace your enrollment and remove improvements.`:`Replace your enrollment with three new samples.`,this.startOver.hidden=i.button.hidden&&o.button.hidden;for(let e of Object.values(this.actions))delete e.frame.dataset.state,e.button.disabled=!n;this.status.textContent=``,delete this.status.dataset.placement}show(e,t,n){let r=this.actions[e];if(this.status.textContent=n,r.button.hidden){this.status.dataset.placement=`dialog`;return}r.frame.dataset.state=t,r.detail.textContent=n}begin(e){if(!(this.busy||this.closed)){this.busy=!0;for(let e of Object.values(this.actions))e.button.disabled=!0;this.show(e,`pending`,`Confirm with your passkey to continue.`),this.perform(e)}}async perform(e){try{let r=await t(this.scope,`enrolled`),a=await s(this.scope),c=a?{kind:`present`,enrollment:a}:{kind:`missing`};if(e===`improve`&&c.kind===`missing`)throw Error(`Record or import your original voice first.`);let u=e===`import`?await i():null;if(e===`import`&&(!u||c.kind!==`missing`))throw Error(`The previous enrollment is unavailable.`);let d=await n.request(this.context,this.scope,r,c,e,this.signal);switch(this.signal.throwIfAborted(),e){case`improve`:if(c.kind!==`present`)throw Error(`Original voice enrollment is unavailable.`);await l({kind:`improve`,scope:this.scope,guard:r,snapshot:c,confirmation:d},this.signal);break;case`replace`:await l({kind:`replace`,scope:this.scope,guard:r,snapshot:c,confirmation:d},this.signal);break;case`reset`:await o(this.scope,r,c,{kind:`reset`,confirmation:d},this.signal);break;case`import`:if(!u)throw Error(`Previous voice enrollment is unavailable.`);await o(this.scope,r,c,{kind:`import`,baseline:u,confirmation:d},this.signal);break}this.closed||this.completion.resolve()}catch(t){if(this.closed)return;this.parent.replaceChildren(this.element),await this.refresh(),this.closed||this.show(e,`error`,t instanceof Error?t.message:`VoiceID could not be updated.`)}finally{this.busy=!1}}cancel=()=>{this.closed||(this.closed=!0,this.parent.dispatchEvent(new Event(`cancel`,{bubbles:!0})),this.completion.reject(Error(`VoiceID management was cancelled.`)))};dispose(){this.closed=!0,this.signal.removeEventListener(`abort`,this.cancel),this.element.remove()}};function $(e,t){let n=document.createElementNS(X,`svg`);n.setAttribute(`viewBox`,`0 0 24 24`),n.setAttribute(`width`,String(t)),n.setAttribute(`height`,String(t)),n.setAttribute(`aria-hidden`,`true`);let r=document.createElementNS(X,`path`);return r.setAttribute(`d`,e),r.setAttribute(`fill`,`none`),r.setAttribute(`stroke`,`currentColor`),r.setAttribute(`stroke-width`,`1.75`),r.setAttribute(`stroke-linecap`,`round`),r.setAttribute(`stroke-linejoin`,`round`),n.append(r),n}async function ee(e,t){return await e.requestOperation({kind:`walletCustodyCeremony`,request:{type:`resealWalletCustodyFromUnlockedCapability`,payload:{capability:t.capability,replacementEnvelopeBindingJson:t.bindingJson,replacementFactorSecret:t.factorSecret},transfer:[t.factorSecret]}})}async function te(t,n){let r=await u.resolveSelectedWalletAuthority(n);if(r.kind!==`resolved`||r.selection.lockState!==`unlocked`||r.selection.walletId!==n||r.authMethod.status!==`active`||r.authMethod.walletId!==n||r.authMethod.walletAuthorityId!==r.authority.authorityId||r.authority.state!==`active`||r.authority.walletId!==n)throw Error(`Unlock this wallet with a passkey or Email OTP before activating VoiceID.`);let i=await R.readExactWithOperationCredential({walletId:n,authorityId:r.authority.authorityId,authMethodId:r.authMethod.walletAuthMethodId});if(i.kind!==`found`||i.record.walletId!==n||i.record.authMethodId!==r.authMethod.walletAuthMethodId||i.record.authorityId!==r.authority.authorityId||i.record.expiresAtMs<=Date.now())throw Error(`VoiceID activation requires an active owner Wallet Session.`);let o=await u.listWalletAuthMethodsV2ForWallet(n);for(let e of o){if(e.kind!==`voiceid`||e.status!==`active`||e.walletAuthorityId!==r.authority.authorityId)continue;let i=await a(n,e.walletAuthMethodId);if(i&&i.deviceKeyId===e.deviceKeyId)return{kind:`enroll_existing`,walletId:n,walletAuthMethodId:e.walletAuthMethodId,context:t}}if(r.authMethod.kind===`voiceid`)throw Error(`An existing local VoiceID enrollment is required for management.`);let s=new Y(u),c=await s.listPending(n);if(c.length>1)throw Error(`More than one VoiceID activation is pending for this wallet.`);let l;if(c.length===1)l={kind:`resume`,pending:c[0]};else{let e=j(n);if(!e||e.walletSessionId!==i.operationCredential.walletSessionId)throw Error(`Unlock the wallet again to make its custody seed available for VoiceID.`);let r=T(P(`wallet-custody-envelope`,24));if(!r.ok)throw Error(`VoiceID envelope ID generation failed.`);let a=L(t.signingEngine.getSignerWorkerContext());l={kind:`new`,walletId:n,walletAuthMethodId:f(crypto.randomUUID()),envelopeId:r.value,capability:e,reseal:ee.bind(null,a)}}return{kind:`activate`,ownerCredential:i.operationCredential,plan:l,http:new e(t.configs.network.relayer.url,location.origin,b),store:s}}async function ne(e,t){t.throwIfAborted();let{context:n,walletId:r}=e,i=await te(n,r);return t.throwIfAborted(),i.kind===`enroll_existing`?{kind:`management`,action:re.bind(null,i)}:{kind:`recording`,action:ie.bind(null,i),expiresAtMs:null}}async function re(e,t){let n=await c(e.walletId,e.walletAuthMethodId);return await Z(e.context,n,t),t.throwIfAborted(),{kind:`activated`,walletId:e.walletId,walletAuthMethodId:e.walletAuthMethodId}}async function ie(e,t,n,i){let a=await U({plan:e.plan,ownerCredential:e.ownerCredential,http:e.http,signal:t,onCapture:n,phrase:i,capture:r,persistPending:e.store.persistPending.bind(e.store),persistActive:e.store.persistActive.bind(e.store)});return{kind:`activated`,walletId:a.authMethod.walletId,walletAuthMethodId:a.authMethod.walletAuthMethodId}}export{ne as prepareActivationSurface};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as e,c as t,f as n,t as r,u as i}from"./prompt-DEkI70F2.js";import{f as a,n as o,s}from"./enrollment-D8oQnM_e.js";import{ActionType as c,IndexedDBManager as l,TransactionReviewError as u,isPlainObject as d,parseTransactionReviewWire as f,resolveNearNetwork as p,walletForHostRequest as m,walletHostExecutionFingerprint as h}from"../voiceid-host.js";function g(e){if(e===void 0)return{kind:`random`};if(!d(e)||Object.keys(e).length!==1||typeof e.confirmationCommand!=`string`||e.confirmationCommand.length>256)throw _();let t=e.confirmationCommand.normalize(`NFC`).trim();if(!t||/[\u0000-\u001f\u007f]/.test(t))throw _();switch(/^[a-z]+\b/i.exec(t)?.[0].toLowerCase()){case`sell`:return{kind:`action`,action:`sale`};case`buy`:return{kind:`action`,action:`purchase`};case`swap`:return{kind:`action`,action:`swap`};case`send`:case`transfer`:return{kind:`action`,action:`transfer`};default:return{kind:`random`}}}function _(){return new u(`review_invalid_input`,`voiceId requires only a nonempty confirmationCommand of at most 256 characters without control characters.`)}function v(e,t){return e.attemptId===t.attemptId&&e.operationHash===t.operationHash&&e.expiresAtMs===t.expiresAtMs}async function y(n,r,a,s,c,l){let u=Math.min(r.expiresAtMs,Date.now()+a);if(s.aborted||u<=Date.now())throw Error(`VoiceID review expired or was cancelled.`);let d=await o({kind:`active`,scope:n},s,c,l);if(s.aborted||u<=Date.now())throw Error(`VoiceID review expired or was cancelled.`);switch(d.kind){case`matched`:return{kind:`approved`};case`insufficient_audio`:return{kind:`retry`,message:t(d.quality)};case`mismatch`:return{kind:`retry`,message:new e(d.quality).message};case`phrase_mismatch`:return{kind:`retry`,message:new i().message};default:return b(d)}}function b(e){throw Error(`Unexpected voice result: ${String(e)}`)}function x(e){throw Error(`Unexpected voice outcome: ${String(e)}`)}function S(e){if(!(!d(e)&&!Array.isArray(e))){for(let t of Object.values(e))S(t);Object.freeze(e)}}async function C(e,t){switch(e.type){case`PM_SIGN_TX_WITH_ACTIONS`:case`PM_SIGN_DELEGATE_ACTION`:case`PM_SIGN_NEP413`:throw D();case`PM_SIGN_TEMPO`:{let n=e.payload;if(!n||n.operationKind!==`evm_transaction`)throw D();let{tx:r}=n.request,i=n.chainTarget;if(!t.config.network.chains.some(w)||i.kind!==`evm`||i.namespace!==`eip155`||i.networkSlug!==`arc-testnet`||i.chainId!==5042002||n.request.chain!==`evm`||n.request.kind!==`eip1559`||n.request.senderSignatureAlgorithm!==`secp256k1`||r.chainId!==i.chainId||typeof r.to!=`string`||!/^0x[0-9a-fA-F]{40}$/.test(r.to)||r.data!==void 0&&r.data!==`0x`||r.accessList!==void 0&&r.accessList.length!==0||r.abi!==void 0)throw D();return await E(e,t,g(n.options?.voiceId))}case`PM_SIGN_AND_SEND_TX`:{if(p(t.config.network.chains)!==`testnet`)throw new u(`review_unsupported_mode`,`This VoiceID prototype requires NEAR testnet.`);let n=e.payload;if(!n||n.transaction.actions.length!==1||n.transaction.actions[0].type!==c.Transfer||!(`transactionReview`in e))throw D();return await E(e,t,g(n.options.voiceId))}default:return{kind:`disabled`}}}function w(e){return e.network===`arc-testnet`&&e.chainId===5042002}function T(e,t){return typeof t==`bigint`?{bigint:t.toString()}:t}async function E(e,t,n){let r=f(e.transactionReview);if(r.validity.kind!==`expires_at`)throw new u(`review_invalid_input`,`VoiceID requires a review deadline.`);S(e);let i=new TextEncoder().encode(JSON.stringify({domain:`seams-voiceid-operation-v1`,origin:location.origin,executionFingerprint:h(t.config),request:e},T)),a=await crypto.subtle.digest(`SHA-256`,i),o=``;for(let e of new Uint8Array(a))o+=e.toString(16).padStart(2,`0`);let s=m(e,null);if(!s)throw Error(`VoiceID review requires a wallet identity.`);return{kind:`required`,approval:O.forReview(r,o,s,n)}}function D(){return new u(`review_unsupported_mode`,`VoiceID requires a reviewed NEAR or Arc testnet transfer.`)}var O=class e{phrase;prompt;surface;get messages(){return{confirm:`Confirm with VoiceID`,approved:`Voice approved this operation.`}}state={kind:`pending`};constructor(e,t,i,a){this.reviewExpiresAtMs=e,this.operationHash=t,this.walletId=i,this.confirmation=a,this.phrase=n(a,null),this.prompt=new r(document,`Approve transfer with VoiceID`,this.phrase,`segments`),this.prompt.record.hidden=!0,this.prompt.cancel.hidden=!0,this.surface=this.prompt.element}static forReview(t,n,r,i){if(t.validity.kind!==`expires_at`||!/^[0-9a-f]{64}$/.test(n))throw new u(`review_invalid_input`,`VoiceID requires a bounded operation.`);return new e(t.validity.atMs,n,r,i)}async approve(e){if(this.state.kind===`approved`){if(e.aborted||performance.now()>=this.state.deadline||Date.now()>=this.state.binding.expiresAtMs)throw Error(`Voice approval expired or closed.`);return{kind:`approved`}}if(this.state.kind!==`pending`)throw Error(`Voice approval already started.`);if(!navigator.userActivation.isActive||document.visibilityState!==`visible`||!document.hasFocus())throw Error(`Open the Wallet review and select Confirm to start voice capture.`);this.state={kind:`preparing`},this.prompt.status.textContent=`Loading VoiceID…`;try{let t=await l.resolveSelectedWalletAuthority(this.walletId);if(t.kind!==`resolved`||t.authMethod.kind!==`voiceid`||t.selection.lockState!==`unlocked`)throw Error(`Unlock this wallet with VoiceID before voice approval.`);let r=await s(this.walletId,t.authMethod.walletAuthMethodId);await a(this.prompt.progress),this.prompt.progress.replaceChildren();let i=this.beginAttempt(e);for(;;){let{binding:t,deadline:a,remaining:o}=i,s=await y(r,t,o,e,this.prompt.capture,this.phrase);if(this.assertListening(t),e.aborted||performance.now()>=a||Date.now()>=t.expiresAtMs)throw Error(`Voice approval expired or closed.`);switch(s.kind){case`approved`:return this.state={kind:`approved`,binding:t,deadline:a,signal:e},s;case`retry`:this.phrase=n(this.confirmation,this.phrase),this.prompt.setPhrase(this.phrase),this.prompt.record.textContent=`Try again`,this.prompt.ready(s.message),await this.waitForRetry(e),i=this.beginAttempt(e);break;default:return x(s)}}}catch(e){throw this.state={kind:`closed`},e}}beginAttempt(e){let t=Math.min(25e3,this.reviewExpiresAtMs-Date.now());if(t<250||e.aborted)throw Error(`This voice review has expired or closed.`);let n=Object.freeze({attemptId:crypto.randomUUID(),operationHash:this.operationHash,expiresAtMs:Date.now()+t}),r=performance.now()+t;return this.prompt.record.hidden=!0,this.prompt.status.textContent=`Opening microphone…`,this.state={kind:`listening`,binding:n,deadline:r,signal:e},{binding:n,deadline:r,remaining:t}}waitForRetry(e){return new Promise((t,n)=>{let r=this.prompt.record,i=i=>{clearTimeout(s),r.removeEventListener(`click`,a),e.removeEventListener(`abort`,o),i?t():n(Error(`This voice review has expired or closed.`))},a=i.bind(null,!0),o=i.bind(null,!1),s=setTimeout(o,Math.max(0,this.reviewExpiresAtMs-Date.now()));r.addEventListener(`click`,a),e.addEventListener(`abort`,o),e.aborted&&o(),r.focus()})}consume(){let e=this.state;if(this.state={kind:`consumed`},e.kind!==`approved`||e.signal.aborted||performance.now()>=e.deadline||Date.now()>=e.binding.expiresAtMs)throw Error(`A current voice approval is required before signing.`)}assertListening(e){if(this.state.kind!==`listening`||!v(e,this.state.binding))throw Error(`This voice attempt has closed.`)}};export{C as voiceIdPolicyForRequest};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as e,c as t,d as n,g as r,h as i,l as a,m as o,p as s,s as c,t as l,u}from"./prompt-DEkI70F2.js";import{IndexedDBManager as d,SEAMS_WALLET_STORES as f,WorkerDeferred as ee,availabilityKey as te,base64UrlDecode as p,base64UrlEncode as m,buildDelegatedWalletAuthorityV1 as h,buildFullOwnerPermissionsV1 as ne,isFiniteNumber as re,isPlainObject as g,parseStoredAuthMethodAvailability as _,parseVoiceIdDevicePublicKeyB64u as v,parseWalletAuthMethodV2StorageRow as y,parseWalletAuthorityStorageRow as ie,parseWalletSelectionStorageRow as ae,requireRecord as b,sameDelegatedWalletAuthorityV1 as oe,sha256DigestB64u as x,toError as S}from"../voiceid-host.js";const C=`e04dd7a6a1e23fc7b89e47682ea9d0a3946cf7bc59d07922c3db3efc52ae4269`,w=`redimnet-b1:${C}`,T=new i({path:`/sdk/voiceid/redimnet-b1.onnx`,sha256:C,bytes:8859593,label:`Speaker verification`});async function E(e){let t=await T.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 D(Array.from(t.data))}finally{for(let t of Object.values(e))t.dispose()}}finally{n.dispose()}}function D(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 O(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}const k=[T,o,a],A=[r,...k.map(se)];function se(e){return e.asset}function j(e,t){return e+t.size}var M=class{element=document.createElement(`section`);status=document.createElement(`p`);meter=document.createElement(`progress`);reveal;received=new Map;total=A.reduce(j,0);constructor(e){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.element.append(this.status,this.meter),this.status.textContent=`Loading saved VoiceID…`,this.reveal=setTimeout(e.append.bind(e,this.element),300)}settle=()=>{clearTimeout(this.reveal)};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 N(e){let t=new M(e);try{for(let e of A)await e.download(t.update.bind(t,e));t.preparing();for(let e of k)await e.prepare();t.ready()}finally{t.settle()}}function P(e,t){return`voiceid-browser-device-v1:${encodeURIComponent(e)}:${encodeURIComponent(t)}`}function F(e,t,n){return new TextEncoder().encode(JSON.stringify([`seams-voiceid-browser-factor-v1`,location.origin,e,t,n]))}async function I(e,t){let n=await d.getAppState(P(e,t));if(n===void 0)return null;let r=b(n,`VoiceID device record`);if(r.kind!==`browser_voiceid_device_v1`||r.walletId!==e||r.walletAuthMethodId!==t)throw Error(`VoiceID device belongs to another wallet method.`);let{privateKey:i,wrappingKey:a,nonce:o,ciphertext:s}=r;if(!(i instanceof CryptoKey)||i.type!==`private`||i.extractable||i.algorithm.name!==`Ed25519`||!i.usages.includes(`sign`)||!(a instanceof CryptoKey)||a.type!==`secret`||a.extractable||a.algorithm.name!==`AES-GCM`||!a.usages.includes(`decrypt`)||!(o instanceof Uint8Array)||o.length!==12||!(s instanceof ArrayBuffer)||s.byteLength!==48)throw Error(`VoiceID device storage is invalid.`);let c=v(r.publicKeyB64u),l=await x(p(c));if(r.deviceKeyId!==l)throw Error(`VoiceID device key binding is invalid.`);return{kind:`browser_voiceid_device_v1`,walletId:e,walletAuthMethodId:t,privateKey:i,wrappingKey:a,nonce:new Uint8Array(o),ciphertext:s,publicKeyB64u:c,deviceKeyId:l}}async function ce(e,t){return await navigator.locks.request(P(e,t),le.bind(null,e,t))}async function le(e,t){let n=await I(e,t);if(n)return n;let r=await crypto.subtle.generateKey({name:`Ed25519`},!1,[`sign`,`verify`]),i=v(m(new Uint8Array(await crypto.subtle.exportKey(`raw`,r.publicKey)))),a=await x(p(i)),o=await crypto.subtle.generateKey({name:`AES-GCM`,length:256},!1,[`encrypt`,`decrypt`]),s=crypto.getRandomValues(new Uint8Array(32)),c=crypto.getRandomValues(new Uint8Array(12));try{let n=await crypto.subtle.encrypt({name:`AES-GCM`,iv:c,additionalData:F(e,t,a)},o,s),l={kind:`browser_voiceid_device_v1`,walletId:e,walletAuthMethodId:t,privateKey:r.privateKey,publicKeyB64u:i,deviceKeyId:a,wrappingKey:o,nonce:c,ciphertext:n};return await d.setAppState(P(e,t),l),l}finally{s.fill(0)}}async function L(e){let t=await crypto.subtle.decrypt({name:`AES-GCM`,iv:e.nonce,additionalData:F(e.walletId,e.walletAuthMethodId,e.deviceKeyId)},e.wrappingKey,e.ciphertext);return new Uint8Array(t)}const R=`voiceid-browser-enrollment-v1`,z=.2,B=[f.appState,f.walletSelections,f.walletAuthMethods,f.walletAuthorities];function V(e,t){return{walletId:e.walletId,walletAuthMethodId:e.walletAuthMethodId,deviceKeyId:e.deviceKeyId,authorityId:t}}async function H(e,t){let n=await d.getWalletAuthMethodV2(t),r=await I(e,t);if(!n||n.kind!==`voiceid`||n.status!==`active`||n.walletId!==e||!r||n.deviceKeyId!==r.deviceKeyId)throw Error(`VoiceID is unavailable for this wallet method.`);return V(r,n.walletAuthorityId)}function U(e){return`voiceid-browser-enrollment-v2:${[e.walletId,e.authorityId,e.walletAuthMethodId,e.deviceKeyId].map(encodeURIComponent).join(`:`)}`}function W(e){if(!Array.isArray(e)||!e.every(re))throw Error(`VoiceID template is invalid.`);let t=D(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 G(e,t){if(e===void 0)return null;let n=b(e,`VoiceID enrollment`),r=b(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!==w)return null;let i=W(n.baseline),a={kind:`baseline`};try{let e=b(n.adaptation,`VoiceID improvements`);if(e.kind===`adapted`&&Array.isArray(e.samples)&&e.samples.length===3){let t=e.samples.map(W);q(i,t),a={kind:`adapted`,samples:t}}}catch{a={kind:`baseline`}}return{kind:`browser_voiceid_enrollment_v2`,scope:t,model:w,generation:n.generation,revision:n.revision,baseline:i,adaptation:a}}function K(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=D(n);for(let t=0;t<n.length;t+=1)n[t]=(1-z)*e[t]+z*r[t];return D(n)}function q(e,t){if(t.length!==3)throw Error(`Record three samples before saving voice improvements.`);for(let n of t)if(O(e,n)<.5)throw Error(`This sample differs from your original voice. Try again or re-record your enrollment.`);if(O(e,K(e,t))<.98)throw Error(`These samples differ too much from your original voice. Re-record your enrollment instead.`)}async function ue(e){let t=(await d.seamsWalletDB.getDB()).transaction(f.appState,`readwrite`);try{let n=await t.store.get(U(e)),r=G(n?.value,e);return r&&JSON.stringify(n?.value?.adaptation)!==JSON.stringify(r.adaptation)&&(r=Z(r,r.adaptation),await t.store.put({key:U(e),value:r})),await t.done,r}catch(e){try{t.abort()}catch{}throw await t.done.catch(X),e}}async function de(){let e=await d.getAppState(R);if(e===void 0)return null;let t=b(e,`Previous VoiceID enrollment`);return t.kind!==`browser_voiceid_enrollment_v1`||t.model!==w?null:W(t.template)}async function J(e,t,n){let r=ae(await e.objectStore(f.walletSelections).get(t.walletId)),i=ie(await e.objectStore(f.walletAuthorities).get(t.authorityId)),a=await e.objectStore(f.walletAuthMethods).get(t.walletAuthMethodId),o=y(a);if(a!==void 0&&!o)throw Error(`VoiceID method storage is invalid.`);if(!i||i.record.state!==`active`||i.record.walletId!==t.walletId)throw Error(`VoiceID authority changed. Start again.`);if(o&&(o.record.kind!==`voiceid`||o.record.status!==`active`||o.record.walletId!==t.walletId||o.record.walletAuthorityId!==t.authorityId||o.record.deviceKeyId!==t.deviceKeyId)||(n===null||n.target===`enrolled`)&&!o)throw Error(`VoiceID method is unavailable.`);if((await e.objectStore(f.appState).get(P(t.walletId,t.walletAuthMethodId)))?.value?.deviceKeyId!==t.deviceKeyId)throw Error(`VoiceID device changed. Start again.`);let s=_((await e.objectStore(f.appState).get(te(t.walletId,t.walletAuthMethodId)))?.value);if(!s.enabled)throw Error(`VoiceID is turned off.`);if(n){if(!oe(h({permissions:i.record.permissions}),h({permissions:ne()})))throw Error(`Full owner authority is required to change VoiceID enrollment.`);let a=y(await e.objectStore(f.walletAuthMethods).get(n.sessionMethodId));if(n.expiresAtMs<=Date.now()||s.revision!==n.availabilityRevision||!r||r.record.lockState!==`unlocked`||r.record.lockGeneration!==n.lockGeneration||r.record.walletAuthMethodId!==n.sessionMethodId||!a||a.record.status!==`active`||n.target===`activating`&&a.record.kind===`voiceid`||a.record.walletId!==t.walletId||a.record.walletAuthorityId!==t.authorityId)throw Error(`The owner session changed or expired. Start again.`);if(n.target===`enrolled`){let r=y(await e.objectStore(f.walletAuthMethods).get(n.confirmationMethodId));if(!r||r.record.kind!==`passkey`||r.record.status!==`active`||r.record.walletId!==t.walletId||r.record.walletAuthorityId!==t.authorityId)throw Error(`The confirming passkey changed or was removed. Start again.`)}}}async function Y(e,t){let n=(await d.seamsWalletDB.getDB()).transaction(B,`readwrite`);try{await J(n,e,t),await n.done}catch(e){throw $(n),await n.done.catch(X),e}}function X(){}async function fe(e,t){let n=e.scope,r=e.kind===`activation`?e.owner:null;if(r&&r.target!==`activating`)throw Error(`VoiceID activation requires its owner scope.`);let i=(await d.seamsWalletDB.getDB()).transaction(B,`readwrite`);try{await J(i,n,r);let e=i.objectStore(f.appState),a=await e.get(U(n)),o=G(a?.value,n);if(!o)throw Error(`Unlock with a passkey, then open Manage voice to import or record your enrollment.`);t.throwIfAborted();let s=o.adaptation,c=s.kind===`adapted`?K(o.baseline,s.samples):[...o.baseline];return JSON.stringify(a?.value?.adaptation)!==JSON.stringify(s)&&await e.put({key:U(n),value:Z(o,s)}),await i.done,t.throwIfAborted(),{template:c,baseline:[...o.baseline],kind:s.kind}}catch(e){try{i.abort()}catch{}throw await i.done.catch(X),e}}function Z(e,t){return{kind:`browser_voiceid_enrollment_v2`,scope:e.scope,model:e.model,generation:e.generation,revision:e.revision+1,baseline:e.baseline,adaptation:t}}async function Q(e,t,n,r,i){let a;if(r.kind===`activate`){if(t.target!==`activating`||n.kind!==`missing`)throw Error(`VoiceID activation cannot replace an enrollment.`)}else{if(t.target!==`enrolled`)throw Error(`VoiceID changes require a confirmation guard.`);await r.confirmation.consume(e,t,n,r.kind,i)}if(r.kind===`import`&&(t.target!==`enrolled`||n.kind!==`missing`))throw Error(`Import requires an existing method without a scoped enrollment.`);if(r.kind===`replace`||r.kind===`activate`||r.kind===`import`)a={kind:`browser_voiceid_enrollment_v2`,scope:e,model:w,generation:crypto.randomUUID(),revision:0,baseline:W(r.baseline),adaptation:{kind:`baseline`}};else{if(n.kind!==`present`)throw Error(`Record or import your original voice first.`);r.kind===`improve`?(q(n.enrollment.baseline,r.samples),a=Z(n.enrollment,{kind:`adapted`,samples:r.samples})):a=Z(n.enrollment,{kind:`baseline`})}if(new TextEncoder().encode(JSON.stringify(a)).length>32768)throw Error(`VoiceID enrollment exceeds its storage limit.`);let o=await d.seamsWalletDB.getDB();i.throwIfAborted();let s=o.transaction(B,`readwrite`),c=$.bind(null,s);i.addEventListener(`abort`,c,{once:!0});try{await J(s,e,t);let o=s.objectStore(f.appState),c=G((await o.get(U(e)))?.value,e);if(n.kind===`missing`?c!==null:!c||c.generation!==n.enrollment.generation||c.revision!==n.enrollment.revision)throw Error(`VoiceID enrollment changed in another operation. Start again.`);i.throwIfAborted(),await o.put({key:U(e),value:a}),r.kind===`import`&&await pe(s),await s.done}catch(e){throw c(),await s.done.catch(X),e}finally{i.removeEventListener(`abort`,c)}}function $(e){try{e.abort()}catch{}}async function pe(e){let t=e.objectStore(f.appState),n=await e.objectStore(f.walletAuthMethods).getAll();for(let e of n){if(!g(e)||e.kind!==`voiceid`)continue;let n=y(e);if(!n)return;let r=n.record;if(r.kind!==`voiceid`||r.status!==`active`)continue;let i={walletId:r.walletId,authorityId:r.walletAuthorityId,walletAuthMethodId:r.walletAuthMethodId,deviceKeyId:r.deviceKeyId};try{if(!G((await t.get(U(i)))?.value,i))return}catch{return}}await t.delete(R)}async function me(e,t){let n=document.querySelector(`.seams-voiceid-activation-surface`);if(!(n instanceof HTMLElement))throw Error(`Open VoiceID activation in the Wallet.`);await N(n),t.throwIfAborted();let r=new he(n,e,t);try{await r.result}finally{r.dispose()}}var he=class{completion=new ee;result=this.completion.promise;status;prompt;phrase=s(null);record;cancel;samples=[];phase=`ready`;get closed(){return this.phase===`closed`}controller=new AbortController;guardTimer;checkingGuard=!1;constructor(e,t,n){this.plan=t,this.signal=n,this.prompt=new l(document,`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.`),e.replaceChildren(this.prompt.element),n.addEventListener(`abort`,this.cancelled,{once:!0}),this.guardTimer=setInterval(this.checkGuard,250),n.aborted?this.cancelled():this.record.focus()}capture=()=>{this.collect()};async collect(){if(this.phase!==`ready`)return;let r=this.samples.length+1;try{c(),this.phase=`recording`,this.record.disabled=!0,this.status.textContent=`Opening microphone…`;let r=await e(this.controller.signal,this.prompt.capture);if(this.signal.aborted||this.closed){r.kind===`captured`&&r.samples.fill(0);return}if(r.kind===`insufficient_audio`){this.status.textContent=t(r.quality);return}if(this.plan.kind===`improve`&&r.quality.noise!==`normal`){r.samples.fill(0),this.status.textContent=`Use a quiet place to improve your voice enrollment.`;return}let i;try{if(await Y(this.plan.scope,this.plan.guard),this.status.textContent=`Checking your voice…`,(await n(r.samples,this.phrase)).kind===`mismatch`)throw new u;i=await E(r.samples)}finally{r.samples.fill(0)}if(this.signal.aborted||this.closed){i.fill(0);return}if(this.plan.kind===`improve`&&O(this.plan.snapshot.enrollment.baseline,i)<.5)throw i.fill(0),Error(`Your voice did not match the original enrollment. To replace it, return to Manage voice and choose Record voice again.`);let a=1;for(let e of this.samples)a=Math.min(a,O(e,i));this.status.dataset.voiceCapture=JSON.stringify({attemptId:r.quality.attemptId,capturedAtMs:r.quality.capturedAtMs,sample:this.samples.length+1,durationMs:r.quality.durationMs,recordingMs:r.quality.recordingMs,endedBy:r.quality.endedBy,voicedMs:r.quality.voicedMs,microphone:r.quality.microphone,processing:r.quality.processing,backgroundRms:r.quality.backgroundRms,speechToBackgroundDb:r.quality.speechToBackgroundDb,noise:r.quality.noise,minimumSimilarity:a});for(let e of this.samples)if(O(e,i)<.5)throw i.fill(0),Error(`This sample differs from the earlier samples. Use the same microphone and try again.`);if(this.samples.push(i),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 Q(s.scope,s.guard,s.snapshot,{kind:`activate`,baseline:D(o)},this.controller.signal);break;case`replace`:await Q(s.scope,s.guard,s.snapshot,{kind:`replace`,baseline:D(o),confirmation:s.confirmation},this.controller.signal);break;case`improve`:await Q(s.scope,s.guard,s.snapshot,{kind:`improve`,samples:this.samples,confirmation:s.confirmation},this.controller.signal);break}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(S(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<r&&(this.record.textContent=`Retry sample ${r}`,this.status.textContent=`Sample ${r} was not accepted. ${this.status.textContent??``}`),this.phrase=s(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.controller.abort(),this.completion.reject(Error(`VoiceID enrollment was cancelled.`)))};checkGuard=()=>{this.closed||this.checkingGuard||(this.checkingGuard=!0,this.validateGuard())};async validateGuard(){try{await Y(this.plan.scope,this.plan.guard)}catch(e){this.closed||(this.phase=`closed`,this.controller.abort(),this.completion.reject(S(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)}};async function ge(t,r,i,a){delete document.body.dataset.voiceVerification;let o=await e(r,i);if(o.kind===`insufficient_audio`)return r.throwIfAborted(),o;try{r.throwIfAborted();let e=await n(o.samples,a),i=e.kind===`matched`,s=o.samples.length/16,c=await E(o.samples),l,u,d;try{r.throwIfAborted();let e=await fe(t,r);try{l=O(c,e.template),u=O(c,e.baseline),d=e.kind}finally{e.template.fill(0),e.baseline.fill(0)}}finally{c.fill(0)}return document.body.dataset.voiceVerification=JSON.stringify({attemptId:o.quality.attemptId,capturedAtMs:o.quality.capturedAtMs,durationMs:o.quality.durationMs,recordingMs:o.quality.recordingMs,endedBy:o.quality.endedBy,voicedMs:o.quality.voicedMs,microphone:o.quality.microphone,processing:o.quality.processing,backgroundRms:o.quality.backgroundRms,speechToBackgroundDb:o.quality.speechToBackgroundDb,noise:o.quality.noise,similarity:l,baselineSimilarity:u,referenceKind:d,modelInputMs:s,threshold:.5,phraseMatched:i,phraseRecognition:e.recognition,expectedPhrase:a}),l<.5?{kind:`mismatch`,quality:o.quality}:e.kind===`matched`?{kind:`matched`}:{kind:`phrase_mismatch`}}finally{o.samples.fill(0)}}export{de as a,V as c,I as d,N as f,Q as i,ce as l,ge as n,ue as o,Y as r,H as s,me as t,L as u};
|