@seams/voiceid 0.0.0-stage → 0.25.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.
Files changed (66) hide show
  1. package/LICENSE.md +201 -0
  2. package/README.md +104 -2
  3. package/THIRD_PARTY_NOTICES.md +504 -0
  4. package/dist/compose-host.mjs +11 -0
  5. package/dist/esm/browser/ribbon.js +489 -0
  6. package/dist/esm/capture/prompt.js +516 -0
  7. package/dist/esm/capture/visual.js +87 -0
  8. package/dist/esm/descriptor.js +7 -0
  9. package/dist/esm/index.js +2 -0
  10. package/dist/esm/react/VoiceAssistantCard.css +313 -0
  11. package/dist/esm/react/VoiceAssistantCard.js +147 -0
  12. package/dist/esm/react.js +3 -0
  13. package/dist/hosting.d.mts +4 -0
  14. package/dist/hosting.mjs +91 -0
  15. package/dist/public/sdk/voiceid/AURORA_LICENSE.txt +202 -0
  16. package/dist/public/sdk/voiceid/NOTICE.txt +54 -0
  17. package/dist/public/sdk/voiceid/SILERO_LICENSE.txt +21 -0
  18. package/dist/public/sdk/voiceid/addVoiceId-Bz9BDPI4.js +1 -0
  19. package/dist/public/sdk/voiceid/approval-CME3DxNc.js +1 -0
  20. package/dist/public/sdk/voiceid/aurora-int8.onnx +0 -0
  21. package/dist/public/sdk/voiceid/deviceBridge-DDviNxL6.js +1 -0
  22. package/dist/public/sdk/voiceid/enrollment-D8oQnM_e.js +1 -0
  23. package/dist/public/sdk/voiceid/host.js +80 -0
  24. package/dist/public/sdk/voiceid/ort-wasm-simd-threaded.mjs +59 -0
  25. package/dist/public/sdk/voiceid/ort-wasm-simd-threaded.wasm +0 -0
  26. package/dist/public/sdk/voiceid/ort.wasm.bundle.min-7y-hlRN8.js +3 -0
  27. package/dist/public/sdk/voiceid/prompt-DEkI70F2.js +606 -0
  28. package/dist/public/sdk/voiceid/redimnet-b1.onnx +0 -0
  29. package/dist/public/sdk/voiceid/silero-vad.onnx +0 -0
  30. package/dist/public/sdk/voiceid/unlock-CGnhhdt4.js +1 -0
  31. package/dist/public/sdk/voiceid/voice-activity-worklet.js +21 -0
  32. package/dist/types/activation/addVoiceId.d.ts +11 -0
  33. package/dist/types/activation/manageEnrollment.d.ts +4 -0
  34. package/dist/types/activation/ownerConfirmation.d.ts +17 -0
  35. package/dist/types/browser/confirmationCommand.d.ts +11 -0
  36. package/dist/types/browser/deviceStore.d.ts +19 -0
  37. package/dist/types/browser/enrollment.d.ts +37 -0
  38. package/dist/types/browser/enrollmentStore.d.ts +91 -0
  39. package/dist/types/browser/inference.d.ts +39 -0
  40. package/dist/types/browser/microphone.d.ts +61 -0
  41. package/dist/types/browser/model.d.ts +8 -0
  42. package/dist/types/browser/phrase.d.ts +37 -0
  43. package/dist/types/browser/phraseVocabulary.d.ts +10 -0
  44. package/dist/types/browser/progress.d.ts +2 -0
  45. package/dist/types/browser/ribbon.d.ts +63 -0
  46. package/dist/types/browser/verificationError.d.ts +6 -0
  47. package/dist/types/browser/voiceActivity.d.ts +15 -0
  48. package/dist/types/capture/approval.d.ts +38 -0
  49. package/dist/types/capture/bridge.d.ts +12 -0
  50. package/dist/types/capture/deviceBridge.d.ts +29 -0
  51. package/dist/types/capture/prompt.d.ts +41 -0
  52. package/dist/types/capture/surface.d.ts +27 -0
  53. package/dist/types/capture/unlockFrame.d.ts +13 -0
  54. package/dist/types/capture/visual.d.ts +24 -0
  55. package/dist/types/descriptor.d.ts +4 -0
  56. package/dist/types/host.d.ts +4 -0
  57. package/dist/types/index.d.ts +2 -0
  58. package/dist/types/react/VoiceAssistantCard.d.ts +37 -0
  59. package/dist/types/react.d.ts +5 -0
  60. package/dist/types/session/activation.d.ts +61 -0
  61. package/dist/types/session/activationStore.d.ts +21 -0
  62. package/dist/types/session/httpClient.d.ts +39 -0
  63. package/dist/types/session/unlock.d.ts +4 -0
  64. package/dist/types/session/unlockFromDevice.d.ts +18 -0
  65. package/package.json +66 -4
  66. package/src/README.md +280 -0
