@seams/wallet-server 0.44.0 → 0.46.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 (35) hide show
  1. package/README.md +4 -4
  2. package/artifact-manifest.json +11 -11
  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/router/build-stamp.json +1 -1
  6. package/cloudflare-router-ab/build/signing-worker/build-stamp.json +1 -1
  7. package/cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json +1 -1
  8. package/dist/esm/authorization/factorEvidence.js +37 -1
  9. package/dist/esm/authorization/voiceIdDeviceProof.js +32 -6
  10. package/dist/esm/hosted-wallet-gateway.js +1 -1
  11. package/dist/esm/router/cloudflare/d1/auth/d1RouterApiAuthService.js +1 -0
  12. package/dist/esm/router/cloudflare/d1/wallet/d1VoiceIdSessions.js +34 -1
  13. package/dist/esm/router/cloudflare.js +317 -100
  14. package/dist/esm/router/domains/ed25519Yao/export/routerAbEd25519YaoExport.js +181 -14
  15. package/dist/esm/router/domains/ed25519Yao/export/routerAbEd25519YaoExportRequestScopedCloudflare.js +21 -5
  16. package/dist/esm/router/domains/signingOperations/routerAbOperationStepUp.js +4 -0
  17. package/dist/esm/router/transport/fetch/routes/ecdsaOperationStepUpFactor.js +51 -1
  18. package/dist/esm/router/transport/fetch/routes/thresholdEcdsa.js +3 -1
  19. package/dist/esm/shared-ts/src/authorization/capabilityKinds.js +2 -1
  20. package/dist/esm/shared-ts/src/authorization/voiceIdDeviceAssertion.js +70 -1
  21. package/dist/esm/shared-ts/src/utils/routerAbEcdsaDerivation.js +18 -1
  22. package/dist/esm/shared-ts/src/utils/routerAbEd25519YaoDigests.js +8 -1
  23. package/dist/types/shared-ts/src/authorization/capabilityKinds.d.ts +2 -1
  24. package/dist/types/shared-ts/src/authorization/voiceIdDeviceAssertion.d.ts +30 -0
  25. package/dist/types/shared-ts/src/utils/routerAbEcdsaDerivation.d.ts +11 -1
  26. package/dist/types/shared-ts/src/utils/routerAbEd25519YaoDigests.d.ts +3 -0
  27. package/dist/types/wallet-server/src/authorization/domain.d.ts +3 -0
  28. package/dist/types/wallet-server/src/authorization/factorEvidence.d.ts +11 -1
  29. package/dist/types/wallet-server/src/authorization/voiceIdDeviceProof.d.ts +27 -1
  30. package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1VoiceIdSessions.d.ts +3 -1
  31. package/dist/types/wallet-server/src/router/domains/ed25519Yao/export/routerAbEd25519YaoExport.d.ts +36 -2
  32. package/dist/types/wallet-server/src/router/domains/voiceId/voiceIdSessions.d.ts +23 -1
  33. package/dist/types/wallet-server/src/router/framework/authServicePort.d.ts +1 -0
  34. package/dist/types/wallet-server/src/router/transport/fetch/routes/ecdsaOperationStepUpFactor.d.ts +2 -1
  35. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { parseEmailOtpChallengeId, parseThresholdEd25519SessionId, parseWalletId, parseWebAuthnCredentialIdB64u, parseWebAuthnRpId } from "../../../../shared-ts/src/utils/domainIds.js";
1
+ import { parseEmailOtpChallengeId, parseThresholdEd25519SessionId, parseWalletAuthMethodId, parseWalletId, parseWebAuthnCredentialIdB64u, parseWebAuthnRpId } from "../../../../shared-ts/src/utils/domainIds.js";
2
2
  import { isPlainObject } from "../../../../shared-ts/src/utils/validation.js";
3
3
  import { base64UrlEncode } from "../../../../shared-ts/src/utils/base64.js";
4
4
  import { alphabetizeStringify, sha256BytesUtf8 } from "../../../../shared-ts/src/utils/digests.js";
@@ -10,8 +10,9 @@ import { sameRouterAbMpcMaterialActivationRef } from "../../../../shared-ts/src/
10
10
  import { deriveRouterAbEd25519YaoExportAuthorizationDigestV1, deriveRouterAbEd25519YaoExportConfirmationDigestV1, deriveRouterAbEd25519YaoRuntimePolicyBindingV1 } from "../../../../shared-ts/src/utils/routerAbEd25519YaoDigests.js";
11
11
  import { ROUTER_AB_ED25519_YAO_EXPORT_ADMISSION_PATH_V1, ROUTER_AB_ED25519_YAO_EXPORT_EXECUTE_PATH_V1, parseRouterAbEd25519YaoExportAdmissionReceiptV1, parseRouterAbEd25519YaoExportAdmissionRequestV1, parseRouterAbEd25519YaoExportExecuteRequestV1, parseRouterAbEd25519YaoExportResultV1 } from "../../../../shared-ts/src/utils/routerAbEd25519Yao.js";
12
12
  import { walletAuthAuthorityRef } from "../../../../shared-ts/src/utils/walletAuthAuthority.js";
13
+ import { parseVoiceIdOperationAssertion } from "../../../../shared-ts/src/authorization/voiceIdDeviceAssertion.js";
13
14
  import { parseSessionOrigin, parseVerifiedOwnerProofId } from "../../../../authorization/domain.js";
