@seams/wallet-server 0.23.0 → 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 (86) hide show
  1. package/THIRD_PARTY_NOTICES.md +182 -5
  2. package/artifact-manifest.json +23 -13
  3. package/cloudflare-router-ab/build/deriver-a/build-stamp.json +1 -1
  4. package/cloudflare-router-ab/build/deriver-b/build-stamp.json +1 -1
  5. package/cloudflare-router-ab/build/local-tools/darwin-arm64/router_ab_local_init +0 -0
  6. package/cloudflare-router-ab/build/local-tools/darwin-x64/router_ab_local_init +0 -0
  7. package/cloudflare-router-ab/build/router/build-stamp.json +1 -1
  8. package/cloudflare-router-ab/build/signing-worker/build-stamp.json +1 -1
  9. package/cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json +1 -1
  10. package/dist/esm/authorization/voiceIdDeviceProof.js +32 -0
  11. package/dist/esm/core/ThresholdService/validation.js +17 -1
  12. package/dist/esm/core/d1WalletAuthMethodStore.js +42 -1
  13. package/dist/esm/core/deviceLinking/linkedDeviceManagement.js +5 -0
  14. package/dist/esm/core/signingLanes/WalletExecutionLaneProjection.js +11 -2
  15. package/dist/esm/router/cloudflare/d1/auth/d1RouterApiAuthService.js +23 -6
  16. package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceAuthorityInstallService.js +5 -5
  17. package/dist/esm/router/cloudflare/d1/emailOtp/d1GoogleEmailOtpRegistrationAttemptStore.js +1 -1
  18. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyEnvelopeStore.js +6 -0
  19. package/dist/esm/router/cloudflare/d1/wallet/d1VoiceIdActivation.js +139 -0
  20. package/dist/esm/router/cloudflare/d1/wallet/d1VoiceIdSessions.js +138 -0
  21. package/dist/esm/router/cloudflare/d1/wallet/d1VoiceIdWalletSessionChallenges.js +88 -0
  22. package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodService.js +12 -4
  23. package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthorityStore.js +14 -0
  24. package/dist/esm/router/cloudflare/d1/webauthn/d1WebAuthnRecords.js +1 -0
  25. package/dist/esm/router/cloudflare.js +576 -10
  26. package/dist/esm/router/domains/ed25519Yao/export/routerAbEd25519YaoExport.js +5 -0
  27. package/dist/esm/router/domains/passkeyCustody/walletRecoveryFinalization.js +1 -0
  28. package/dist/esm/router/domains/signingOperations/routerAbNormalSigningAdmissionCore.js +1 -0
  29. package/dist/esm/router/domains/signingOperations/walletExecutionAdmission.js +1 -0
  30. package/dist/esm/router/domains/voiceId/voiceIdActivation.js +12 -0
  31. package/dist/esm/router/domains/voiceId/voiceIdSessions.js +0 -0
  32. package/dist/esm/router/framework/routeDefinitions.js +13 -0
  33. package/dist/esm/router/transport/fetch/createFetchRouter.js +5 -0
  34. package/dist/esm/router/transport/fetch/routes/deviceLinkingOwnerAuthorization.js +12 -1
  35. package/dist/esm/router/transport/fetch/routes/walletExecutionLanePreflight.js +1 -0
  36. package/dist/esm/router/transport/fetch/routes/walletVoiceIdActivation.js +139 -0
  37. package/dist/esm/router/transport/fetch/routes/walletVoiceIdNear.js +36 -0
  38. package/dist/esm/router/transport/fetch/routes/walletVoiceIdSessions.js +127 -0
  39. package/dist/esm/shared-ts/src/authorization/voiceIdDeviceAssertion.js +110 -0
  40. package/dist/esm/shared-ts/src/device-linking/authorityActivation.js +1 -0
  41. package/dist/esm/shared-ts/src/device-linking/committedSignerPackages.js +1 -1
  42. package/dist/esm/shared-ts/src/passkey-custody/custodyEnvelope.js +30 -3
  43. package/dist/esm/shared-ts/src/passkey-custody/custodySecretBinding.js +1 -1
  44. package/dist/esm/shared-ts/src/signing-lanes/recordParsers.js +15 -1
  45. package/dist/esm/shared-ts/src/threshold/sessionPolicy.js +2 -1
  46. package/dist/esm/shared-ts/src/utils/registrationIntent.js +1 -3
  47. package/dist/esm/shared-ts/src/utils/walletAuthAuthority.js +46 -2
  48. package/dist/esm/shared-ts/src/utils/walletAuthMethodRecord.js +116 -7
  49. package/dist/esm/shared-ts/src/wallet-recovery/walletRecoveryCommittedProjection.js +1 -0
  50. package/dist/esm/wasm/near_signer/pkg/wasm_signer_worker_bg.wasm +0 -0
  51. package/dist/types/shared-ts/src/authorization/voiceIdDeviceAssertion.d.ts +32 -0
  52. package/dist/types/shared-ts/src/device-linking/committedSignerPackages.d.ts +3 -1
  53. package/dist/types/shared-ts/src/device-linking/contracts.d.ts +8 -0
  54. package/dist/types/shared-ts/src/passkey-custody/custodyEnvelope.d.ts +29 -0
  55. package/dist/types/shared-ts/src/passkey-custody/custodySecretBinding.d.ts +1 -0
  56. package/dist/types/shared-ts/src/signing-lanes/execution.d.ts +2 -2
  57. package/dist/types/shared-ts/src/signing-lanes/recordParsers.d.ts +2 -1
  58. package/dist/types/shared-ts/src/signing-lanes/records.d.ts +10 -2
  59. package/dist/types/shared-ts/src/signing-lanes/rotation.d.ts +2 -2
  60. package/dist/types/shared-ts/src/threshold/sessionPolicy.d.ts +9 -0
  61. package/dist/types/shared-ts/src/utils/addWalletAuthMethod.d.ts +3 -3
  62. package/dist/types/shared-ts/src/utils/domainIds.d.ts +4 -5
  63. package/dist/types/shared-ts/src/utils/registrationIntent.d.ts +0 -1
  64. package/dist/types/shared-ts/src/utils/signerDomain.d.ts +3 -0
  65. package/dist/types/shared-ts/src/utils/walletAuthAuthority.d.ts +32 -2
  66. package/dist/types/shared-ts/src/utils/walletAuthMethodRecord.d.ts +15 -1
  67. package/dist/types/shared-ts/src/utils/walletCapabilityBindings.d.ts +20 -0
  68. package/dist/types/wallet-server/src/authorization/voiceIdDeviceProof.d.ts +29 -0
  69. package/dist/types/wallet-server/src/core/d1WalletAuthMethodStore.d.ts +13 -2
  70. package/dist/types/wallet-server/src/core/registrationContracts.d.ts +8 -0
  71. package/dist/types/wallet-server/src/core/signingLanes/WalletExecutionLaneProjection.d.ts +4 -0
  72. package/dist/types/wallet-server/src/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyEnvelopeStore.d.ts +4 -0
  73. package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1VoiceIdActivation.d.ts +14 -0
  74. package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1VoiceIdSessions.d.ts +19 -0
  75. package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1VoiceIdWalletSessionChallenges.d.ts +26 -0
  76. package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1WalletAuthMethodService.d.ts +1 -0
  77. package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1WalletRelocationOwner.d.ts +25 -0
  78. package/dist/types/wallet-server/src/router/domains/voiceId/voiceIdActivation.d.ts +29 -0
  79. package/dist/types/wallet-server/src/router/domains/voiceId/voiceIdSessions.d.ts +35 -0
  80. package/dist/types/wallet-server/src/router/framework/authServicePort.d.ts +3 -0
  81. package/dist/types/wallet-server/src/router/transport/fetch/routes/walletVoiceIdActivation.d.ts +12 -0
  82. package/dist/types/wallet-server/src/router/transport/fetch/routes/walletVoiceIdNear.d.ts +14 -0
  83. package/dist/types/wallet-server/src/router/transport/fetch/routes/walletVoiceIdSessions.d.ts +19 -0
  84. package/migrations/d1-signer/0083_voiceid_wallet_session_challenges.sql +29 -0
  85. package/migrations/d1-signer/0084_voiceid_wallet_auth_methods.sql +373 -0
  86. package/package.json +1 -1