@@ -0,0 +1,13 @@
1
+ import type { VoicePrompt } from './prompt';
2
+ export declare class VoiceUnlockFrame {
3
+ private readonly prompt;
4
+ private readonly footer;
5
+ constructor(element: HTMLElement, prompt: VoicePrompt);
6
+ /** After an unlock the back control cannot act, so it fades and the success centers under the origin. */
7
+ succeed(): Readonly<{
8
+ top: number;
9
+ bottom: number;
10
+ }>;
11
+ dispose(): void;
12
+ }
13
+ //# sourceMappingURL=unlockFrame.d.ts.map
@@ -0,0 +1,24 @@
1
+ import type { VoiceActivityEvent } from '../browser/microphone';
2
+ import { type VoiceCheckingStyle } from '../browser/ribbon';
3
+ /** Shared by enrollment, unlock and signing. The recorder remains the only audio owner. */
4
+ export declare class VoiceVisual {
5
+ readonly element: SVGSVGElement;
6
+ readonly glow: HTMLDivElement;
7
+ private readonly ribbon;
8
+ private readonly prompt;
9
+ private readonly themeObserver;
10
+ private phase;
11
+ private disposed;
12
+ constructor(document: Document, prompt: HTMLElement, checking: VoiceCheckingStyle);
13
+ listen(): void;
14
+ add(event: VoiceActivityEvent): void;
15
+ checking(): void;
16
+ accept(): void;
17
+ ready(): void;
18
+ /** The glow settles while the ribbon closes into the success seal; resolves when the check is drawn. */
19
+ succeed(begin: () => void): Promise<void>;
20
+ stop(): void;
21
+ private setPhase;
22
+ private readonly render;
23
+ }
24
+ //# sourceMappingURL=visual.d.ts.map
@@ -0,0 +1,4 @@
1
+ import type { VoiceIdAuthMethodPlugin } from '@seams/wallet/voiceid-host';
2
+ /** A small opt-in descriptor with no capture, model, or host implementation. */
3
+ export declare function voiceIdAuthMethod(): VoiceIdAuthMethodPlugin;
4
+ //# sourceMappingURL=descriptor.d.ts.map
@@ -0,0 +1,4 @@
1
+ import type { WalletAuthMethodHostPlugin } from '@seams/wallet/voiceid-host';
2
+ export declare const hostApiVersion = 1;
3
+ export declare const voiceIdHostPlugin: WalletAuthMethodHostPlugin;
4
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { voiceIdAuthMethod } from './descriptor';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { type VoiceBeamLevel } from 'voice-glow';
3
+ export type VoiceAssistantInput = {
4
+ readonly kind: 'level';
5
+ /** 0–1, or a getter sampled by the glow without React renders. */
6
+ readonly level: VoiceBeamLevel;
7
+ readonly stream?: never;
8
+ } | {
9
+ readonly kind: 'microphone';
10
+ readonly stream: MediaStream;
11
+ readonly level?: never;
12
+ };
13
+ export type VoiceAssistantState = {
14
+ readonly kind: 'idle';
15
+ readonly input?: never;
16
+ } | {
17
+ readonly kind: 'listening';
18
+ readonly input: VoiceAssistantInput;
19
+ } | {
20
+ readonly kind: 'processing';
21
+ readonly input?: never;
22
+ };
23
+ export type VoiceAssistantCardProps = {
24
+ readonly name: string;
25
+ readonly initials: string;
26
+ readonly description: string;
27
+ readonly message: string;
28
+ readonly transcript: React.ReactNode;
29
+ readonly state: VoiceAssistantState;
30
+ readonly theme: 'dark' | 'light';
31
+ readonly onToggle: () => void;
32
+ readonly className?: string;
33
+ readonly style?: React.CSSProperties;
34
+ };
35
+ /** Controlled presentation. The caller owns audio, transcripts, and work. */
36
+ export declare function VoiceAssistantCard(props: VoiceAssistantCardProps): React.ReactNode;
37
+ //# sourceMappingURL=VoiceAssistantCard.d.ts.map
@@ -0,0 +1,5 @@
1
+ export { VoiceBeam } from 'voice-glow';
2
+ export type { VoiceBeamLevel, VoiceBeamProps } from 'voice-glow';
3
+ export { VoiceAssistantCard } from './react/VoiceAssistantCard';
4
+ export type { VoiceAssistantCardProps, VoiceAssistantInput, VoiceAssistantState, } from './react/VoiceAssistantCard';
5
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1,61 @@
1
+ import type { VoiceCaptureObserver } from '../browser/microphone';
2
+ import type { SpokenPhrase } from '../browser/phrase';
3
+ import { type VoiceIdSeedEnvelope } from '@seams/wallet/voiceid-host';
4
+ import { type DigestB64u } from '@seams/wallet/voiceid-host';
5
+ import type { WalletSessionOperationCredentialV1 } from '@seams/wallet/voiceid-host';
6
+ import { type PasskeyEnvelopeId, type WalletAuthMethodId, type WalletId } from '@seams/wallet/voiceid-host';
7
+ import { type VoiceIdDevicePublicKeyB64u } from '@seams/wallet/voiceid-host';
8
+ import type { ActiveVoiceIdWalletAuthMethodRecordV2 } from '@seams/wallet/voiceid-host';
9
+ import type { UnlockedWalletEd25519ExportRootCapabilityV1 } from '@seams/wallet/voiceid-host';
10
+ import { captureVoiceIdActivation } from '../capture/deviceBridge';
11
+ import type { VoiceIdHttpClient } from './httpClient';
12
+ export type PendingVoiceIdActivation = Readonly<{
13
+ kind: 'pending_voiceid_activation_v1';
14
+ walletId: WalletId;
15
+ walletAuthMethodId: WalletAuthMethodId;
16
+ devicePublicKeyB64u: VoiceIdDevicePublicKeyB64u;
17
+ deviceKeyId: DigestB64u;
18
+ custodyEnvelope: VoiceIdSeedEnvelope;
19
+ }>;
20
+ export declare function parsePendingVoiceIdActivation(raw: unknown): Promise<PendingVoiceIdActivation>;
21
+ type NewVoiceIdActivation = Readonly<{
22
+ kind: 'new';
23
+ walletId: WalletId;
24
+ walletAuthMethodId: WalletAuthMethodId;
25
+ envelopeId: PasskeyEnvelopeId;
26
+ capability: UnlockedWalletEd25519ExportRootCapabilityV1;
27
+ reseal: VoiceIdSeedResealPort;
28
+ pending?: never;
29
+ }>;
30
+ type ResumeVoiceIdActivation = Readonly<{
31
+ kind: 'resume';
32
+ pending: PendingVoiceIdActivation;
33
+ walletId?: never;
34
+ walletAuthMethodId?: never;
35
+ envelopeId?: never;
36
+ capability?: never;
37
+ reseal?: never;
38
+ }>;
39
+ export type VoiceIdSeedResealPort = (input: {
40
+ readonly capability: UnlockedWalletEd25519ExportRootCapabilityV1;
41
+ readonly bindingJson: string;
42
+ readonly factorSecret: ArrayBuffer;
43
+ }) => Promise<unknown>;
44
+ export type VoiceIdActivationPlan = NewVoiceIdActivation | ResumeVoiceIdActivation;
45
+ export type VoiceIdActivationOutcome = Readonly<{
46
+ authMethod: ActiveVoiceIdWalletAuthMethodRecordV2;
47
+ pending: PendingVoiceIdActivation;
48
+ }>;
49
+ export declare function activateVoiceIdForUnlockedWallet(input: {
50
+ readonly plan: VoiceIdActivationPlan;
51
+ readonly ownerCredential: WalletSessionOperationCredentialV1;
52
+ readonly http: Pick<VoiceIdHttpClient, 'activate'>;
53
+ readonly signal: AbortSignal;
54
+ readonly onCapture: VoiceCaptureObserver;
55
+ readonly phrase: SpokenPhrase;
56
+ readonly capture: typeof captureVoiceIdActivation;
57
+ readonly persistPending: (pending: PendingVoiceIdActivation) => Promise<void>;
58
+ readonly persistActive: (authMethod: ActiveVoiceIdWalletAuthMethodRecordV2, pending: PendingVoiceIdActivation) => Promise<void>;
59
+ }): Promise<VoiceIdActivationOutcome>;
60
+ export {};
61
+ //# sourceMappingURL=activation.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { type ActiveVoiceIdWalletAuthMethodRecordV2 } from '@seams/wallet/voiceid-host';
2
+ import type { WalletAuthMethodId, WalletId } from '@seams/wallet/voiceid-host';
3
+ import { type PendingVoiceIdActivation } from './activation';
4
+ export type VoiceIdActivationStoragePort = {
5
+ getAppState(key: string): Promise<unknown | undefined>;
6
+ setAppState(key: string, value: unknown): Promise<void>;
7
+ listAppStateEntriesByPrefix(prefix: string): Promise<ReadonlyArray<{
8
+ key: string;
9
+ value: unknown;
10
+ }>>;
11
+ upsertWalletAuthMethodV2(record: ActiveVoiceIdWalletAuthMethodRecordV2): Promise<void>;
12
+ };
13
+ export declare class VoiceIdActivationStore {
14
+ private readonly storage;
15
+ constructor(storage: VoiceIdActivationStoragePort);
16
+ persistPending(pending: PendingVoiceIdActivation): Promise<void>;
17
+ persistActive(authMethod: ActiveVoiceIdWalletAuthMethodRecordV2, pending: PendingVoiceIdActivation): Promise<void>;
18
+ readPending(walletId: WalletId, walletAuthMethodId: WalletAuthMethodId): Promise<PendingVoiceIdActivation | null>;
19
+ listPending(walletId: WalletId): Promise<readonly PendingVoiceIdActivation[]>;
20
+ }
21
+ //# sourceMappingURL=activationStore.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { type VoiceIdDeviceAssertion, type VoiceIdDevicePublicKeyB64u, type VoiceIdWalletSessionChallenge } from '@seams/wallet/voiceid-host';
2
+ import { type VoiceIdSeedEnvelope } from '@seams/wallet/voiceid-host';
3
+ import type { ActiveWalletSessionV1, WalletSessionOperationCredentialV1 } from '@seams/wallet/voiceid-host';
4
+ import type { DigestB64u } from '@seams/wallet/voiceid-host';
5
+ import type { WalletAuthMethodId, WalletId } from '@seams/wallet/voiceid-host';
6
+ import { type ActiveVoiceIdWalletAuthMethodRecordV2 } from '@seams/wallet/voiceid-host';
7
+ import type { AuthMethodNearInstallation } from '@seams/wallet/voiceid-host';
8
+ export type VoiceIdHttpSend = (request: Request) => Promise<Response>;
9
+ export type VoiceIdVerifiedSession = Readonly<{
10
+ near: AuthMethodNearInstallation;
11
+ walletSession: ActiveWalletSessionV1;
12
+ operationCredential: WalletSessionOperationCredentialV1;
13
+ custodyEnvelope: VoiceIdSeedEnvelope;
14
+ }>;
15
+ export declare class VoiceIdHttpClient {
16
+ private readonly relayerUrl;
17
+ private readonly send;
18
+ private readonly walletOrigin;
19
+ constructor(relayerUrl: string, walletOrigin: string, send: VoiceIdHttpSend);
20
+ activate(input: {
21
+ readonly ownerCredential: WalletSessionOperationCredentialV1;
22
+ readonly walletId: WalletId;
23
+ readonly walletAuthMethodId: WalletAuthMethodId;
24
+ readonly devicePublicKeyB64u: VoiceIdDevicePublicKeyB64u;
25
+ readonly deviceKeyId: DigestB64u;
26
+ readonly custodyEnvelope: VoiceIdSeedEnvelope;
27
+ readonly signal: AbortSignal;
28
+ }): Promise<ActiveVoiceIdWalletAuthMethodRecordV2>;
29
+ challenge(input: {
30
+ readonly walletId: WalletId;
31
+ readonly walletAuthMethodId: WalletAuthMethodId;
32
+ readonly signal: AbortSignal;
33
+ }): Promise<VoiceIdWalletSessionChallenge>;
34
+ verify(input: {
35
+ readonly assertion: VoiceIdDeviceAssertion;
36
+ readonly signal: AbortSignal;
37
+ }): Promise<VoiceIdVerifiedSession>;
38
+ }
39
+ //# sourceMappingURL=httpClient.d.ts.map
@@ -0,0 +1,4 @@
1
+ import type { AuthMethodUnlockSuccess, WalletAuthMethodHostPlugin } from '@seams/wallet/voiceid-host';
2
+ import type { PreparedVoiceCapture } from '../capture/surface';
3
+ export declare function prepareUnlockSurface(input: Parameters<WalletAuthMethodHostPlugin['unlock']>[0], signal: AbortSignal, parent: HTMLElement): Promise<PreparedVoiceCapture<AuthMethodUnlockSuccess>>;
4
+ //# sourceMappingURL=unlock.d.ts.map
@@ -0,0 +1,18 @@
1
+ import type { VoiceCaptureObserver } from '../browser/microphone';
2
+ import type { SpokenPhrase } from '../browser/phrase';
3
+ import type { VoiceIdWalletSessionChallenge } from '@seams/wallet/voiceid-host';
4
+ import type { ActiveVoiceIdWalletAuthMethodRecordV2 } from '@seams/wallet/voiceid-host';
5
+ import { captureVoiceIdUnlock } from '../capture/deviceBridge';
6
+ import type { VoiceIdHttpClient, VoiceIdVerifiedSession } from './httpClient';
7
+ /** The installer must finish custody activation before the caller reports unlock. */
8
+ export declare function verifyVoiceIdChallengeWithDevice<T>(input: {
9
+ readonly authMethod: ActiveVoiceIdWalletAuthMethodRecordV2;
10
+ readonly challenge: VoiceIdWalletSessionChallenge;
11
+ readonly http: Pick<VoiceIdHttpClient, 'verify'>;
12
+ readonly capture: typeof captureVoiceIdUnlock;
13
+ readonly install: (verified: VoiceIdVerifiedSession, factorSecret32: Uint8Array) => Promise<T>;
14
+ readonly signal: AbortSignal;
15
+ readonly onCapture: VoiceCaptureObserver;
16
+ readonly phrase: SpokenPhrase;
17
+ }): Promise<T>;
18
+ //# sourceMappingURL=unlockFromDevice.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@seams/voiceid",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.25.0",
4
+ "type": "module",
5
+ "description": "Optional browser VoiceID authentication for Seams Wallet",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/seams-tech/seams-wallet.git"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types/index.d.ts",
14
+ "import": "./dist/esm/index.js"
15
+ },
16
+ "./react": {
17
+ "types": "./dist/types/react.d.ts",
18
+ "import": "./dist/esm/react.js"
19
+ },
20
+ "./hosting": {
21
+ "types": "./dist/hosting.d.mts",
22
+ "import": "./dist/hosting.mjs"
23
+ },
24
+ "./package.json": "./package.json",
25
+ "./styles": "./dist/esm/react/VoiceAssistantCard.css"
26
+ },
27
+ "files": [
28
+ "dist/",
29
+ "README.md",
30
+ "LICENSE.md",
31
+ "THIRD_PARTY_NOTICES.md",
32
+ "!dist/**/*.map",
33
+ "src/README.md"
34
+ ],
35
+ "sideEffects": [
36
+ "**/*.css"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "node scripts/build.mjs && tsrs -p tsconfig.build.json",
43
+ "type-check": "tsrs --noEmit",
44
+ "prepack": "node ../../scripts/package-license.mjs stage",
45
+ "postpack": "node ../../scripts/package-license.mjs clean"
46
+ },
47
+ "dependencies": {
48
+ "onnxruntime-web": "1.30.0",
49
+ "preact": "10.29.8",
50
+ "voice-glow": "0.2.1"
51
+ },
52
+ "peerDependencies": {
53
+ "@seams/wallet": "0.25.0",
54
+ "react": "^18.0.0 || ^19.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "react": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@seams/wallet": "workspace:*",
63
+ "rolldown": "^1.0.0-beta.27"
64
+ },
65
+ "bin": {
66
+ "seams-voiceid-host": "dist/compose-host.mjs"
67
+ }
68
+ }
package/src/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # Browser VoiceID plugin
2
+
3
+ Enable with `authMethods: [voiceIdAuthMethod()]`. The descriptor is small and
4
+ loads no model code. Import it from `@seams/voiceid`. The operator deploys this
5
+ package's assets alongside a matching Wallet host. The trusted Wallet registry
6
+ loads `/sdk/voiceid/host.js` on first use and checks the host API version.
7
+ The plugin owns browser microphone capture, three-sample enrollment, ReDimNet B1
8
+ speaker matching, AuroraASR phrase recognition, WebCrypto device credentials, and the existing server proof
9
+ exchange. SDK core owns custody installation, quotas, and NEAR/ECDSA MPC signing.
10
+
11
+ Availability and selection are separate. Configuring the descriptor does not apply
12
+ voice approval to passkey or Email OTP signing. The Wallet selects policy from the
13
+ requested wallet's trusted authority selection. Enrolling VoiceID does not change
14
+ the current owner method. Unlock with VoiceID to select voice approval; unlock with
15
+ a passkey to return to passkey signing. Built-in methods do not load the VoiceID
16
+ entry or inference assets, including after a reload with an enrolled voice method.
17
+ A signing lane must match its review policy, so a method change cannot reuse an
18
+ incompatible approval. Omitting the plugin while VoiceID is selected rejects the
19
+ operation; it cannot silently bypass voice approval.
20
+
21
+
22
+ After a passkey or Email OTP owner unlock, call
23
+ `registration.activateAuthMethod({ walletId, method: 'voiceid' })`.
24
+ It resolves with `{ kind: 'activated', walletId, walletAuthMethodId }`, or with
25
+ `{ kind: 'cancelled' }` when the person closes the Wallet surface.
26
+ The Wallet requests microphone permission and enrolls this wallet method through three
27
+ Record clicks. For an existing method, the same command opens Manage voice. Later use `auth.unlockWithAuthMethod` with that wallet and method
28
+ ID. A VoiceID-selected NEAR or Arc testnet transfer requires Confirm and a fresh voice match.
29
+ The button confirms intent. Read the displayed phrase at a normal pace. Use Cancel to cancel.
30
+
31
+ `browser/` contains capture, model inference, enrollment, and device storage.
32
+ No native service, extension, Keychain, or OS settings are required. The model
33
+ loads only when enabled VoiceID needs inference. Audio stays local and is
34
+ discarded after processing. Browser storage retains the template, nonextractable
35
+ Ed25519 and AES-GCM keys, and encrypted random factor. Clearing Wallet-origin
36
+ storage requires owner unlock and enrollment again. Nonextractability does not
37
+ prevent trusted Wallet-origin code from using a key; server verification proves
38
+ device-key possession, and the Wallet code enforces speaker and phrase matching.
39
+
40
+ ## Hosted model asset
41
+
42
+ Place the approved exports at `packages/wallet/assets/voiceid/redimnet-b1.onnx`
43
+ and `packages/wallet/assets/voiceid/aurora-int8.onnx` before building the SDK. The existing
44
+ static-asset build includes it and the matching ONNX Runtime Web WASM files.
45
+ If the optional model is absent, ordinary Wallet builds still work, and enabling
46
+ VoiceID reports that its model is unavailable. Release builds offering VoiceID
47
+ must provision both assets. It is not fetched from a third party during capture.
48
+
49
+ Current model: ReDimNet B1, VoxCeleb2 large-margin fine-tuned, MIT license.
50
+ Source revision: `ce039a624cb99fe127702ceb94c6080090e5032f`.
51
+ Weight revision: `5180983b6895dafa0cd88aa88e8a07296df27834`.
52
+ The full-waveform export accepts 16 kHz mono PCM and returns a normalized
53
+ 192-dimensional embedding. Model bytes: 8,859,593.
54
+ SHA-256: `e04dd7a6a1e23fc7b89e47682ea9d0a3946cf7bc59d07922c3db3efc52ae4269`.
55
+ The deployment includes the model's MIT attribution and export details in NOTICE.txt.
56
+ Use the pinned exporter in `tests/scripts/voiceid-model-evaluation/prepare.py`
57
+ to reproduce the model. The browser evaluation evidence records parity checks.
58
+
59
+ The prototype uses a B1 threshold of 0.5: the next tenth above the four-second
60
+ calibration cohort's maximum impostor score (0.4417). The separate evaluation
61
+ cohort had 0/728 false accepts and 2/56 false rejects at this threshold. This small
62
+ clean-speech experiment does not qualify production authentication accuracy.
63
+
64
+ AuroraASR-1M checks the spoken phrase. Its INT8 full-waveform ONNX export is
65
+ 1,342,384 bytes and shares the existing WASM runtime. It accepts 16 kHz mono PCM.
66
+ Source revision: `cd307b54b4d666d6ad7dc982a7a58a96d6e7f942`.
67
+ SHA-256: `83ed6f3856458cd96d49a70d8c5175aeec18ba8c184e5367e089cd424eebefd6`.
68
+ The deployment includes its Apache-2.0 license and export details.
69
+ See [export and screening instructions](../../../../../tests/scripts/voiceid-aurora/README.md).
70
+
71
+ ## Download progress and browser cache
72
+
73
+ First activation shows an accessible progress bar with bytes and percentage while
74
+ each model downloads. Enrollment recording becomes available after preparation.
75
+ The trusted Wallet origin stores only hash-verified model bytes in Cache Storage,
76
+ under a key containing the model digest. A reload reads and verifies that cached
77
+ copy before loading inference. The normal HTTP cache also applies to runtime assets.
78
+
79
+ Storage can be unavailable or full. In that case inference continues for the
80
+ current session and the UI explains that the model could not be saved. Clearing
81
+ site data or browser eviction can require another download. A corrupted cached
82
+ model is discarded and downloaded again. Failed loads remain retryable.
83
+
84
+ ## Re-enrollment after a model change
85
+
86
+ The persistence boundary treats an enrollment for a different model as unavailable.
87
+ Voice-only unlock cannot use it. The user must first unlock with a passkey and
88
+ select Manage voice, then Record voice again. Activation reuses an existing local device
89
+ credential for that wallet and authority, then replaces the speaker template only
90
+ after all three samples succeed. It does not create another server method or change
91
+ the existing custody envelope. Cancellation preserves the prior stored record.
92
+
93
+ ## Manual adaptive enrollment
94
+
95
+ Manage voice shows whether the enrollment is original or improved, and the
96
+ remaining checks and time of an improvement. It provides Improve recognition,
97
+ Reset improvements, and Record voice again, and shows only the actions that apply.
98
+ Each action requires a fresh passkey confirmation. The trusted
99
+ Wallet validates the assertion's signature, challenge, origin, RP ID, user presence,
100
+ and user verification. ES256 passkey credentials use the existing public-key crypto
101
+ worker for COSE and signature decoding, then WebCrypto for signature verification. No credential PRF
102
+ output is requested. Confirmation authorizes one local enrollment change only.
103
+ Both ES256 and RS256 passkey credentials are supported. RS256 uses WebCrypto and
104
+ the existing small CBOR dependency for its RSA public key. These are passkey algorithms.
105
+ Wallet transaction signers remain ECDSA/secp256k1 and Ed25519. VoiceID signing currently
106
+ supports Ed25519-Yao NEAR transfers and native ECDSA Arc transfers on testnet. The server must still recognize the
107
+ same owner session before confirmation and before saving; no signing quota is consumed.
108
+
109
+ Enrollment storage binds wallet, authority, method, and device key. An atomic commit
110
+ checks owner selection, lock generation, availability revision, and enrollment
111
+ generation/revision. A competing update cannot be overwritten. Capture watches
112
+ owner lifecycle changes and releases the microphone if the action becomes invalid.
113
+
114
+ Three accepted supplements contribute 20% of the effective template; the immutable
115
+ baseline contributes 80%. Each supplement must match the baseline at 0.5. The final
116
+ template must stay within cosine 0.98 of the baseline. Supplements remain saved
117
+ without a time or usage limit. Reset removes them and preserves the baseline. Re-recording creates a
118
+ new baseline generation. These are bounded experimental settings, not calibrated
119
+ accuracy or false-acceptance guarantees. Automatic learning is not implemented.
120
+
121
+ Old browser-wide records cannot authenticate directly. Manage voice offers an
122
+ explicit, passkey-confirmed import for the selected method. This migration boundary
123
+ is the only reader of the old record. Other wallets require their own import or
124
+ enrollment. Import deletes it once all known active VoiceID methods have scoped records.
125
+
126
+ No additional model weights are needed. Fresh passkey confirmation can load the
127
+ existing crypto worker and WASM if uncached: about 1.34 MB uncompressed or 0.31 MB
128
+ with local gzip compression, plus management JavaScript. This is separate from the
129
+ 26.86 MB uncompressed voice models and inference runtime. Actual network transfer
130
+ depends on compression and caching. The local enrollment record has a 32 KiB limit.
131
+
132
+ To replace the implementation, preserve `WalletAuthMethodHostPlugin`, the device
133
+ assertion contract, and method-bound custody envelopes. To remove VoiceID, remove
134
+ its registry entry, descriptor exports, implementation directory, ONNX dependency,
135
+ and optional assets. Omitting the descriptor prevents runtime use and downloads.
136
+
137
+ ## Smaller models and spoken phrases
138
+
139
+ See the [model candidate evaluation plan](../../../../../docs/voiceid-model-candidates.md)
140
+ for verified download sizes, speaker-verification and STT shortlists, licensing,
141
+ and qualification criteria. The prototype uses pinned B1 and AuroraASR exports.
142
+
143
+ ## Prompt and preparation
144
+
145
+ Enrollment, unlock, and ordinary transfer approval randomly select one of 32 short
146
+ English trading-oriented sentences. The synthetic natural-paced fixtures
147
+ last about 1.2–2.4 seconds.
148
+ Each attempt holds its displayed sentence until recording ends. Retries
149
+ select another phrase. Read naturally, then pause. Capture stops after 1.5 seconds
150
+ of quiet once audible input has continued for 100 ms. Initial silence does not
151
+ stop capture. An eight-second maximum bounds silence, continued noise, and
152
+ recordings where live analysis is unavailable. Both speaker and phrase checks use the same recording.
153
+ Existing B1 enrollment remains valid. Aurora adds a separate phrase check.
154
+
155
+ Aurora decodes the audio independently of the displayed phrase. The matcher compares
156
+ that text with all 40 phrases. Eight additional phrases form four action-only
157
+ confirmation families. It ignores case, punctuation, and articles, and maps
158
+ selected homophones to a common spelling. It permits at most 30% normalized character
159
+ edit distance, requires a 10-point distance margin over the next candidate, and
160
+ limits decoded word count to within two words of the candidate. Negation must
161
+ match the candidate phrase; added, omitted, or changed negation rejects. These are development bounds for an approximate challenge. Some changed
162
+ content words can pass; this check does not interpret transaction intent.
163
+ A different recognized phrase, ambiguity, or an out-of-bounds result prevents
164
+ enrollment, device-key use, or signing. The speaker threshold remains 0.5. A signing retry
165
+ keeps the original transaction review and deadline and requires another Confirm.
166
+ No audio or transcript leaves the browser or enters persistent storage.
167
+
168
+ Per-call `options.voiceId.confirmationCommand` selects an action-only family from
169
+ the first English verb: sell, buy, swap, send, or transfer. Other wording keeps
170
+ the default challenges. The host validates this optional configuration once and
171
+ rejects empty, oversized, non-string, or unknown configuration fields. Each retry
172
+ rotates within the family. The raw command is included in the frozen operation
173
+ hash. It never changes transaction decoding, operation permissions, or speaker
174
+ and phrase thresholds. Numbers, asset names, and prices are not spoken. The
175
+ application cannot supply a replacement challenge.
176
+
177
+ The finite phrase bank does not establish replay resistance or liveness. The MVP
178
+ still assumes physical possession of an open, active device. Spoof detection stays
179
+ outside scope. See [screening results](../../../../../docs/evidence/voiceid-aurora-20261024/README.md).
180
+
181
+ The prompt reports its dimensions through the Wallet request measurement binding.
182
+ This reveals the parent modal immediately after layout, without its four-second
183
+ fallback. The reporter watches the stable outer section during enrollment changes
184
+ and disconnects when the prompt closes.
185
+
186
+ The lazy host entry contains the prompt shell. It mounts the modal and yields a
187
+ frame before importing the unlock or activation implementation. The card shows
188
+ Loading VoiceID and allows cancellation while its recording button is disabled.
189
+ Unlock then prepares its core session installer, including lock cleanup and a new
190
+ lock generation. It checks local state, loads the cached model if needed, and
191
+ requests its server challenge. One cumulative indicator covers the engine WASM
192
+ and all three models inside the same card. Verified cached bytes count toward the
193
+ same total. All four downloads finish before the card shows “Preparing VoiceID…”
194
+ and creates inference sessions. The streamed engine bytes go directly to ONNX
195
+ Runtime, so initialization does not fetch that binary again. The engine uses the
196
+ same hash-verified browser cache as the models. If storage is unavailable, loading
197
+ still works for the current session. Transaction approval uses the same preparation
198
+ before starting its bounded recording attempt.
199
+ The lightweight shell itself still needs its first module download.
200
+ Unlock starts listening automatically after preparation. The initial Unlock with
201
+ VoiceID action opens this flow. Back cancels capture and returns to the login menu.
202
+ Retries require Try again. The card uses the wallet's Back icon and Seams wordmark,
203
+ with one rounded surface and a transparent activation container.
204
+ Cancellation aborts the challenge request and prevents late UI or recording.
205
+ Shared model loading can finish and populate the cache after cancellation.
206
+
207
+ ## Minimal capture UI
208
+
209
+ Enrollment, unlock, and signing share the plugin-owned `VoicePrompt`. The card shows
210
+ Read aloud, a large phrase, a live waveform, a short status, and Cancel. Enrollment
211
+ also shows the sample number and a Record control between samples. The record
212
+ control disappears during capture. Download details disappear when preparation ends.
213
+
214
+ After transaction Confirm, the Wallet replaces the review contents with the plugin's
215
+ trusted DOM surface. The Wallet retains its modal, cancellation, expiry, and review
216
+ binding. A mismatch returns to the original review and requires another Confirm.
217
+ The core host loads no VoiceID UI code when the plugin is disabled.
218
+
219
+ The prompt combines a thin, four-layer flowing ribbon with the SDK's existing
220
+ VoiceBeam glow. Both use the recorder's activity levels; neither opens an audio
221
+ graph or requests a microphone. Speech drives full movement and ambient sound
222
+ is restrained. The ribbon settles when recording ends and the glow indicates
223
+ processing. Reduced motion removes ribbon travel and the glow honors the system
224
+ preference. Disposal cancels the ribbon clock and unmounts the glow.
225
+ The visual is loaded through the optional VoiceID host module. It cannot grant
226
+ approval or change capture, phrase, speaker, or signing policy.
227
+
228
+ ## Recording quality feedback
229
+
230
+ Short or quiet recordings stay in the unlock prompt with Try again. The original
231
+ server challenge and expiry timer remain unchanged. Only typed insufficient-audio
232
+ results can take this path. A retry displays a new phrase before capture starts.
233
+ The normal phrase and speaker checks still apply before device-key use.
234
+
235
+ The prompt shows brief failure guidance and a retry control. It exposes no
236
+ Recording details expander or technical recognition text. Internal measurements
237
+ retain the input label, duration, stop reason, and frame levels for behavior checks. The
238
+ level threshold is 0.006. The quality precheck requires 600 ms of qualifying audio.
239
+ Aurora phrase and B1 speaker thresholds are unchanged. This energy proxy cannot generally separate
240
+ short speech from quiet or suppressed speech. Each measurement has an attempt ID
241
+ and timestamp. No audio or transcript is saved or sent by these diagnostics.
242
+
243
+
244
+ ## Settings and SeamsAuthMenu
245
+
246
+ The VoiceID card in Authentication methods shows the enable/disable switch, the
247
+ state in this browser, and **Manage voice**.
248
+ Management requires an enabled VoiceID method and an active owner session.
249
+ Each change requires a fresh passkey assertion from the same authority. VoiceID
250
+ can remain selected while that passkey confirms the change. Use
251
+ `registration.getAuthMethodAvailability({ walletId, method: 'voiceid' })` to read
252
+ `not_enrolled`, `enabled`, or `disabled`. Use
253
+ `registration.setAuthMethodEnabled({ walletId, method: 'voiceid', enabled })`
254
+ to change the browser preference. It preserves enrollment. Enable requires an
255
+ owner passkey or Email OTP session. Disable requires another active method and
256
+ locks a current VoiceID session. Existing enrollments without a preference start
257
+ enabled at the persistence boundary. Removal is a separate revocation action.
258
+
259
+ The login menu lists a wallet once, with its available methods below. Discovery
260
+ reads local metadata without loading the voice models. Voice login uses the menu's
261
+ measurement and cancellation channel; the old menu is hidden and inert during
262
+ capture. Back restores it. The minimal signing prompt is unchanged, with an
263
+ explicit “Confirm with VoiceID” review button. Capture remains click initiated.
264
+
265
+ ## Background noise
266
+
267
+ Silero VAD adds 2,327,524 bytes, loaded only with VoiceID and cached with integrity
268
+ verification. Browser noise suppression, echo cancellation, and automatic gain
269
+ control remain requested. High-noise recordings receive brief retry guidance.
270
+
271
+ Speech detection replaces volume alone for pause detection and clip qualification.
272
+ It retains the 1.5-second pause and eight-second recording cap. Speech probability
273
+ must reach 0.5 and the original minimum volume must also pass. The speaker
274
+ threshold remains 0.5. Phrase matching retains the bounded text-difference and
275
+ candidate-margin checks described above.
276
+
277
+ Noise estimates compare frames classified as speech with other frames. They are
278
+ approximate guidance, not a calibrated acoustic SNR or an authorization decision.
279
+ Nearby conversation can still register as speech. No extra denoiser is shipped.
280
+ See `tests/scripts/voiceid-noise/README.md` for model provisioning and evaluation.