14
- import { buildVerifiedWalletOperationEmailOtpFactorResult, buildVerifiedWalletOperationPasskeyFactorResult } from "../../../../authorization/factorEvidence.js";
15
+ import { buildVerifiedWalletOperationEmailOtpFactorResult, buildVerifiedWalletOperationPasskeyFactorResult, buildVerifiedWalletOperationVoiceIdFactorResult } from "../../../../authorization/factorEvidence.js";
15
16
  import { WALLET_EMAIL_OTP_EXPORT_OPERATION } from "../../../../shared-ts/src/utils/emailOtpDomain.js";
16
17
  import { parseWebAuthnAuthenticationCredential, webAuthnCredentialIdB64uFromCredential } from "../../../auth/webAuthnCredentialCodecs.js";
17
18
  import { normalizeCorsOrigin } from "../../../../core/SessionService.js";
@@ -657,7 +658,8 @@ function exportPasskeyCredentialId(authorization) {
657
658
  const parsed = webAuthnCredentialIdB64uFromCredential(authorization.webauthnAuthentication);
658
659
  return parsed.ok ? parsed.credentialIdB64u : null;
659
660
  }
660
- case "email_otp_factor": return null;
661
+ case "email_otp_factor":
662
+ case "voiceid_device": return null;
661
663
  default: return assertNeverExportAdmissionAuthorization(authorization);
662
664
  }
663
665
  }
@@ -671,6 +673,10 @@ function exportAuthorizationDigestAuthority(authorization, passkeyCredentialId)
671
673
  kind: "email_otp",
672
674
  providerSubjectId: authorization.providerSubjectId
673
675
  };
676
+ case "voiceid_device": return {
677
+ kind: "voiceid",
678
+ deviceKeyId: authorization.assertion.statement.deviceKeyId
679
+ };
674
680
  default: return assertNeverExportAdmissionAuthorization(authorization);
675
681
  }
676
682
  }
@@ -831,6 +837,14 @@ async function authorizeExportAdmission(args) {
831
837
  expiresAtMs: authorization.expires_at_ms
832
838
  });
833
839
  }
840
+ if (args.input.authorization.kind === "voiceid_device") return await authorizeVoiceIdExportAdmission({
841
+ walletId: walletId.value,
842
+ input: args.input,
843
+ authorization: args.input.authorization,
844
+ voiceId: args.voiceId,
845
+ authorizedOperations: args.authorizedOperations,
846
+ nowMs
847
+ });
834
848
  const authority = await args.walletAuthMethods.resolveActiveEmailOtpAuthorityForVerifiedMethod({
835
849
  walletId: String(walletId.value),
836
850
  walletAuthMethodId: args.input.authorization.walletAuthMethodId,
@@ -865,6 +879,77 @@ async function authorizeExportAdmission(args) {
865
879
  factorRelease: checked.factorRelease
866
880
  };
867
881
  }
882
+ async function verifyVoiceIdExportProof(args) {
883
+ const walletAuthMethodId = parseWalletAuthMethodId(args.authorization.walletAuthMethodId);
884
+ let requestOrigin;
885
+ let operationChallenge;
886
+ try {
887
+ requestOrigin = parseSessionOrigin(args.input.expectedOrigin);
888
+ operationChallenge = parseDigestB64u(base64UrlEncode(Uint8Array.from(args.input.body.authorization.confirmation_digest)));
889
+ } catch {
890
+ return authorizationFailure({
891
+ status: 403,
892
+ code: "export_authorization_invalid",
893
+ message: "Ed25519 Yao export VoiceID scope is invalid"
894
+ });
895
+ }
896
+ if (!walletAuthMethodId.ok) return authorizationFailure({
897
+ status: 403,
898
+ code: "invalid_body",
899
+ message: walletAuthMethodId.error.message
900
+ });
901
+ let verified;
902
+ try {
903
+ verified = await args.voiceId.verifyVoiceIdOperation({
904
+ walletId: args.walletId,
905
+ walletAuthMethodId: walletAuthMethodId.value,
906
+ assertion: args.authorization.assertion,
907
+ operationKind: "near.export_key",
908
+ operationChallenge,
909
+ requestOrigin,
910
+ nowMs: args.nowMs
911
+ });
912
+ } catch {
913
+ return authorizationFailure({
914
+ status: 503,
915
+ code: "export_voiceid_unavailable",
916
+ message: "VoiceID export verification is unavailable"
917
+ });
918
+ }
919
+ if (verified.kind === "rejected") return authorizationFailure({
920
+ status: 403,
921
+ code: verified.code === "method_unavailable" ? "export_voiceid_method_unavailable" : "export_voiceid_not_verified",
922
+ message: "Fresh VoiceID export proof was not verified"
923
+ });
924
+ return {
925
+ ok: true,
926
+ verified
927
+ };
928
+ }
929
+ async function authorizeVoiceIdExportAdmission(args) {
930
+ const checked = await verifyVoiceIdExportProof(args);
931
+ if (!checked.ok) return checked;
932
+ const verified = checked.verified;
933
+ const recorded = await recordFreshExportProof({
934
+ walletId: args.walletId,
935
+ input: args.input,
936
+ authority: verified.authority,
937
+ authorizedOperations: args.authorizedOperations,
938
+ voiceIdProof: verified.proof,
939
+ verifiedAtMs: args.nowMs,
940
+ expiresAtMs: Math.min(args.input.body.authorization.expires_at_ms, verified.proof.statement.expiresAtMs)
941
+ });
942
+ if (!recorded.ok) return recorded;
943
+ return {
944
+ ok: true,
945
+ admission: recorded.admission,
946
+ walletId: recorded.walletId,
947
+ custodyRelease: {
948
+ kind: "voiceid_custody_envelope",
949
+ envelope: verified.custodyEnvelope
950
+ }
951
+ };
952
+ }
868
953
  async function recordFreshExportProof(args) {
869
954
  const authorizationIdentitySuffix = base64UrlEncode(Uint8Array.from(args.input.body.authorization.authorization_digest));
870
955
  const principalId = parsePrincipalId(`ed25519-export:${args.walletId}`);
@@ -891,11 +976,9 @@ async function recordFreshExportProof(args) {
891
976
  let challengeId = null;
892
977
  let assertionDigest = null;
893
978
  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
- });
979
+ case "voiceid":
980
+ factorId = parseAuthFactorId(`voiceid:${args.authority.factor.deviceKeyId}`);
981
+ break;
899
982
  case "passkey":
900
983
  factorId = parseAuthFactorId(`passkey:${args.authority.factor.credentialIdB64u}`);
901
984
  try {
@@ -911,14 +994,15 @@ async function recordFreshExportProof(args) {
911
994
  case "email_otp_factor":
912
995
  rawChallengeId = args.input.authorization.challengeId;
913
996
  break;
914
- case "passkey": break;
997
+ case "passkey":
998
+ case "voiceid_device": break;
915
999
  default: assertNeverExportAdmissionAuthorization(args.input.authorization);
916
1000
  }
917
1001
  challengeId = parseEmailOtpChallengeId(rawChallengeId);
918
1002
  break;
919
1003
  }
920
1004
  }