@@ -891,6 +891,11 @@ async function recordFreshExportProof(args) {
891
891
  let challengeId = null;
892
892
  let assertionDigest = null;
893
893
  switch (args.authority.factor.kind) {
894
+ case "voiceid": return authorizationFailure({
895
+ status: 403,
896
+ code: "export_authorization_invalid",
897
+ message: "Use a passkey or Email OTP to export keys."
898
+ });
894
899
  case "passkey":
895
900
  factorId = parseAuthFactorId(`passkey:${args.authority.factor.credentialIdB64u}`);
896
901
  try {
@@ -21,6 +21,7 @@ function sameWalletCustodyRecoveryReplacementEnvelopeV1(left, right) {
21
21
  switch (left.factor.kind) {
22
22
  case "passkey": return right.factor.kind === "passkey" && left.factor.rpId === right.factor.rpId && left.factor.credentialIdB64u === right.factor.credentialIdB64u && left.factor.kekVersion === right.factor.kekVersion;
23
23
  case "email_otp": return right.factor.kind === "email_otp" && left.factor.enrollmentId === right.factor.enrollmentId && left.factor.enrollmentSealKeyVersion === right.factor.enrollmentSealKeyVersion && left.factor.kekVersion === right.factor.kekVersion;
24
+ case "voiceid": return right.factor.kind === "voiceid" && left.factor.deviceKeyId === right.factor.deviceKeyId && left.factor.kekVersion === right.factor.kekVersion;
24
25
  default: return assertNeverWalletRecoveryFinalizationBranch(left.factor, "custody envelope factor");
25
26
  }
26
27
  }
@@ -75,6 +75,7 @@ function runtimePolicyScopeKey(scope) {
75
75
  }
76
76
  function ed25519AdmissionAuthorityScopeKey(scope) {
77
77
  switch (scope.kind) {
78
+ case "voiceid": return `voiceid:${scope.deviceKeyId}`;
78
79
  case "passkey_rp": return `passkey_rp:${scope.rpId}`;
79
80
  case "email_otp": return `email_otp:${scope.provider}:${scope.providerUserId}`;
80
81
  }
@@ -199,6 +199,7 @@ function isActiveOwnerLane(lane) {
199
199
  if (lane.lifecycle.state !== "active") return false;
200
200
  switch (lane.laneKind) {
201
201
  case "owner_passkey":
202
+ case "owner_voiceid":
202
203
  case "owner_email_otp":
203
204
  case "recovery":
204
205
  case "break_glass": return true;
@@ -0,0 +1,12 @@
1
+ import { isVoiceIdSeedEnvelope, parsePasskeyCustodyEnvelopeRecord } from "../../../shared-ts/src/passkey-custody/custodyEnvelope.js";
2
+ //#region src/router/domains/voiceId/voiceIdActivation.ts
3
+ /** Accepts only a method-bound owner seed envelope for the new VoiceID method. */
4
+ function parseVoiceIdActivationEnvelope(raw) {
5
+ const envelope = parsePasskeyCustodyEnvelopeRecord(raw);
6
+ if (!isVoiceIdSeedEnvelope(envelope) || Number(envelope.envelopeRevision) !== 1) throw new Error("VoiceID activation requires an active, method-bound wallet seed envelope");
7
+ return envelope;
8
+ }
9
+ //#endregion
10
+ export { parseVoiceIdActivationEnvelope };
11
+
12
+ //# sourceMappingURL=voiceIdActivation.js.map
@@ -45,6 +45,7 @@ const ROUTER_API_PASSKEY_CUSTODY_SERVICES = [
45
45
  ];
46
46
  const ROUTER_API_LINKED_DEVICE_PASSKEY_SERVICES = ["publishableKeyAuth"];
47
47
  const ROUTER_API_WALLET_UNLOCK_SERVICES = ["walletUnlock"];
48
+ const ROUTER_API_VOICEID_ACTIVATION_SERVICES = ["session", "walletAuthMethods"];
48
49
  const ROUTER_API_HOSTED_SESSION_EXCHANGE_SERVICES = ["session"];
49
50
  const ROUTER_API_EMAIL_OTP_PUBLIC_SERVICES = ["emailOtp"];
50
51
  const ROUTER_API_EMAIL_OTP_CHALLENGE_SERVICES = ["emailOtp", "walletAuthMethods"];
@@ -420,6 +421,18 @@ function createRouterApiRouteDefinitions(options = {}) {
420
421
  plane: "public",
421
422
  proof: "challenge_exchange",
422
423
  rationale: "Wallet unlock verification is intentionally public because the challenge proof is the gate."
424
+ }, ROUTER_API_WALLET_UNLOCK_SERVICES), publicRoute("wallet_voiceid_activate", "POST", "/wallet/voiceid/activate", "Activate a device-bound VoiceID method under an owner Wallet Session", {
425
+ plane: "public",
426
+ proof: "challenge_exchange",
427
+ rationale: "An active full-owner Wallet Session and the hosted wallet origin authorize activation."
428
+ }, ROUTER_API_VOICEID_ACTIVATION_SERVICES), publicRoute("wallet_voiceid_challenge", "POST", "/wallet/voiceid/challenge", "Issue a device-bound VoiceID Wallet Session challenge", {
429
+ plane: "public",
430
+ proof: "challenge_exchange",
431
+ rationale: "An active method receives a short-lived challenge bound to the hosted wallet origin."
432
+ }, ROUTER_API_WALLET_UNLOCK_SERVICES), publicRoute("wallet_voiceid_verify", "POST", "/wallet/voiceid/verify", "Verify a VoiceID device assertion and issue a Wallet Session", {
433
+ plane: "public",
434
+ proof: "signed_payload",
435
+ rationale: "The registered device signature and single-use server challenge authorize issuance."
423
436
  }, ROUTER_API_WALLET_UNLOCK_SERVICES), publicRoute("wallet_email_otp_challenge", "POST", "/wallet/email-otp/challenge", "Create an operation-bound Email OTP challenge", {
424
437
  plane: "public",
425
438
  proof: "challenge_exchange",
@@ -10,6 +10,8 @@ import { coerceRouterLogger } from "../../framework/logger.js";
10
10
  import { json, jsonFailure, withCors } from "../../framework/http.js";
11
11
  import { handleHealth, handleReady } from "./routes/health.js";
12
12
  import { handleWalletRegistration } from "./routes/walletRegistration.js";
13
+ import { handleWalletVoiceIdActivation } from "./routes/walletVoiceIdActivation.js";
14
+ import { handleWalletVoiceIdChallenge, handleWalletVoiceIdVerify } from "./routes/walletVoiceIdSessions.js";
13
15
  import { handlePasskeyCustody, handleWalletCustodyCredentialLabel, handleWalletCustodyCredentialsList, handleWalletCustodyEmailOtpChallenge, handleWalletCustodyEnvelopeOwnershipUpgrade, handleWalletRecoveryBackupAcknowledge, handleWalletRecoveryEmailOtpRelease, handleWalletRecoveryEmailOtpVerify, handleWalletRecoveryFinalize, handleWalletRecoveryGoogleEmailOtpFinalize, handleWalletRecoveryGoogleVerify, handleWalletRecoveryPrepare, handleWalletRecoveryRead, handleWalletRecoveryRotate, handleWalletRecoveryStatus } from "./routes/passkeyCustody.js";
14
16
  import { handleThresholdEcdsa } from "./routes/thresholdEcdsa.js";
15
17
  import { handleExactWalletSessionStatus, handleHostedWalletSessionExchangeIssue, handleHostedWalletSessionExchangeRedeem, handleWalletEmailOtpChallenge, handleWalletEmailOtpDevCleanupGoogleRegistration, handleWalletEmailOtpDevOutbox, handleWalletEmailOtpFactorRelease, handleWalletEmailOtpRegistrationSeal, handleWalletUnlockChallenge, handleWalletUnlockVerify } from "./routes/sessions.js";
@@ -234,6 +236,9 @@ function createFetchRouter(service, opts, runtime) {
234
236
  handleHostedWalletSessionExchangeRedeem,
235
237
  handleWalletUnlockChallenge,
236
238
  handleWalletUnlockVerify,
239
+ handleWalletVoiceIdActivation,
240
+ handleWalletVoiceIdChallenge,
241
+ handleWalletVoiceIdVerify,
237
242
  handleWalletEmailOtpChallenge,
238
243
  handleWalletEmailOtpFactorRelease,
239
244
  handleWalletEmailOtpDevCleanupGoogleRegistration,
@@ -1,7 +1,7 @@
1
1
  import { parseProviderSubject } from "../../../../shared-ts/src/utils/domainIds.js";
2
2
  import { buildDelegatedWalletAuthorityV1 } from "../../../../shared-ts/src/authorization/delegatedAuthority.js";
3
3
  import { sha256DigestB64u } from "../../../../shared-ts/src/utils/canonicalPrimitives.js";
4
- import { buildEmailOtpWalletAuthAuthority, buildPasskeyWalletAuthAuthority, walletAuthAuthorityRef } from "../../../../shared-ts/src/utils/walletAuthAuthority.js";
4
+ import { buildEmailOtpWalletAuthAuthority, buildPasskeyWalletAuthAuthority, buildVoiceIdWalletAuthAuthority, walletAuthAuthorityRef } from "../../../../shared-ts/src/utils/walletAuthAuthority.js";
5
5
  import { parseLinkedDeviceOwnerAuthorizationRequestV1 } from "../../../../shared-ts/src/device-linking/parsers.js";
6
6
  import { buildWalletSessionCapabilitySubjectsV1, walletSessionCapabilitySubjectsV1Equal } from "../../../../authorization/domain.js";
7
7
  import { extractBearerCredential } from "../../../auth/routerApiKeyAuth.js";
@@ -203,6 +203,17 @@ function walletAuthAuthorityFromExactV2MethodV1(input) {
203
203
  } catch {
204
204
  return null;
205
205
  }
206
+ if (input.authMethod.kind === "voiceid") return {
207
+ authority: buildVoiceIdWalletAuthAuthority({
208
+ walletId: input.walletId,
209
+ walletAuthMethodId: input.authMethod.walletAuthMethodId,
210
+ deviceKeyId: input.authMethod.deviceKeyId
211
+ }),
212
+ authSource: {
213
+ kind: "voiceid",
214
+ deviceKeyId: input.authMethod.deviceKeyId
215
+ }
216
+ };
206
217
  const providerSubject = parseProviderSubject(input.principalId);
207
218
  if (!providerSubject.ok) return null;
208
219
  try {
@@ -116,6 +116,7 @@ function serializeProjection(value) {
116
116
  const lane = value.lane;
117
117
  switch (lane.laneKind) {
118
118
  case "owner_passkey":
119
+ case "owner_voiceid":
119
120
  case "owner_email_otp": return {
120
121
  kind: value.kind,
121
122
  walletKey: serializeWalletKey(value.walletKey),
@@ -0,0 +1,139 @@
1
+ import { parseWalletAuthMethodId } from "../../../../shared-ts/src/utils/domainIds.js";
2
+ import { requireRecord } from "../../../../shared-ts/src/utils/validation.js";
3
+ import { rejectUnknownFields } from "../../../../shared-ts/src/utils/exactRecord.js";
4
+ import { parseVoiceIdDevicePublicKeyB64u } from "../../../../shared-ts/src/authorization/voiceIdDeviceAssertion.js";
5
+ import { parsePrimaryWalletSessionOperationCredentialToken, parseSessionOrigin } from "../../../../authorization/domain.js";
6
+ import { extractBearerCredential } from "../../../auth/routerApiKeyAuth.js";
7
+ import { json, readJson } from "../../../framework/http.js";
8
+ import { parseVoiceIdActivationEnvelope } from "../../../domains/voiceId/voiceIdActivation.js";
9
+ //#region src/router/transport/fetch/routes/walletVoiceIdActivation.ts
10
+ const ACTIVATION_FIELDS = [
11
+ "targetWalletAuthMethodId",
12
+ "devicePublicKeyB64u",
13
+ "custodyEnvelope"
14
+ ];
15
+ function hostedWalletOriginAllowed(request, allowedOrigins) {
16
+ const rawOrigin = request.headers.get("origin");
17
+ let requestOrigin;
18
+ try {
19
+ requestOrigin = parseSessionOrigin(rawOrigin);
20
+ } catch {
21
+ return false;
22
+ }
23
+ for (const allowed of allowedOrigins) try {
24
+ if (parseSessionOrigin(allowed) === requestOrigin) return true;
25
+ } catch {
26
+ continue;
27
+ }
28
+ return false;
29
+ }
30
+ function parseActivationBody(raw) {
31
+ const body = requireRecord(raw, "VoiceID activation");
32
+ rejectUnknownFields(body, ACTIVATION_FIELDS, "VoiceID activation");
33
+ const target = parseWalletAuthMethodId(body.targetWalletAuthMethodId);
34
+ if (!target.ok) throw new Error(target.error.message);
35
+ return {
36
+ targetWalletAuthMethodId: target.value,
37
+ devicePublicKeyB64u: parseVoiceIdDevicePublicKeyB64u(body.devicePublicKeyB64u),
38
+ envelope: parseVoiceIdActivationEnvelope(body.custodyEnvelope)
39
+ };
40
+ }
41
+ function rejectionStatus(code) {
42
+ switch (code) {
43
+ case "invalid_source": return 403;
44
+ case "invalid_target": return 400;
45
+ case "conflict": return 409;
46
+ default: {
47
+ const unexpected = code;
48
+ throw new Error(`Unsupported VoiceID activation rejection: ${unexpected}`);
49
+ }
50
+ }
51
+ }
52
+ async function handleWalletVoiceIdActivation(ctx) {
53
+ if (ctx.method !== "POST" || ctx.pathname !== "/wallet/voiceid/activate") return null;
54
+ return await handleVoiceIdActivationRequest(ctx.request, {
55
+ tenantId: ctx.service.authorizationSessions.tenantId,
56
+ readWalletSessionAuthorizationV2ByOperationCredential: ctx.service.authorizationSessions.readWalletSessionAuthorizationV2ByOperationCredential,
57
+ activateVoiceId: ctx.service.walletAuthMethods.activateVoiceId,
58
+ hostedWalletOrigins: ctx.opts.hostedWalletOrigins ?? []
59
+ });
60
+ }
61
+ async function handleVoiceIdActivationRequest(request, deps) {
62
+ if (!hostedWalletOriginAllowed(request, deps.hostedWalletOrigins)) return json({
63
+ ok: false,
64
+ code: "origin_mismatch"
65
+ }, { status: 403 });
66
+ const credential = extractBearerCredential(request.headers);
67
+ if (!credential) return json({
68
+ ok: false,
69
+ code: "unauthorized"
70
+ }, { status: 401 });
71
+ let token;
72
+ try {
73
+ token = parsePrimaryWalletSessionOperationCredentialToken(credential);
74
+ } catch {
75
+ return json({
76
+ ok: false,
77
+ code: "unauthorized"
78
+ }, { status: 401 });
79
+ }
80
+ const nowMs = Date.now();
81
+ let source;
82
+ try {
83
+ source = await deps.readWalletSessionAuthorizationV2ByOperationCredential({
84
+ tenantId: deps.tenantId,
85
+ token,
86
+ nowMs
87
+ });
88
+ } catch {
89
+ return json({
90
+ ok: false,
91
+ code: "wallet_session_unavailable"
92
+ }, { status: 503 });
93
+ }
94
+ if (!source || source.retiredAtMs !== null || source.authMethod.status !== "active" || source.authMethod.kind !== "passkey" && source.authMethod.kind !== "email_otp") return json({
95
+ ok: false,
96
+ code: "unauthorized"
97
+ }, { status: 401 });
98
+ let parsedBody;
99
+ try {
100
+ parsedBody = parseActivationBody(await readJson(request));
101
+ } catch (error) {
102
+ return json({
103
+ ok: false,
104
+ code: "invalid_body",
105
+ message: error instanceof Error ? error.message : "VoiceID activation body is invalid"
106
+ }, { status: 400 });
107
+ }
108
+ let result;
109
+ try {
110
+ result = await deps.activateVoiceId({
111
+ source: {
112
+ authorization: source.authorization,
113
+ authority: source.authority,
114
+ authMethod: source.authMethod
115
+ },
116
+ targetWalletAuthMethodId: parsedBody.targetWalletAuthMethodId,
117
+ devicePublicKeyB64u: parsedBody.devicePublicKeyB64u,
118
+ envelope: parsedBody.envelope,
119
+ nowMs
120
+ });
121
+ } catch {
122
+ return json({
123
+ ok: false,
124
+ code: "activation_unavailable"
125
+ }, { status: 503 });
126
+ }
127
+ if (result.kind === "rejected") return json({
128
+ ok: false,
129
+ code: result.code
130
+ }, { status: rejectionStatus(result.code) });
131
+ return json({
132
+ ok: true,
133
+ authMethod: result.authMethod
134
+ }, { status: 200 });
135
+ }
136
+ //#endregion
137
+ export { handleVoiceIdActivationRequest, handleWalletVoiceIdActivation, hostedWalletOriginAllowed };
138
+
139
+ //# sourceMappingURL=walletVoiceIdActivation.js.map
@@ -0,0 +1,36 @@
1
+ import { routerAbMpcMaterialActivationRefToWire, sameRouterAbMpcMaterialActivationRef } from "../../../../shared-ts/src/utils/routerAbNormalSigningIdentity.js";
2
+ //#region src/router/transport/fetch/routes/walletVoiceIdNear.ts
3
+ async function prepareVoiceIdNearInstallation(ctx, verified) {
4
+ const session = verified.authorization.session;
5
+ const activation = session.capabilitySubjects.find(isNearSignSubject);
6
+ if (!activation || activation.kind !== "sign") return { kind: "unavailable" };
7
+ const resolved = await ctx.service.walletRegistration.resolveEd25519MaterialActivation({
8
+ walletId: session.walletId,
9
+ materialActivation: routerAbMpcMaterialActivationRefToWire(activation.materialActivation)
10
+ });
11
+ if (!resolved.ok) throw new Error(resolved.message);
12
+ const product = ctx.opts.routerAbEd25519YaoProduct;
13
+ if (!product) throw new Error("NEAR signing is not configured");
14
+ const capability = await product.resolveActiveCapability({
15
+ kind: "router_ab_ed25519_yao_active_capability_lookup_v1",
16
+ walletId: session.walletId,
17
+ nearEd25519SigningKeyId: resolved.exportIdentity.application_binding.near_ed25519_signing_key_id,
18
+ signerSlot: resolved.signerSlot,
19
+ signingWorkerId: resolved.signingWorkerId,
20
+ participantIds: resolved.participantIds
21
+ });
22
+ if (!capability.ok) throw new Error(capability.message);
23
+ if (!sameRouterAbMpcMaterialActivationRef(capability.capability.materialActivation, resolved.materialActivation)) throw new Error("NEAR capability does not belong to this Wallet Session");
24
+ return {
25
+ kind: "ready",
26
+ capability: capability.capability,
27
+ remainingUses: verified.authorization.quota.remainingUses
28
+ };
29
+ }
30
+ function isNearSignSubject(subject) {
31
+ return subject.kind === "sign" && subject.keyFamily === "ed25519";
32
+ }
33
+ //#endregion
34
+ export { prepareVoiceIdNearInstallation };
35
+
36
+ //# sourceMappingURL=walletVoiceIdNear.js.map
@@ -0,0 +1,127 @@
1
+ import { parseWalletAuthMethodId, parseWalletId } from "../../../../shared-ts/src/utils/domainIds.js";
2
+ import { requireRecord } from "../../../../shared-ts/src/utils/validation.js";
3
+ import { rejectUnknownFields } from "../../../../shared-ts/src/utils/exactRecord.js";
4
+ import { parseVoiceIdDeviceAssertion } from "../../../../shared-ts/src/authorization/voiceIdDeviceAssertion.js";
5
+ import { parseSessionOrigin, projectActiveWalletSession } from "../../../../authorization/domain.js";
6
+ import { json, readJson } from "../../../framework/http.js";
7
+ import { hostedWalletOriginAllowed } from "./walletVoiceIdActivation.js";
8
+ import { prepareVoiceIdNearInstallation } from "./walletVoiceIdNear.js";
9
+ //#region src/router/transport/fetch/routes/walletVoiceIdSessions.ts
10
+ function hostedOrigin(request, allowedOrigins) {
11
+ if (!hostedWalletOriginAllowed(request, allowedOrigins)) return null;
12
+ return parseSessionOrigin(request.headers.get("origin"));
13
+ }
14
+ function parseChallengeBody(raw) {
15
+ const body = requireRecord(raw, "VoiceID challenge");
16
+ rejectUnknownFields(body, ["walletId", "walletAuthMethodId"], "VoiceID challenge");
17
+ const walletId = parseWalletId(body.walletId);
18
+ if (!walletId.ok) throw new Error(walletId.error.message);
19
+ const walletAuthMethodId = parseWalletAuthMethodId(body.walletAuthMethodId);
20
+ if (!walletAuthMethodId.ok) throw new Error(walletAuthMethodId.error.message);
21
+ return {
22
+ walletId: walletId.value,
23
+ walletAuthMethodId: walletAuthMethodId.value
24
+ };
25
+ }
26
+ async function handleWalletVoiceIdChallenge(ctx) {
27
+ if (ctx.method !== "POST" || ctx.pathname !== "/wallet/voiceid/challenge") return null;
28
+ return await handleVoiceIdChallengeRequest(ctx.request, {
29
+ issueVoiceIdChallenge: ctx.service.walletUnlock.issueVoiceIdChallenge,
30
+ hostedWalletOrigins: ctx.opts.hostedWalletOrigins ?? []
31
+ });
32
+ }
33
+ async function handleWalletVoiceIdVerify(ctx) {
34
+ if (ctx.method !== "POST" || ctx.pathname !== "/wallet/voiceid/verify") return null;
35
+ return await handleVoiceIdVerifyRequest(ctx.request, {
36
+ verifyAndIssueVoiceId: ctx.service.walletUnlock.verifyAndIssueVoiceId,
37
+ prepareNear: prepareVoiceIdNearInstallation.bind(null, ctx),
38
+ hostedWalletOrigins: ctx.opts.hostedWalletOrigins ?? []
39
+ });
40
+ }
41
+ async function handleVoiceIdChallengeRequest(request, deps) {
42
+ const requestOrigin = hostedOrigin(request, deps.hostedWalletOrigins);
43
+ if (!requestOrigin) return json({
44
+ ok: false,
45
+ code: "origin_mismatch"
46
+ }, { status: 403 });
47
+ let body;
48
+ try {
49
+ body = parseChallengeBody(await readJson(request));
50
+ } catch (error) {
51
+ return json({
52
+ ok: false,
53
+ code: "invalid_body",
54
+ message: error instanceof Error ? error.message : "VoiceID challenge body is invalid"
55
+ }, { status: 400 });
56
+ }
57
+ try {
58
+ const result = await deps.issueVoiceIdChallenge({
59
+ walletId: body.walletId,
60
+ walletAuthMethodId: body.walletAuthMethodId,
61
+ requestOrigin,
62
+ nowMs: Date.now()
63
+ });
64
+ if (result.kind === "rejected") return json({
65
+ ok: false,
66
+ code: result.code
67
+ }, { status: 403 });
68
+ return json({
69
+ ok: true,
70
+ challenge: result.challenge
71
+ }, { status: 200 });
72
+ } catch {
73
+ return json({
74
+ ok: false,
75
+ code: "challenge_unavailable"
76
+ }, { status: 503 });
77
+ }
78
+ }
79
+ async function handleVoiceIdVerifyRequest(request, deps) {
80
+ const requestOrigin = hostedOrigin(request, deps.hostedWalletOrigins);
81
+ if (!requestOrigin) return json({
82
+ ok: false,
83
+ code: "origin_mismatch"
84
+ }, { status: 403 });
85
+ let assertion;
86
+ try {
87
+ const body = requireRecord(await readJson(request), "VoiceID verification");
88
+ rejectUnknownFields(body, ["assertion"], "VoiceID verification");
89
+ assertion = parseVoiceIdDeviceAssertion(body.assertion);
90
+ } catch (error) {
91
+ return json({
92
+ ok: false,
93
+ code: "invalid_body",
94
+ message: error instanceof Error ? error.message : "VoiceID assertion is invalid"
95
+ }, { status: 400 });
96
+ }
97
+ try {
98
+ const result = await deps.verifyAndIssueVoiceId({
99
+ assertion,
100
+ requestOrigin,
101
+ nowMs: Date.now()
102
+ });
103
+ if (result.kind === "rejected") {
104
+ const status = result.code === "issuance_unavailable" ? 503 : 403;
105
+ return json({
106
+ ok: false,
107
+ code: result.code
108
+ }, { status });
109
+ }
110
+ return json({
111
+ ok: true,
112
+ walletSession: projectActiveWalletSession(result.authorization),
113
+ operationCredential: result.operationCredential,
114
+ custodyEnvelope: result.custodyEnvelope,
115
+ near: await deps.prepareNear(result)
116
+ }, { status: 200 });
117
+ } catch {
118
+ return json({
119
+ ok: false,
120
+ code: "verification_unavailable"
121
+ }, { status: 503 });
122
+ }
123
+ }
124
+ //#endregion
125
+ export { handleVoiceIdChallengeRequest, handleVoiceIdVerifyRequest, handleWalletVoiceIdChallenge, handleWalletVoiceIdVerify };
126
+
127
+ //# sourceMappingURL=walletVoiceIdSessions.js.map
@@ -0,0 +1,110 @@
1
+ import { parseWalletAuthMethodId, parseWalletAuthorityId, parseWalletId } from "../utils/domainIds.js";
2
+ import { parseWalletSessionMintId } from "./capabilityKinds.js";
3
+ import { isPlainObject } from "../utils/validation.js";
4
+ import { base64UrlDecode, base64UrlEncode } from "../utils/base64.js";
5
+ import { alphabetizeStringify } from "../utils/digests.js";
6
+ import { parseDigestB64u } from "../utils/canonicalPrimitives.js";
7
+ //#region ../shared-ts/src/authorization/voiceIdDeviceAssertion.ts
8
+ const CHALLENGE_DOMAIN = "seams:wallet-session:voiceid-device:v1";
9
+ const MAXIMUM_CHALLENGE_MS = 6e4;
10
+ function parseVoiceIdDevicePublicKeyB64u(value) {
11
+ if (typeof value !== "string") throw new Error("VoiceID public key is invalid");
12
+ const bytes = base64UrlDecode(value);
13
+ if (bytes.length !== 32 || base64UrlEncode(bytes) !== value) throw new Error("VoiceID public key must be canonical Ed25519 bytes");
14
+ return value;
15
+ }
16
+ function exactKeys(value, keys) {
17
+ if (Object.keys(value).length !== keys.length) return false;
18
+ for (const key of keys) if (!Object.hasOwn(value, key)) return false;
19
+ return true;
20
+ }
21
+ function requireOrigin(value, label) {
22
+ if (typeof value !== "string") throw new Error(`${label} must be an origin`);
23
+ let origin;
24
+ let protocol;
25
+ try {
26
+ const parsed = new URL(value);
27
+ origin = parsed.origin;
28
+ protocol = parsed.protocol;
29
+ } catch {
30
+ throw new Error(`${label} must be an origin`);
31
+ }
32
+ if (origin !== value || protocol !== "http:" && protocol !== "https:") throw new Error(`${label} must be a canonical HTTP origin`);
33
+ return origin;
34
+ }
35
+ function requireWindow(issuedAtMs, expiresAtMs) {
36
+ if (!Number.isSafeInteger(issuedAtMs) || !Number.isSafeInteger(expiresAtMs) || Number(issuedAtMs) <= 0 || Number(expiresAtMs) <= Number(issuedAtMs) || Number(expiresAtMs) - Number(issuedAtMs) > MAXIMUM_CHALLENGE_MS) throw new Error("VoiceID challenge has an invalid validity window");
37
+ }
38
+ function buildVoiceIdWalletSessionChallenge(fields) {
39
+ requireWindow(fields.issuedAtMs, fields.expiresAtMs);
40
+ requireOrigin(fields.requestOrigin, "VoiceID requestOrigin");
41
+ requireOrigin(fields.audience, "VoiceID audience");
42
+ return {
43
+ kind: "voiceid_wallet_session_challenge_v1",
44
+ walletId: fields.walletId,
45
+ walletAuthMethodId: fields.walletAuthMethodId,
46
+ walletAuthorityId: fields.walletAuthorityId,
47
+ deviceKeyId: fields.deviceKeyId,
48
+ challengeId: fields.challengeId,
49
+ nonce: fields.nonce,
50
+ requestOrigin: fields.requestOrigin,
51
+ audience: fields.audience,
52
+ issuedAtMs: fields.issuedAtMs,
53
+ expiresAtMs: fields.expiresAtMs
54
+ };
55
+ }
56
+ function parseVoiceIdWalletSessionChallenge(raw) {
57
+ if (!isPlainObject(raw) || !exactKeys(raw, [
58
+ "kind",
59
+ "walletId",
60
+ "walletAuthMethodId",
61
+ "walletAuthorityId",
62
+ "deviceKeyId",
63
+ "challengeId",
64
+ "nonce",
65
+ "requestOrigin",
66
+ "audience",
67
+ "issuedAtMs",
68
+ "expiresAtMs"
69
+ ]) || raw.kind !== "voiceid_wallet_session_challenge_v1") throw new Error("Invalid VoiceID Wallet Session challenge");
70
+ const walletId = parseWalletId(raw.walletId);
71
+ const walletAuthMethodId = parseWalletAuthMethodId(raw.walletAuthMethodId);
72
+ const walletAuthorityId = parseWalletAuthorityId(raw.walletAuthorityId);
73
+ const challengeId = parseWalletSessionMintId(raw.challengeId);
74
+ if (!walletId.ok || !walletAuthMethodId.ok || !walletAuthorityId.ok || !challengeId.ok) throw new Error("VoiceID challenge identity is invalid");
75
+ if (typeof raw.issuedAtMs !== "number" || typeof raw.expiresAtMs !== "number") throw new Error("VoiceID challenge times must be numbers");
76
+ return buildVoiceIdWalletSessionChallenge({
77
+ walletId: walletId.value,
78
+ walletAuthMethodId: walletAuthMethodId.value,
79
+ walletAuthorityId: walletAuthorityId.value,
80
+ deviceKeyId: parseDigestB64u(raw.deviceKeyId),
81
+ challengeId: challengeId.value,
82
+ nonce: parseDigestB64u(raw.nonce),
83
+ requestOrigin: requireOrigin(raw.requestOrigin, "VoiceID requestOrigin"),
84
+ audience: requireOrigin(raw.audience, "VoiceID audience"),
85
+ issuedAtMs: raw.issuedAtMs,
86
+ expiresAtMs: raw.expiresAtMs
87
+ });
88
+ }
89
+ function parseVoiceIdDeviceAssertion(raw) {
90
+ if (!isPlainObject(raw) || !exactKeys(raw, [
91
+ "kind",
92
+ "challenge",
93
+ "signatureB64u"
94
+ ]) || raw.kind !== "voiceid_device_assertion_v1" || typeof raw.signatureB64u !== "string") throw new Error("Invalid VoiceID device assertion");
95
+ const signature = base64UrlDecode(raw.signatureB64u);
96
+ if (signature.length !== 64 || base64UrlEncode(signature) !== raw.signatureB64u) throw new Error("VoiceID signature must be canonical Ed25519 bytes");
97
+ return {
98
+ kind: "voiceid_device_assertion_v1",
99
+ challenge: parseVoiceIdWalletSessionChallenge(raw.challenge),
100
+ signatureB64u: raw.signatureB64u
101
+ };
102
+ }
103
+ function voiceIdWalletSessionChallengeBytes(challenge) {
104
+ const payload = alphabetizeStringify(challenge);
105
+ return new TextEncoder().encode(`${CHALLENGE_DOMAIN}|${payload}`);
106
+ }
107
+ //#endregion
108
+ export { buildVoiceIdWalletSessionChallenge, parseVoiceIdDeviceAssertion, parseVoiceIdDevicePublicKeyB64u, parseVoiceIdWalletSessionChallenge, voiceIdWalletSessionChallengeBytes };
109
+
110
+ //# sourceMappingURL=voiceIdDeviceAssertion.js.map
@@ -1,6 +1,7 @@
1
1
  import { wireLiteral, wireObject } from "../utils/wireSchema.js";
2
2
  import { authorizationDeviceId, digest, keyLabeled, parseUnixTime, sessionId, walletAuthMethodId, walletAuthorityId, walletId, walletSessionAuthorizationId, walletSessionId } from "./wireFields.js";
3
3
  import "./activeWalletSession.js";
4
+ import "../utils/walletAuthMethodRecord.js";
4
5
  import { parseWalletSignerActivationSetV1 } from "../authorization/walletAuthority.js";
5
6
  import "./walletSessionCredentialDelivery.js";
6
7
  //#region ../shared-ts/src/device-linking/authorityActivation.ts
@@ -16,7 +16,7 @@ function parseCommittedAuthorityPackagesV1(raw) {
16
16
  const authority = parseWalletAuthorityV1(record.authority);
17
17
  if (!authority.ok || authority.value.state !== "pending_local_install") throw new Error("CommittedAuthorityPackagesV1.authority is not pending");
18
18
  const authMethod = parseWalletAuthMethodRecordV2(record.authMethod);
19
- if (!authMethod || authMethod.status !== "pending_local_install") throw new Error("CommittedAuthorityPackagesV1.authMethod is not pending");
19
+ if (!authMethod || authMethod.status !== "pending_local_install" || authMethod.kind === "voiceid") throw new Error("CommittedAuthorityPackagesV1.authMethod is not pending");
20
20
  if (authMethod.walletId !== authority.value.walletId || authMethod.walletAuthorityId !== authority.value.authorityId) throw new Error("CommittedAuthorityPackagesV1 authority and auth method identities differ");
21
21
  const signerPackages = parseCommittedSignerPackageSetV1(record.signerPackages);
22
22
  assertSignerPackagesMatchAuthority(signerPackages, authority.value.signerActivations);
@@ -7,6 +7,13 @@ import { parsePasskeyCustodySecretBinding } from "./custodySecretBinding.js";
7
7
  const WALLET_CUSTODY_ENVELOPE_VERSION_V2 = "wallet_custody_envelope_v2";
8
8
  const PASSKEY_PRF_KEK_VERSION_V1 = "passkey_prf_kek_hkdf_sha256_v1";
9
9
  const EMAIL_OTP_FACTOR_KEK_VERSION_V1 = "email_otp_factor_kek_hkdf_sha256_v1";
10
+ const VOICEID_DEVICE_FACTOR_KEK_VERSION_V1 = "voiceid_device_factor_kek_hkdf_sha256_v1";
11
+ function isActiveMethodBoundWalletSeedEnvelope(envelope) {
12
+ return envelope.ownership.kind === "method_bound" && envelope.binding.kind === "wallet_custody_seed_v1" && envelope.lifecycle.state === "active";
13
+ }
14
+ function isVoiceIdSeedEnvelope(envelope) {
15
+ return isActiveMethodBoundWalletSeedEnvelope(envelope) && envelope.factor.kind === "voiceid";
16
+ }
10
17
  const ENVELOPE_OWNERSHIP_FIELDS = ["kind", "walletAuthMethodId"];
11
18
  function parseWalletCustodyEnvelopeOwnership(raw, label) {
12
19
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`${label} must be an object`);
@@ -67,6 +74,13 @@ function buildEmailOtpEnvelopeFactor(args) {
67
74
  kekVersion: EMAIL_OTP_FACTOR_KEK_VERSION_V1
68
75
  };
69
76
  }
77
+ function buildVoiceIdEnvelopeFactor(args) {
78
+ return {
79
+ kind: "voiceid",
80
+ deviceKeyId: args.deviceKeyId,
81
+ kekVersion: VOICEID_DEVICE_FACTOR_KEK_VERSION_V1
82
+ };
83
+ }
70
84
  function buildActiveEnvelopeLifecycle(args) {
71
85
  return {
72
86
  state: "active",
@@ -153,7 +167,16 @@ const EMAIL_OTP_FACTOR_FIELDS = [
153
167
  "enrollmentSealKeyVersion",
154
168
  "kekVersion"
155
169
  ];
156
- const ALL_FACTOR_FIELDS = Array.from(new Set([...PASSKEY_FACTOR_FIELDS, ...EMAIL_OTP_FACTOR_FIELDS]));
170
+ const VOICEID_FACTOR_FIELDS = [
171
+ "kind",
172
+ "deviceKeyId",
173
+ "kekVersion"
174
+ ];
175
+ const ALL_FACTOR_FIELDS = Array.from(new Set([
176
+ ...PASSKEY_FACTOR_FIELDS,
177
+ ...EMAIL_OTP_FACTOR_FIELDS,
178
+ ...VOICEID_FACTOR_FIELDS
179
+ ]));
157
180
  function requireNonEmptyString(value, label) {
158
181
  if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is required`);
159
182
  return value;
@@ -180,7 +203,11 @@ function parseWalletCustodyEnvelopeFactor(raw, label = "factor") {
180
203
  enrollmentId: requireNonEmptyString(record.enrollmentId, `${label}.enrollmentId`),
181
204
  enrollmentSealKeyVersion: requireNonEmptyString(record.enrollmentSealKeyVersion, `${label}.enrollmentSealKeyVersion`)
182
205
  });
183
- default: throw new Error(`${label}.kind must be passkey or email_otp`);
206
+ case "voiceid":
207
+ rejectUnknownFields(record, VOICEID_FACTOR_FIELDS, label, ALL_FACTOR_FIELDS);
208
+ if (record.kekVersion !== VOICEID_DEVICE_FACTOR_KEK_VERSION_V1) throw new Error(`${label}.kekVersion must be ${VOICEID_DEVICE_FACTOR_KEK_VERSION_V1}`);
209
+ return buildVoiceIdEnvelopeFactor({ deviceKeyId: parseDigestField(record.deviceKeyId, `${label}.deviceKeyId`) });
210
+ default: throw new Error(`${label}.kind must be passkey, email_otp, or voiceid`);
184
211
  }
185
212
  }
186
213
  const ENVELOPE_RECORD_FIELDS = [
@@ -269,6 +296,6 @@ function admitWalletCustodyEnvelopeOwnershipReplacementV1(input) {
269
296
  }
270
297
  }
271
298
  //#endregion
272
- export { WALLET_CUSTODY_ENVELOPE_VERSION_V2, admitWalletCustodyEnvelopeOwnershipReplacementV1, buildActiveEnvelopeLifecycle, buildEmailOtpEnvelopeFactor, buildPasskeyCustodyEnvelopeRecord, buildPasskeyEnvelopeFactor, buildRevokedEnvelopeLifecycle, parsePasskeyCustodyEnvelopeRecord, parseWalletCustodyEnvelopeFactor, parseWalletCustodyEnvelopeOwnershipWireV1, sameWalletCustodyEnvelopeOwnership };
299
+ export { WALLET_CUSTODY_ENVELOPE_VERSION_V2, admitWalletCustodyEnvelopeOwnershipReplacementV1, buildActiveEnvelopeLifecycle, buildEmailOtpEnvelopeFactor, buildPasskeyCustodyEnvelopeRecord, buildPasskeyEnvelopeFactor, buildRevokedEnvelopeLifecycle, buildVoiceIdEnvelopeFactor, isActiveMethodBoundWalletSeedEnvelope, isVoiceIdSeedEnvelope, parsePasskeyCustodyEnvelopeRecord, parseWalletCustodyEnvelopeFactor, parseWalletCustodyEnvelopeOwnershipWireV1, sameWalletCustodyEnvelopeOwnership };
273
300
 
274
301
  //# sourceMappingURL=custodyEnvelope.js.map
@@ -172,6 +172,6 @@ function parsePasskeyCustodySecretBinding(raw, label = "passkeyCustodySecretBind
172
172
  }
173
173
  }
174
174
  //#endregion
175
- export { buildEd25519YaoClientRootBinding, parsePasskeyCustodySecretBinding };
175
+ export { buildEd25519YaoClientRootBinding, buildWalletCustodySeedBinding, parsePasskeyCustodySecretBinding };
176
176
 
177
177
  //# sourceMappingURL=custodySecretBinding.js.map