921
- if (!principalId.ok || !capabilityId.ok || !operationId.ok || !evidenceId.ok || !evidenceSetId.ok || !authorizedOperationId.ok || !auditEventId.ok || !factorId.ok || challengeId !== null && !challengeId.ok || args.authority.factor.kind === "passkey" && assertionDigest === null) return authorizationFailure({
1005
+ if (!principalId.ok || !capabilityId.ok || !operationId.ok || !evidenceId.ok || !evidenceSetId.ok || !authorizedOperationId.ok || !auditEventId.ok || !factorId.ok || challengeId !== null && !challengeId.ok || args.authority.factor.kind === "passkey" && assertionDigest === null || args.authority.factor.kind === "voiceid" && !args.voiceIdProof) return authorizationFailure({
922
1006
  status: 403,
923
1007
  code: "export_authorization_invalid",
924
1008
  message: "Ed25519 Yao export owner proof identifiers are invalid"
@@ -974,6 +1058,21 @@ async function recordFreshExportProof(args) {
974
1058
  expiresAtMs: args.expiresAtMs
975
1059
  });
976
1060
  break;
1061
+ case "voiceid":
1062
+ factor = buildVerifiedWalletOperationVoiceIdFactorResult({
1063
+ tenantId: args.authorizedOperations.tenantId,
1064
+ principalId: principalId.value,
1065
+ walletId: args.walletId,
1066
+ requestOrigin,
1067
+ audience: requestOrigin,
1068
+ factorId: factorId.value,
1069
+ authorityRef,
1070
+ operation,
1071
+ proof: requireVoiceIdProof(args.voiceIdProof),
1072
+ verifiedAtMs: args.verifiedAtMs,
1073
+ expiresAtMs: args.expiresAtMs
1074
+ });
1075
+ break;
977
1076
  }
978
1077
  const evidence = await args.authorizedOperations.recordVerifiedWalletOperationFactorEvidenceSet({
979
1078
  operation,
@@ -1003,6 +1102,10 @@ async function recordFreshExportProof(args) {
1003
1102
  })
1004
1103
  };
1005
1104
  }
1105
+ function requireVoiceIdProof(proof) {
1106
+ if (!proof) throw new Error("VoiceID export proof is required");
1107
+ return proof;
1108
+ }
1006
1109
  var ExportOperationAdmission = class {
1007
1110
  constructor(authorizedOperations, operation) {
1008
1111
  this.authorizedOperations = authorizedOperations;
@@ -1053,9 +1156,10 @@ function exportOwnerProofRejected(code) {
1053
1156
  });
1054
1157
  }
1055
1158
  var RouterAbEd25519YaoExportOwnerProofAuthorizationAdapter = class {
1056
- constructor(webAuthn, emailOtp, walletAuthMethods, authorizedOperations, resolveEd25519MaterialActivation, admitEstablishedHome) {
1159
+ constructor(webAuthn, emailOtp, voiceId, walletAuthMethods, authorizedOperations, resolveEd25519MaterialActivation, admitEstablishedHome) {
1057
1160
  this.webAuthn = webAuthn;
1058
1161
  this.emailOtp = emailOtp;
1162
+ this.voiceId = voiceId;
1059
1163
  this.walletAuthMethods = walletAuthMethods;
1060
1164
  this.authorizedOperations = authorizedOperations;
1061
1165
  this.resolveEd25519MaterialActivation = resolveEd25519MaterialActivation;
@@ -1071,6 +1175,7 @@ var RouterAbEd25519YaoExportOwnerProofAuthorizationAdapter = class {
1071
1175
  input,
1072
1176
  webAuthn: this.webAuthn,
1073
1177
  emailOtp: this.emailOtp,
1178
+ voiceId: this.voiceId,
1074
1179
  walletAuthMethods: this.walletAuthMethods,
1075
1180
  authorizedOperations: this.authorizedOperations
1076
1181
  });
@@ -1095,7 +1200,8 @@ var RouterAbEd25519YaoExportOwnerProofAuthorizationAdapter = class {
1095
1200
  authorizationIdentity: activeIdentity.authorizationIdentity,
1096
1201
  ownershipGeneration: home.ownershipGeneration,
1097
1202
  admission: checked.admission,
1098
- ...checked.factorRelease ? { factorRelease: checked.factorRelease } : {}
1203
+ ...checked.factorRelease ? { factorRelease: checked.factorRelease } : {},
1204
+ ...checked.custodyRelease ? { custodyRelease: checked.custodyRelease } : {}
1099
1205
  };
1100
1206
  }
1101
1207
  authorizeExecution(input) {
@@ -1112,6 +1218,29 @@ var RouterAbEd25519YaoExportOwnerProofAuthorizationAdapter = class {
1112
1218
  authorizationIdentity
1113
1219
  } : checked;
1114
1220
  }
1221
+ async releaseVoiceIdCustodyForAuthorizedExport(input) {
1222
+ const walletId = parseWalletId(input.body.application_binding.wallet_id);
1223
+ if (input.authorization.kind !== "voiceid_device" || !walletId.ok) return authorizationFailure({
1224
+ status: 403,
1225
+ code: "export_authorization_method_mismatch",
1226
+ message: "Only a VoiceID export releases a VoiceID custody envelope"
1227
+ });
1228
+ const checked = await verifyVoiceIdExportProof({
1229
+ walletId: walletId.value,
1230
+ input,
1231
+ authorization: input.authorization,
1232
+ voiceId: this.voiceId,
1233
+ nowMs: Date.now()
1234
+ });
1235
+ if (!checked.ok) return checked;
1236
+ return {
1237
+ ok: true,
1238
+ custodyRelease: {
1239
+ kind: "voiceid_custody_envelope",
1240
+ envelope: checked.verified.custodyEnvelope
1241
+ }
1242
+ };
1243
+ }
1115
1244
  async resolveAuthorizationIdentity(body) {
1116
1245
  return await resolveActiveExportAuthorizationIdentity({
1117
1246
  body,
@@ -1127,7 +1256,7 @@ const ROUTES = Object.freeze([defineRoute({
1127
1256
  auth: {
1128
1257
  plane: "public",
1129
1258
  proof: "challenge_exchange",
1130
- rationale: "Exact-seed export requires fresh passkey assertion or Email OTP factor possession."
1259
+ rationale: "Exact-seed export requires a fresh passkey assertion, Email OTP factor possession or VoiceID device proof."
1131
1260
  },
1132
1261
  metering: { kind: "none" },
1133
1262
  requiredServices: [],
@@ -1213,6 +1342,42 @@ function parseEmailOtpExportAdmissionAuthorization(authorization) {
1213
1342
  }
1214
1343
  };
1215
1344
  }
1345
+ function parseVoiceIdExportAdmissionAuthorization(authorization) {
1346
+ const unexpectedField = firstUnexpectedField(authorization, [
1347
+ "kind",
1348
+ "walletAuthMethodId",
1349
+ "assertion"
1350
+ ]);
1351
+ if (unexpectedField) return {
1352
+ ok: false,
1353
+ message: `VoiceID export authorization has unknown field: ${unexpectedField}`
1354
+ };
1355
+ let assertion;
1356
+ try {
1357
+ assertion = parseVoiceIdOperationAssertion(authorization.assertion);
1358
+ } catch (error) {
1359
+ return {
1360
+ ok: false,
1361
+ message: error instanceof Error ? error.message : "VoiceID export assertion is invalid"
1362
+ };
1363
+ }
1364
+ if (typeof authorization.walletAuthMethodId !== "string" || authorization.walletAuthMethodId !== assertion.statement.walletAuthMethodId) return {
1365
+ ok: false,
1366
+ message: "walletAuthMethodId must name the signing VoiceID method"
1367
+ };
1368
+ if (assertion.statement.operationKind !== "near.export_key") return {
1369
+ ok: false,
1370
+ message: "VoiceID export assertion names another operation"
1371
+ };
1372
+ return {
1373
+ ok: true,
1374
+ value: {
1375
+ kind: "voiceid_device",
1376
+ walletAuthMethodId: authorization.walletAuthMethodId,
1377
+ assertion
1378
+ }
1379
+ };
1380
+ }
1216
1381
  function parseExportAdmissionAuthorization(value) {
1217
1382
  if (!isPlainObject(value)) return {
1218
1383
  ok: false,
@@ -1221,6 +1386,7 @@ function parseExportAdmissionAuthorization(value) {
1221
1386
  switch (value.kind) {
1222
1387
  case "passkey": return parsePasskeyExportAdmissionAuthorization(value);
1223
1388
  case "email_otp_factor": return parseEmailOtpExportAdmissionAuthorization(value);
1389
+ case "voiceid_device": return parseVoiceIdExportAdmissionAuthorization(value);
1224
1390
  default: return {
1225
1391
  ok: false,
1226
1392
  message: "export admission authorization kind is invalid"
@@ -1311,7 +1477,8 @@ var RouterAbEd25519YaoExportRouteExtension = class {
1311
1477
  const result = await this.service.admitExport(parsed.protocol, authorized.authorizationIdentity, authorized.ownershipGeneration, authorized.admission, authorized.factorRelease ?? null, traceContext.value);
1312
1478
  return result.ok ? json({
1313
1479
  protocol: result.value,
1314
- ...authorized.factorRelease ? { factorRelease: authorized.factorRelease } : {}
1480
+ ...authorized.factorRelease ? { factorRelease: authorized.factorRelease } : {},
1481
+ ...authorized.custodyRelease ? { voiceIdCustodyEnvelope: authorized.custodyRelease.envelope } : {}
1315
1482
  }, { status: result.status }) : json({
1316
1483
  ok: false,
1317
1484
  code: result.code,
@@ -165,7 +165,8 @@ async function handleAdmissionRequest(context, raw) {
165
165
  const result = await runAdmission(context, parsed.protocol, admission.authorizationIdentity);
166
166
  if (result.ok) return json({
167
167
  protocol: result.value,
168
- ...admission.factorRelease ? { factorRelease: admission.factorRelease } : {}
168
+ ...admission.factorRelease ? { factorRelease: admission.factorRelease } : {},
169
+ ...admission.custodyRelease ? { voiceIdCustodyEnvelope: admission.custodyRelease.envelope } : {}
169
170
  }, { status: result.status });
170
171
  return exportResponse(result);
171
172
  }
@@ -204,10 +205,22 @@ async function runAuthorization(context, parsed, expectedOrigin) {
204
205
  const resolved = await context.input.authorization.resolveAuthorizationIdentity(parsed.protocol);
205
206
  if (!resolved.ok) return resolved;
206
207
  if (!sameAuthorizationIdentity(resolved.authorizationIdentity, existingAuthority.authorizationIdentity)) return authorizationFailure(409, "export_authorization_conflict", "Export authorization owner changed for an existing request");
208
+ let custodyRelease = null;
209
+ if (parsed.authorization.kind === "voiceid_device") {
210
+ const released = await context.input.authorization.releaseVoiceIdCustodyForAuthorizedExport({
211
+ request: context.input.request,
212
+ body: parsed.protocol,
213
+ authorization: parsed.authorization,
214
+ expectedOrigin
215
+ });
216
+ if (!released.ok) return released;
217
+ custodyRelease = released.custodyRelease;
218
+ }
207
219
  verified = {
208
220
  identity: existingAuthority.authorizationIdentity,
209
221
  ownershipGeneration: existingAuthority.ownershipGeneration,
210
- admission: null
222
+ admission: null,
223
+ ...custodyRelease ? { custodyRelease } : {}
211
224
  };
212
225
  } else {
213
226
  let authorized;
@@ -226,7 +239,8 @@ async function runAuthorization(context, parsed, expectedOrigin) {
226
239
  identity: authorized.authorizationIdentity,
227
240
  ownershipGeneration: authorized.ownershipGeneration,
228
241
  admission: authorized.admission,
229
- ...authorized.factorRelease ? { factorRelease: authorized.factorRelease } : {}
242
+ ...authorized.factorRelease ? { factorRelease: authorized.factorRelease } : {},
243
+ ...authorized.custodyRelease ? { custodyRelease: authorized.custodyRelease } : {}
230
244
  };
231
245
  }
232
246
  const authorizationFingerprint = await authorizationFingerprintForIdentity(parsed, verified.identity);
@@ -236,7 +250,8 @@ async function runAuthorization(context, parsed, expectedOrigin) {
236
250
  identity: verified.identity,
237
251
  ownershipGeneration: verified.ownershipGeneration,
238
252
  admission: verified.admission,
239
- ...decision.value.factorRelease ? { factorRelease: decision.value.factorRelease } : {}
253
+ ...decision.value.factorRelease ? { factorRelease: decision.value.factorRelease } : {},
254
+ ...verified.custodyRelease ? { custodyRelease: verified.custodyRelease } : {}
240
255
  }) : decision.value;
241
256
  if (!verified.admission) return authorizationFailure(503, "export_authorization_uncertain", "Export authorization changed while it was being replayed");
242
257
  const prepared = await verified.admission.prepare();
@@ -264,7 +279,8 @@ function authorizedRun(verified) {
264
279
  return {
265
280
  ok: true,
266
281
  authorizationIdentity: verified.identity,
267
- ...verified.factorRelease ? { factorRelease: verified.factorRelease } : {}
282
+ ...verified.factorRelease ? { factorRelease: verified.factorRelease } : {},
283
+ ...verified.custodyRelease ? { custodyRelease: verified.custodyRelease } : {}
268
284
  };
269
285
  }
270
286
  async function persistAuthorizationUncertain(context, request) {
@@ -266,6 +266,10 @@ function walletExecutionLaneAuthSourceFromAuthority(authority) {
266
266
  kind: "passkey",
267
267
  credentialIdB64u: authority.factor.credentialIdB64u
268
268
  };
269
+ if (authority.factor.kind === "voiceid") return {
270
+ kind: "voiceid",
271
+ deviceKeyId: authority.factor.deviceKeyId
272
+ };
269
273
  const providerSubject = parseProviderSubject(authority.factor.providerUserId);
270
274
  return providerSubject.ok ? {
271
275
  kind: "oidc_provider",
@@ -4,7 +4,8 @@ import { alphabetizeStringify, sha256BytesUtf8 } from "../../../../shared-ts/src
4
4
  import { parseDigestB64u } from "../../../../shared-ts/src/utils/canonicalPrimitives.js";
5
5
  import "../../../../shared-ts/src/utils/encoders.js";
6
6
  import { parseAuthFactorId } from "../../../../shared-ts/src/authorization/capabilityKinds.js";
7
- import { buildVerifiedWalletOperationEmailOtpFactorResult, buildVerifiedWalletOperationPasskeyFactorResult } from "../../../../authorization/factorEvidence.js";
7
+ import { walletAuthAuthoritiesMatch } from "../../../../shared-ts/src/utils/walletAuthAuthority.js";
8
+ import { buildVerifiedWalletOperationEmailOtpFactorResult, buildVerifiedWalletOperationPasskeyFactorResult, buildVerifiedWalletOperationVoiceIdFactorResult } from "../../../../authorization/factorEvidence.js";
8
9
  import { EMAIL_OTP_CHANNEL, WALLET_EMAIL_OTP_EXPORT_OPERATION, WALLET_EMAIL_OTP_TRANSACTION_SIGN_OPERATION } from "../../../../shared-ts/src/utils/emailOtpDomain.js";
9
10
  import { hashEmailOtpOperationBinding } from "../../../domains/emailOtp/emailOtpSessionRouteHelpers.js";
10
11
  import { json } from "../../../framework/http.js";
@@ -152,6 +153,55 @@ async function verifyEcdsaOperationStepUpFactor(input) {
152
153
  } : { kind: "not_requested" }
153
154
  };
154
155
  }
156
+ case "voiceid": {
157
+ if (input.operation.operation.operationKind !== "evm.export_key" || input.authSource.kind !== "voiceid" || proof.authority.factor.deviceKeyId !== input.authSource.deviceKeyId) return {
158
+ ok: false,
159
+ response: json({
160
+ ok: false,
161
+ code: "scope_mismatch",
162
+ message: "VoiceID authority changed"
163
+ }, { status: 403 })
164
+ };
165
+ const verified = await input.voiceId.verifyVoiceIdOperation({
166
+ walletId: input.walletId,
167
+ walletAuthMethodId: proof.authority.bindingId,
168
+ assertion: proof.assertion,
169
+ operationKind: "evm.export_key",
170
+ operationChallenge: parseDigestB64u(input.expectedChallenge),
171
+ requestOrigin: input.requestOrigin,
172
+ nowMs: Date.now()
173
+ });
174
+ if (verified.kind === "rejected" || !walletAuthAuthoritiesMatch(verified.authority, proof.authority)) return {
175
+ ok: false,
176
+ response: json({
177
+ ok: false,
178
+ code: "not_verified",
179
+ message: "VoiceID operation proof was not verified"
180
+ }, { status: 401 })
181
+ };
182
+ const verifiedAtMs = Date.now();
183
+ const expiresAtMs = Math.min(input.expiresAtMs, verified.proof.statement.expiresAtMs);
184
+ if (expiresAtMs <= verifiedAtMs) return expiredStepUpFactor();
185
+ const factorId = parseAuthFactorId(`voiceid:${proof.authority.factor.deviceKeyId}`);
186
+ if (!factorId.ok) throw new Error(factorId.error.message);
187
+ return {
188
+ ok: true,
189
+ factor: buildVerifiedWalletOperationVoiceIdFactorResult({
190
+ tenantId: input.operation.tenantId,
191
+ principalId: input.operation.principalId,
192
+ walletId: input.walletId,
193
+ requestOrigin: input.requestOrigin,
194
+ audience: input.requestOrigin,
195
+ factorId: factorId.value,
196
+ authorityRef: input.authorityRef,
197
+ operation: input.operation,
198
+ proof: verified.proof,
199
+ verifiedAtMs,
200
+ expiresAtMs
201
+ }),
202
+ unseal: { kind: "not_requested" }
203
+ };
204
+ }
155
205
  default: throw new Error("Unsupported ECDSA operation step-up proof");
156
206
  }
157
207
  }
@@ -31,6 +31,7 @@ async function verifyActiveEcdsaOperationStepUpAuthority(walletAuthMethods, proo
31
31
  switch (proof.kind) {
32
32
  case "passkey": return await walletAuthMethods.verifyActivePasskeyAuthority(proof.authority);
33
33
  case "email_otp": return await walletAuthMethods.verifyActiveEmailOtpAuthority(proof.authority);
34
+ case "voiceid": return { ok: true };
34
35
  default: throw new Error("Unsupported ECDSA operation step-up proof");
35
36
  }
36
37
  }
@@ -569,7 +570,8 @@ async function issueEcdsaOperationStepUpAuthorization(input) {
569
570
  expectedChallenge,
570
571
  expiresAtMs,
571
572
  webAuthn: input.ctx.service.webAuthn,
572
- emailOtp: input.ctx.service.emailOtp
573
+ emailOtp: input.ctx.service.emailOtp,
574
+ voiceId: input.ctx.service.walletUnlock
573
575
  });
574
576
  if (!verifiedFactor.ok) return verifiedFactor.response;
575
577
  const evidenceSet = await input.ctx.service.authorizedOperations.recordVerifiedWalletOperationFactorEvidenceSet({
@@ -22,7 +22,8 @@ const EVM_ECDSA_MPC_OPERATION_KINDS = {
22
22
  const AUTHORIZATION_EVIDENCE_KINDS = {
23
23
  seamsSession: "seams_session",
24
24
  passkeyAssertion: "passkey_assertion",
25
- emailOtp: "email_otp"
25
+ emailOtp: "email_otp",
26
+ voiceIdDevice: "voiceid_device"
26
27
  };
27
28
  const AUTHORIZATION_GRANT_KINDS = { walletSession: "wallet_session_authorization" };
28
29
  const CAPABILITY_KIND_VALUES = Object.values(CAPABILITY_KINDS);
@@ -104,7 +104,76 @@ function voiceIdWalletSessionChallengeBytes(challenge) {
104
104
  const payload = alphabetizeStringify(challenge);
105
105
  return new TextEncoder().encode(`${CHALLENGE_DOMAIN}|${payload}`);
106
106
  }
107
+ const OPERATION_DOMAIN = "seams:wallet-operation:voiceid-device:v1";
108
+ function parseVoiceIdOperationKind(value) {
109
+ if (value === "near.export_key" || value === "evm.export_key") return value;
110
+ throw new Error("VoiceID operation kind is unsupported");
111
+ }
112
+ function buildVoiceIdOperationStatement(fields) {
113
+ requireWindow(fields.issuedAtMs, fields.expiresAtMs);
114
+ requireOrigin(fields.requestOrigin, "VoiceID requestOrigin");
115
+ return {
116
+ kind: "voiceid_operation_statement_v1",
117
+ walletId: fields.walletId,
118
+ walletAuthMethodId: fields.walletAuthMethodId,
119
+ walletAuthorityId: fields.walletAuthorityId,
120
+ deviceKeyId: fields.deviceKeyId,
121
+ operationKind: parseVoiceIdOperationKind(fields.operationKind),
122
+ operationChallenge: parseDigestB64u(fields.operationChallenge),
123
+ requestOrigin: fields.requestOrigin,
124
+ issuedAtMs: fields.issuedAtMs,
125
+ expiresAtMs: fields.expiresAtMs
126
+ };
127
+ }
128
+ function parseVoiceIdOperationStatement(raw) {
129
+ if (!isPlainObject(raw) || !exactKeys(raw, [
130
+ "kind",
131
+ "walletId",
132
+ "walletAuthMethodId",
133
+ "walletAuthorityId",
134
+ "deviceKeyId",
135
+ "operationKind",
136
+ "operationChallenge",
137
+ "requestOrigin",
138
+ "issuedAtMs",
139
+ "expiresAtMs"
140
+ ]) || raw.kind !== "voiceid_operation_statement_v1") throw new Error("Invalid VoiceID operation statement");
141
+ const walletId = parseWalletId(raw.walletId);
142
+ const walletAuthMethodId = parseWalletAuthMethodId(raw.walletAuthMethodId);
143
+ const walletAuthorityId = parseWalletAuthorityId(raw.walletAuthorityId);
144
+ if (!walletId.ok || !walletAuthMethodId.ok || !walletAuthorityId.ok) throw new Error("VoiceID operation identity is invalid");
145
+ if (typeof raw.issuedAtMs !== "number" || typeof raw.expiresAtMs !== "number") throw new Error("VoiceID operation times must be numbers");
146
+ return buildVoiceIdOperationStatement({
147
+ walletId: walletId.value,
148
+ walletAuthMethodId: walletAuthMethodId.value,
149
+ walletAuthorityId: walletAuthorityId.value,
150
+ deviceKeyId: parseDigestB64u(raw.deviceKeyId),
151
+ operationKind: parseVoiceIdOperationKind(raw.operationKind),
152
+ operationChallenge: parseDigestB64u(raw.operationChallenge),
153
+ requestOrigin: requireOrigin(raw.requestOrigin, "VoiceID requestOrigin"),
154
+ issuedAtMs: raw.issuedAtMs,
155
+ expiresAtMs: raw.expiresAtMs
156
+ });
157
+ }
158
+ function parseVoiceIdOperationAssertion(raw) {
159
+ if (!isPlainObject(raw) || !exactKeys(raw, [
160
+ "kind",
161
+ "statement",
162
+ "signatureB64u"
163
+ ]) || raw.kind !== "voiceid_operation_assertion_v1" || typeof raw.signatureB64u !== "string") throw new Error("Invalid VoiceID operation assertion");
164
+ const signature = base64UrlDecode(raw.signatureB64u);
165
+ if (signature.length !== 64 || base64UrlEncode(signature) !== raw.signatureB64u) throw new Error("VoiceID signature must be canonical Ed25519 bytes");
166
+ return {
167
+ kind: "voiceid_operation_assertion_v1",
168
+ statement: parseVoiceIdOperationStatement(raw.statement),
169
+ signatureB64u: raw.signatureB64u
170
+ };
171
+ }
172
+ function voiceIdOperationStatementBytes(statement) {
173
+ const payload = alphabetizeStringify(statement);
174
+ return new TextEncoder().encode(`${OPERATION_DOMAIN}|${payload}`);
175
+ }
107
176
  //#endregion
108
- export { buildVoiceIdWalletSessionChallenge, parseVoiceIdDeviceAssertion, parseVoiceIdDevicePublicKeyB64u, parseVoiceIdWalletSessionChallenge, voiceIdWalletSessionChallengeBytes };
177
+ export { buildVoiceIdWalletSessionChallenge, parseVoiceIdDeviceAssertion, parseVoiceIdDevicePublicKeyB64u, parseVoiceIdOperationAssertion, parseVoiceIdWalletSessionChallenge, voiceIdOperationStatementBytes, voiceIdWalletSessionChallengeBytes };
109
178
 
110
179
  //# sourceMappingURL=voiceIdDeviceAssertion.js.map
@@ -9,7 +9,8 @@ import { requireRouterAbX25519PublicKey } from "./routerAbPublicKeyset.js";
9
9
  import { canonicalRouterAbMpcMaterialActivationRefBytes, canonicalRouterAbNormalSigningAuthorizationBytes, parseRouterAbMpcMaterialActivationRef, parseRouterAbNormalSigningAuthorization, routerAbMpcMaterialActivationRefToWire, sameRouterAbMpcMaterialActivationRef } from "./routerAbNormalSigningIdentity.js";
10
10
  import { normalizeRuntimePolicyScope } from "../threshold/signingRootScope.js";
11
11
  import { parseEcdsaServerGeneration } from "./ecdsaCapabilityActivation.js";
12
- import { isEmailOtpWalletAuthAuthority, isPasskeyWalletAuthAuthority, parseWalletAuthAuthority } from "./walletAuthAuthority.js";
12
+ import { isEmailOtpWalletAuthAuthority, isPasskeyWalletAuthAuthority, isVoiceIdWalletAuthAuthority, parseWalletAuthAuthority } from "./walletAuthAuthority.js";
13
+ import { parseVoiceIdOperationAssertion } from "../authorization/voiceIdDeviceAssertion.js";
13
14
  //#region ../shared-ts/src/utils/routerAbEcdsaDerivation.ts
14
15
  /**
15
16
  * The longest an ECDSA presignature ceremony may run. The Gateway authorizes a
@@ -1463,6 +1464,22 @@ function parseRouterAbEcdsaOperationStepUpAuthorizationRequestV1(value) {
1463
1464
  otp_code: requireAsciiNonEmptyString(proof.otp_code, "operationStepUpAuthorizationRequest.proof.otp_code")
1464
1465
  };
1465
1466
  break;
1467
+ case "voiceid": {
1468
+ requireExactKeys(proof, "operationStepUpAuthorizationRequest.proof", [
1469
+ "kind",
1470
+ "authority",
1471
+ "assertion"
1472
+ ]);
1473
+ if (!authority || !isVoiceIdWalletAuthAuthority(authority)) throw new Error("operationStepUpAuthorizationRequest.proof requires an exact VoiceID authority");
1474
+ const assertion = parseVoiceIdOperationAssertion(proof.assertion);
1475
+ if (assertion.statement.walletId !== authority.walletId || assertion.statement.walletAuthMethodId !== authority.bindingId || assertion.statement.deviceKeyId !== authority.factor.deviceKeyId) throw new Error("operationStepUpAuthorizationRequest.proof names another VoiceID method");
1476
+ parsedProof = {
1477
+ kind: "voiceid",
1478
+ authority,
1479
+ assertion
1480
+ };
1481
+ break;
1482
+ }
1466
1483
  default: throw new Error("operationStepUpAuthorizationRequest.proof.kind is invalid");
1467
1484
  }
1468
1485
  return {
@@ -139,6 +139,13 @@ async function deriveRouterAbEd25519YaoExportConfirmationDigestV1(input) {
139
139
  ]);
140
140
  return Array.from(await sha256(encoded));
141
141
  }
142
+ function exportAuthoritySubject(authority) {
143
+ switch (authority.kind) {
144
+ case "passkey": return requireVisibleIdentifier(authority.credentialIdB64u, "credentialIdB64u");
145
+ case "email_otp": return requireVisibleIdentifier(authority.providerSubjectId, "providerSubjectId");
146
+ case "voiceid": return requireVisibleIdentifier(authority.deviceKeyId, "deviceKeyId");
147
+ }
148
+ }
142
149
  async function deriveRouterAbEd25519YaoExportAuthorizationDigestV1(input) {
143
150
  const confirmationDigest = requireBytes32(input.confirmationDigest, "export confirmation digest", true);
144
151
  const nonce = requireBytes32(input.nonce, "export authorization nonce", true);
@@ -150,7 +157,7 @@ async function deriveRouterAbEd25519YaoExportAuthorizationDigestV1(input) {
150
157
  labeledField("issuedAtMs", u64BigEndian(input.issuedAtMs)),
151
158
  labeledField("expiresAtMs", u64BigEndian(input.expiresAtMs)),
152
159
  labeledField("authorityKind", UTF8.encode(input.authority.kind)),
153
- labeledField("authoritySubject", UTF8.encode(input.authority.kind === "passkey" ? requireVisibleIdentifier(input.authority.credentialIdB64u, "credentialIdB64u") : requireVisibleIdentifier(input.authority.providerSubjectId, "providerSubjectId")))
160
+ labeledField("authoritySubject", UTF8.encode(exportAuthoritySubject(input.authority)))
154
161
  ]);
155
162
  return Array.from(await sha256(encoded));
156
163
  }
@@ -48,9 +48,10 @@ export declare const AUTHORIZATION_EVIDENCE_KINDS: {
48
48
  readonly seamsSession: 'seams_session';
49
49
  readonly passkeyAssertion: 'passkey_assertion';
50
50
  readonly emailOtp: 'email_otp';
51
+ readonly voiceIdDevice: 'voiceid_device';
51
52
  };
52
53
  type SessionAuthorizationEvidenceKind = typeof AUTHORIZATION_EVIDENCE_KINDS.seamsSession;
53
- type InteractiveAuthorizationEvidenceKind = typeof AUTHORIZATION_EVIDENCE_KINDS.passkeyAssertion | typeof AUTHORIZATION_EVIDENCE_KINDS.emailOtp;
54
+ type InteractiveAuthorizationEvidenceKind = typeof AUTHORIZATION_EVIDENCE_KINDS.passkeyAssertion | typeof AUTHORIZATION_EVIDENCE_KINDS.emailOtp | typeof AUTHORIZATION_EVIDENCE_KINDS.voiceIdDevice;
54
55
  export type AuthorizationEvidenceKind = SessionAuthorizationEvidenceKind | InteractiveAuthorizationEvidenceKind;
55
56
  export type AuthorizationEvidenceRequirement = {
56
57
  readonly mode: 'all' | 'any';