@seams/wallet-server 0.20.0 → 0.21.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 (71) hide show
  1. package/artifact-manifest.json +22 -21
  2. package/cloudflare-router-ab/build/deriver-a/build-stamp.json +1 -1
  3. package/cloudflare-router-ab/build/deriver-a/index_bg.wasm +0 -0
  4. package/cloudflare-router-ab/build/deriver-b/build-stamp.json +1 -1
  5. package/cloudflare-router-ab/build/deriver-b/index_bg.wasm +0 -0
  6. package/cloudflare-router-ab/build/local-tools/linux-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/router/index_bg.wasm +0 -0
  9. package/cloudflare-router-ab/build/signing-worker/build-stamp.json +1 -1
  10. package/cloudflare-router-ab/build/signing-worker/index_bg.wasm +0 -0
  11. package/cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json +1 -1
  12. package/cloudflare-router-ab/build/tenant-root-control-plane/index_bg.wasm +0 -0
  13. package/cloudflare-router-ab/scripts/prepare-local-runtime-config.mjs +16 -2
  14. package/dist/esm/core/deviceLinking/linkedDeviceManagement.js +11 -30
  15. package/dist/esm/core/emailOtpD1Statements.js +14 -2
  16. package/dist/esm/core/nearRelayerAccountProvisioning.js +2 -7
  17. package/dist/esm/hosted-wallet-gateway.js +10 -7
  18. package/dist/esm/router/auth/webAuthnCredentialCodecs.js +16 -1
  19. package/dist/esm/router/cloudflare/d1/auth/d1AuthorizationAssembly.js +1 -0
  20. package/dist/esm/router/cloudflare/d1/auth/d1RouterApiAuthService.js +0 -1
  21. package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceEmailOtpTargetFactor.js +4 -1
  22. package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceManagementService.js +6 -11
  23. package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceRevocation.js +7 -0
  24. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpGrantStore.js +5 -2
  25. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRateLimitStore.js +18 -0
  26. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRecoveryService.js +28 -8
  27. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpServerSealRuntime.js +12 -8
  28. package/dist/esm/router/cloudflare/d1/oidc/d1OidcBoundary.js +1 -0
  29. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.js +19 -5
  30. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.js +26 -10
  31. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletRecoveryGoogleEmailOtpService.js +4 -1
  32. package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodService.js +6 -0
  33. package/dist/esm/router/cloudflare/d1/wallet/d1WalletRelocationApprovals.js +12 -0
  34. package/dist/esm/router/cloudflare.js +252 -150
  35. package/dist/esm/router/domains/ecdsa/routerAbEcdsaStrictRegistration.js +1 -1
  36. package/dist/esm/router/domains/passkeyCustody/passkeyCustodyEnvelopeRetrieval.js +4 -22
  37. package/dist/esm/router/domains/passkeyCustody/registrationCustodyOutcome.js +8 -2
  38. package/dist/esm/router/framework/routeDefinitions.js +3 -2
  39. package/dist/esm/router/transport/fetch/routes/auth.js +5 -0
  40. package/dist/esm/router/transport/fetch/routes/passkeyCustody.js +78 -88
  41. package/dist/esm/router/transport/fetch/routes/sessions.js +4 -1
  42. package/dist/esm/router/transport/fetch/routes/thresholdEd25519.js +4 -1
  43. package/dist/esm/router/transport/fetch/routes/walletCustodyRequestBoundary.js +80 -0
  44. package/dist/esm/shared-ts/src/passkey-custody/ceremonyCommitPayload.js +5 -1
  45. package/dist/esm/shared-ts/src/utils/evmRpcResults.js +5 -3
  46. package/dist/esm/shared-ts/src/utils/routerAbEcdsaDerivation.js +2 -1
  47. package/dist/esm/threshold/session/signingSessionSeal/crypto/cipher.js +160 -40
  48. package/dist/types/shared-ts/src/authorization/walletCustodyOperation.d.ts +1 -1
  49. package/dist/types/shared-ts/src/device-linking/contracts.d.ts +1 -7
  50. package/dist/types/shared-ts/src/passkey-custody/ceremonyCommitPayload.d.ts +5 -0
  51. package/dist/types/shared-ts/src/utils/evmRpcResults.d.ts +3 -3
  52. package/dist/types/shared-ts/src/utils/routerAbEcdsaDerivation.d.ts +1 -1
  53. package/dist/types/wallet-server/src/core/authService/emailOtpSeal.d.ts +5 -1
  54. package/dist/types/wallet-server/src/core/deviceLinking/linkedDeviceManagement.d.ts +1 -9
  55. package/dist/types/wallet-server/src/core/emailOtpD1Statements.d.ts +2 -1
  56. package/dist/types/wallet-server/src/hosted-wallet-gateway.d.ts +1 -0
  57. package/dist/types/wallet-server/src/router/auth/webAuthnCredentialCodecs.d.ts +1 -0
  58. package/dist/types/wallet-server/src/router/cloudflare/d1/deviceLinking/d1LinkedDeviceManagementService.d.ts +0 -3
  59. package/dist/types/wallet-server/src/router/cloudflare/d1/deviceLinking/d1LinkedDeviceRevocation.d.ts +1 -1
  60. package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpGrantStore.d.ts +3 -1
  61. package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpRateLimitStore.d.ts +15 -2
  62. package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpServerSealRuntime.d.ts +3 -3
  63. package/dist/types/wallet-server/src/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.d.ts +3 -0
  64. package/dist/types/wallet-server/src/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.d.ts +5 -6
  65. package/dist/types/wallet-server/src/router/domains/passkeyCustody/walletCustodyRegistrationCommit.d.ts +4 -6
  66. package/dist/types/wallet-server/src/router/framework/authServicePort.d.ts +7 -1
  67. package/dist/types/wallet-server/src/router/transport/fetch/routes/passkeyCustody.d.ts +1 -10
  68. package/dist/types/wallet-server/src/router/transport/fetch/routes/walletCustodyRequestBoundary.d.ts +28 -0
  69. package/dist/types/wallet-server/src/threshold/session/signingSessionSeal/crypto/cipher.d.ts +3 -0
  70. package/package.json +1 -1
  71. package/src/README.md +11 -4
@@ -1245,7 +1245,9 @@ function wireLabeled(parse) {
1245
1245
  }
1246
1246
  };
1247
1247
  }
1248
- Object.freeze([
1248
+ //#endregion
1249
+ //#region ../shared-ts/src/authorization/delegatedAuthority.ts
1250
+ const FULL_OWNER_PERMISSIONS = Object.freeze([
1249
1251
  "export_keys",
1250
1252
  "link_devices",
1251
1253
  "revoke_devices",
@@ -1308,6 +1310,19 @@ function parseDelegatedWalletAuthorityV1(raw) {
1308
1310
  value: buildDelegatedWalletAuthorityV1({ permissions: permissions.value })
1309
1311
  };
1310
1312
  }
1313
+ function buildFullOwnerPermissionsV1() {
1314
+ return buildPresetPermissionSet(FULL_OWNER_PERMISSIONS);
1315
+ }
1316
+ function sameDelegatedWalletAuthorityV1(left, right) {
1317
+ if (left.kind !== right.kind || left.permissions.length !== right.permissions.length) return false;
1318
+ for (let index = 0; index < left.permissions.length; index += 1) if (left.permissions[index] !== right.permissions[index]) return false;
1319
+ return true;
1320
+ }
1321
+ function buildPresetPermissionSet(permissions) {
1322
+ const parsed = parseDelegatedWalletPermissionSetV1(permissions);
1323
+ if (!parsed.ok) throw new Error(parsed.error.message);
1324
+ return parsed.value;
1325
+ }
1311
1326
  function compareDelegatedWalletPermissions(left, right) {
1312
1327
  return left < right ? -1 : left > right ? 1 : 0;
1313
1328
  }
@@ -2791,6 +2806,7 @@ function parseRouterAbEcdsaOperationStepUpWebAuthnCredentialV1(value) {
2791
2806
  "signature",
2792
2807
  "userHandle"
2793
2808
  ]);
2809
+ if (credential.clientExtensionResults !== null) throw new Error("webauthn_authentication.clientExtensionResults must be redacted");
2794
2810
  const authenticatorAttachment = credential.authenticatorAttachment === null ? null : requireAsciiNonEmptyString(credential.authenticatorAttachment, "webauthn_authentication.authenticatorAttachment");
2795
2811
  const userHandle = response.userHandle === null ? null : requireAsciiNonEmptyString(response.userHandle, "webauthn_authentication.response.userHandle");
2796
2812
  return {
@@ -2804,7 +2820,7 @@ function parseRouterAbEcdsaOperationStepUpWebAuthnCredentialV1(value) {
2804
2820
  signature: requireAsciiNonEmptyString(response.signature, "webauthn_authentication.response.signature"),
2805
2821
  userHandle
2806
2822
  },
2807
- clientExtensionResults: credential.clientExtensionResults
2823
+ clientExtensionResults: null
2808
2824
  };
2809
2825
  }
2810
2826
  function parseRouterAbEcdsaOperationStepUpAuthorizationRequestV1(value) {
@@ -5654,7 +5670,7 @@ function createRouterApiRouteDefinitions(options = {}) {
5654
5670
  }, ROUTER_API_WALLET_REGISTRATION_SESSION_SERVICES, {
5655
5671
  kind: "event",
5656
5672
  action: "wallet_created"
5657
- }), publicRoute("wallet_recovery_status", "GET", "/wallets/:walletId/recovery/status", "Report how many recovery codes remain and whether the owner saved them", {
5673
+ }), publicRoute("wallet_recovery_status", "POST", "/wallets/:walletId/recovery/status", "Report how many recovery codes remain and whether the owner saved them", {
5658
5674
  plane: "public",
5659
5675
  proof: "webauthn",
5660
5676
  rationale: "Fresh operation-bound owner proof is required."
@@ -5668,7 +5684,8 @@ function createRouterApiRouteDefinitions(options = {}) {
5668
5684
  rationale: "Fresh operation-bound owner proof is required."
5669
5685
  }, ROUTER_API_PASSKEY_CUSTODY_SERVICES), publicRoute("wallet_recovery_backup_acknowledge", "POST", "/wallets/recovery/acknowledge-backup", "Record that the owner saved their recovery codes", {
5670
5686
  plane: "public",
5671
- rationale: "Acknowledgement only clears backup-reminder state after the client displays its locally retained codes."
5687
+ proof: "webauthn",
5688
+ rationale: "Fresh operation-bound owner proof is required after the client displays its locally retained codes."
5672
5689
  }, ["passkeyCustody"]), publicRoute("wallet_recovery_finalize", "POST", "/wallets/recovery/finalize", "Finalize an admitted wallet recovery and install its replacement credential", {
5673
5690
  plane: "public",
5674
5691
  proof: "recovery_proof",
@@ -14819,65 +14836,21 @@ function b64uField(prefix) {
14819
14836
  }
14820
14837
  b64uField("seed"), b64uField("canonicalSeed"), b64uField("xClientBase"), b64uField("clientOutputMask"), b64uField("clientRecoverableSecret"), b64uField("prfFirst"), b64uField("prfSecond"), b64uField("signingShare32"), b64uField("clientNonceHandle");
14821
14838
  //#endregion
14822
- //#region src/core/authService/webauthnOidcHelpers.ts
14823
- function originHostnameOrEmpty(origin) {
14824
- try {
14825
- return new URL(origin).hostname.toLowerCase();
14826
- } catch {
14827
- return "";
14828
- }
14829
- }
14830
- function isHostWithinRpId(host, rpId) {
14831
- const h = (host || "").toLowerCase();
14832
- const r = String(rpId).toLowerCase();
14833
- if (!h || !r) return false;
14834
- if ((process.env.NO_CADDY === "1" || process.env.VITE_NO_CADDY === "1") && (h === "localhost" || h === "127.0.0.1") && r.endsWith(".localhost")) return true;
14835
- return h === r || h.endsWith(`.${r}`);
14836
- }
14837
- //#endregion
14838
- //#region ../shared-ts/src/wallet-recovery/recoveryCodeReservation.ts
14839
- function parseRecoveryCodeReservationId(value) {
14840
- if (typeof value !== "string" || value.length === 0 || value.length > 512 || value.trim() !== value || hasWhitespaceOrControlCharacters(value)) throw new Error("recovery code reservation id must be a compact opaque identifier");
14841
- return value;
14842
- }
14843
- //#endregion
14844
- //#region ../shared-ts/src/authorization/walletCustodyOperation.ts
14845
- /** Public challenge digest for a single wallet-administration factor proof. */
14846
- async function computeWalletCustodyAdminChallengeDigest(input) {
14847
- return sha256Utf8DigestB64u(`seams:wallet-custody:challenge:v1|${alphabetizeStringify({
14848
- walletId: input.walletId,
14849
- operation: input.operation,
14850
- payload: input.payload,
14851
- requestOrigin: input.requestOrigin
14852
- })}`);
14839
+ //#region src/router/auth/webAuthnCredentialCodecs.ts
14840
+ function containsWebAuthnExtensionOutput(value) {
14841
+ if (value == null) return false;
14842
+ if (!isPlainObject(value)) return true;
14843
+ for (const nested of Object.values(value)) if (containsWebAuthnExtensionOutput(nested)) return true;
14844
+ return false;
14853
14845
  }
14854
- //#endregion
14855
- //#region ../shared-ts/src/wallet-recovery/walletRecoveryTarget.ts
14856
- function parseWalletRecoveryTargetV1(raw) {
14857
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("wallet recovery target must be an object");
14858
- const record = raw;
14859
- const fields = Object.keys(record);
14860
- switch (record.kind) {
14861
- case "passkey": {
14862
- if (fields.length !== 2 || !fields.includes("rpId")) throw new Error("Passkey recovery target fields are invalid");
14863
- const rpId = parseWebAuthnRpId(record.rpId);
14864
- if (!rpId.ok) throw new Error("Passkey recovery target RP ID is invalid");
14865
- return {
14866
- kind: "passkey",
14867
- rpId: rpId.value
14868
- };
14869
- }
14870
- case "google_email_otp":
14871
- if (fields.length !== 2 || !fields.includes("googleProvider") || record.googleProvider !== "google") throw new Error("Google Email OTP recovery target fields are invalid");
14872
- return {
14873
- kind: "google_email_otp",
14874
- googleProvider: "google"
14875
- };
14876
- default: throw new Error("wallet recovery target kind is unsupported");
14877
- }
14846
+ function unredactedWebAuthnExtensionField(input) {
14847
+ if (!isPlainObject(input)) return null;
14848
+ if (containsWebAuthnExtensionOutput(input.clientExtensionResults)) return "clientExtensionResults";
14849
+ const response = input.response;
14850
+ if (!isPlainObject(response)) return null;
14851
+ if (containsWebAuthnExtensionOutput(response.clientExtensionResults)) return "response.clientExtensionResults";
14852
+ return null;
14878
14853
  }
14879
- //#endregion
14880
- //#region src/router/auth/webAuthnCredentialCodecs.ts
14881
14854
  function decodeWebAuthnBase64UrlOrBase64(input, fieldName) {
14882
14855
  try {
14883
14856
  return base64UrlDecode(input);
@@ -14964,6 +14937,137 @@ function parseWebAuthnAuthenticationCredential$1(input) {
14964
14937
  };
14965
14938
  }
14966
14939
  //#endregion
14940
+ //#region src/router/transport/fetch/routes/walletCustodyRequestBoundary.ts
14941
+ function requireExactObjectFields(value, fields, label) {
14942
+ const keys = Object.keys(value).sort();
14943
+ const expected = [...fields].sort();
14944
+ if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new Error(`${label} contains unsupported fields`);
14945
+ }
14946
+ function parseCustodyOwnerProof(value) {
14947
+ if (!isPlainObject(value)) throw new Error("factorProof is required");
14948
+ const kind = normalizeOptionalTrimmedString(value.kind);
14949
+ if (kind === "passkey") {
14950
+ requireExactObjectFields(value, [
14951
+ "kind",
14952
+ "walletId",
14953
+ "rpId",
14954
+ "credentialIdB64u",
14955
+ "challenge_digest",
14956
+ "webauthn_authentication"
14957
+ ], "factorProof");
14958
+ const extensionField = unredactedWebAuthnExtensionField(value.webauthn_authentication);
14959
+ if (extensionField) throw new Error(`factorProof.webauthn_authentication.${extensionField} must be redacted`);
14960
+ const webauthnAuthentication = parseWebAuthnAuthenticationCredential$1(value.webauthn_authentication);
14961
+ if (!webauthnAuthentication) throw new Error("factorProof.webauthn_authentication is invalid");
14962
+ return {
14963
+ kind,
14964
+ walletId: requireTrimmedString(value.walletId, "factorProof.walletId"),
14965
+ rpId: requireTrimmedString(value.rpId, "factorProof.rpId"),
14966
+ credentialIdB64u: requireTrimmedString(value.credentialIdB64u, "factorProof.credentialIdB64u"),
14967
+ challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest"),
14968
+ webauthn_authentication: webauthnAuthentication
14969
+ };
14970
+ }
14971
+ if (kind === "email_otp") {
14972
+ requireExactObjectFields(value, [
14973
+ "kind",
14974
+ "provider_subject_id",
14975
+ "challenge_id",
14976
+ "otp_code",
14977
+ "challenge_digest"
14978
+ ], "factorProof");
14979
+ return {
14980
+ kind,
14981
+ provider_subject_id: requireTrimmedString(value.provider_subject_id, "factorProof.provider_subject_id"),
14982
+ challenge_id: requireTrimmedString(value.challenge_id, "factorProof.challenge_id"),
14983
+ otp_code: requireTrimmedString(value.otp_code, "factorProof.otp_code"),
14984
+ challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest")
14985
+ };
14986
+ }
14987
+ throw new Error("factorProof.kind must be passkey or email_otp");
14988
+ }
14989
+ function parseRecoveryBackupAcknowledgementRequest(body) {
14990
+ if (!isPlainObject(body)) throw new Error("acknowledgement body is required");
14991
+ requireExactObjectFields(body, [
14992
+ "expectedIssuedAtMs",
14993
+ "factorProof",
14994
+ "requestId",
14995
+ "walletId"
14996
+ ], "recovery backup acknowledgement");
14997
+ return {
14998
+ walletId: requireTrimmedString(body.walletId, "walletId"),
14999
+ requestId: requireTrimmedString(body.requestId, "requestId"),
15000
+ expectedIssuedAtMs: parseUnixMs$1(body.expectedIssuedAtMs, "expectedIssuedAtMs"),
15001
+ factorProof: body.factorProof
15002
+ };
15003
+ }
15004
+ function parseRecoveryStatusRequest(body) {
15005
+ if (!isPlainObject(body)) throw new Error("status body is required");
15006
+ requireExactObjectFields(body, ["factorProof", "requestId"], "recovery status");
15007
+ return {
15008
+ requestId: requireTrimmedString(body.requestId, "requestId"),
15009
+ factorProof: body.factorProof
15010
+ };
15011
+ }
15012
+ //#endregion
15013
+ //#region src/core/authService/webauthnOidcHelpers.ts
15014
+ function originHostnameOrEmpty(origin) {
15015
+ try {
15016
+ return new URL(origin).hostname.toLowerCase();
15017
+ } catch {
15018
+ return "";
15019
+ }
15020
+ }
15021
+ function isHostWithinRpId(host, rpId) {
15022
+ const h = (host || "").toLowerCase();
15023
+ const r = String(rpId).toLowerCase();
15024
+ if (!h || !r) return false;
15025
+ if ((process.env.NO_CADDY === "1" || process.env.VITE_NO_CADDY === "1") && (h === "localhost" || h === "127.0.0.1") && r.endsWith(".localhost")) return true;
15026
+ return h === r || h.endsWith(`.${r}`);
15027
+ }
15028
+ //#endregion
15029
+ //#region ../shared-ts/src/wallet-recovery/recoveryCodeReservation.ts
15030
+ function parseRecoveryCodeReservationId(value) {
15031
+ if (typeof value !== "string" || value.length === 0 || value.length > 512 || value.trim() !== value || hasWhitespaceOrControlCharacters(value)) throw new Error("recovery code reservation id must be a compact opaque identifier");
15032
+ return value;
15033
+ }
15034
+ //#endregion
15035
+ //#region ../shared-ts/src/authorization/walletCustodyOperation.ts
15036
+ /** Public challenge digest for a single wallet-administration factor proof. */
15037
+ async function computeWalletCustodyAdminChallengeDigest(input) {
15038
+ return sha256Utf8DigestB64u(`seams:wallet-custody:challenge:v1|${alphabetizeStringify({
15039
+ walletId: input.walletId,
15040
+ operation: input.operation,
15041
+ payload: input.payload,
15042
+ requestOrigin: input.requestOrigin
15043
+ })}`);
15044
+ }
15045
+ //#endregion
15046
+ //#region ../shared-ts/src/wallet-recovery/walletRecoveryTarget.ts
15047
+ function parseWalletRecoveryTargetV1(raw) {
15048
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("wallet recovery target must be an object");
15049
+ const record = raw;
15050
+ const fields = Object.keys(record);
15051
+ switch (record.kind) {
15052
+ case "passkey": {
15053
+ if (fields.length !== 2 || !fields.includes("rpId")) throw new Error("Passkey recovery target fields are invalid");
15054
+ const rpId = parseWebAuthnRpId(record.rpId);
15055
+ if (!rpId.ok) throw new Error("Passkey recovery target RP ID is invalid");
15056
+ return {
15057
+ kind: "passkey",
15058
+ rpId: rpId.value
15059
+ };
15060
+ }
15061
+ case "google_email_otp":
15062
+ if (fields.length !== 2 || !fields.includes("googleProvider") || record.googleProvider !== "google") throw new Error("Google Email OTP recovery target fields are invalid");
15063
+ return {
15064
+ kind: "google_email_otp",
15065
+ googleProvider: "google"
15066
+ };
15067
+ default: throw new Error("wallet recovery target kind is unsupported");
15068
+ }
15069
+ }
15070
+ //#endregion
14967
15071
  //#region src/router/domains/emailOtp/emailOtpSessionRouteHelpers.ts
14968
15072
  function emailOtpStatusCode(code) {
14969
15073
  if (code === "internal") return 500;
@@ -15256,51 +15360,8 @@ const EMAIL_OTP_CHALLENGE_ROUTE_ID = "wallet_custody_email_otp_challenge";
15256
15360
  const ENVELOPE_OWNERSHIP_UPGRADE_ROUTE_ID = "wallet_custody_envelope_ownership_upgrade";
15257
15361
  const CUSTODY_OPERATION_CAPABILITY_ID = "wallet-custody-admin";
15258
15362
  const CUSTODY_PROOF_TTL_MS = 6e4;
15259
- function requireExactObjectFields(value, fields, label) {
15260
- const keys = Object.keys(value).sort();
15261
- const expected = [...fields].sort();
15262
- if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new Error(`${label} contains unsupported fields`);
15263
- }
15264
- function parseCustodyOwnerProof(value) {
15265
- if (!isPlainObject(value)) throw new Error("factorProof is required");
15266
- const kind = normalizeOptionalTrimmedString(value.kind);
15267
- if (kind === "passkey") {
15268
- requireExactObjectFields(value, [
15269
- "kind",
15270
- "walletId",
15271
- "rpId",
15272
- "credentialIdB64u",
15273
- "challenge_digest",
15274
- "webauthn_authentication"
15275
- ], "factorProof");
15276
- const webauthnAuthentication = parseWebAuthnAuthenticationCredential$1(value.webauthn_authentication);
15277
- if (!webauthnAuthentication) throw new Error("factorProof.webauthn_authentication is invalid");
15278
- return {
15279
- kind,
15280
- walletId: requireTrimmedString(value.walletId, "factorProof.walletId"),
15281
- rpId: requireTrimmedString(value.rpId, "factorProof.rpId"),
15282
- credentialIdB64u: requireTrimmedString(value.credentialIdB64u, "factorProof.credentialIdB64u"),
15283
- challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest"),
15284
- webauthn_authentication: webauthnAuthentication
15285
- };
15286
- }
15287
- if (kind === "email_otp") {
15288
- requireExactObjectFields(value, [
15289
- "kind",
15290
- "provider_subject_id",
15291
- "challenge_id",
15292
- "otp_code",
15293
- "challenge_digest"
15294
- ], "factorProof");
15295
- return {
15296
- kind,
15297
- provider_subject_id: requireTrimmedString(value.provider_subject_id, "factorProof.provider_subject_id"),
15298
- challenge_id: requireTrimmedString(value.challenge_id, "factorProof.challenge_id"),
15299
- otp_code: requireTrimmedString(value.otp_code, "factorProof.otp_code"),
15300
- challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest")
15301
- };
15302
- }
15303
- throw new Error("factorProof.kind must be passkey or email_otp");
15363
+ function hasFullOwnerAuthority$1(authority) {
15364
+ return sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }));
15304
15365
  }
15305
15366
  function parseRequiredAuthorizationValue(parsed) {
15306
15367
  if (!parsed.ok) throw new Error(parsed.error.message);
@@ -15393,8 +15454,13 @@ async function authorizeWalletCustodyOperation(input) {
15393
15454
  } catch (error) {
15394
15455
  return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "passkey authority is invalid");
15395
15456
  }
15396
- const active = await input.ctx.service.walletAuthMethods.verifyActivePasskeyAuthority(authority);
15457
+ const active = await input.ctx.service.walletAuthMethods.resolveActivePasskeyAuthorityForVerifiedCredential({
15458
+ walletId,
15459
+ rpId: authority.verifier.rpId,
15460
+ credentialIdB64u: authority.factor.credentialIdB64u
15461
+ });
15397
15462
  if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
15463
+ if (!hasFullOwnerAuthority$1(active.walletAuthority)) return toCustodyAuthorizationFailure(403, "forbidden", "Full owner authority is required");
15398
15464
  const credential = parseWebAuthnAuthenticationCredential$1(proof.webauthn_authentication);
15399
15465
  if (!credential) return toCustodyAuthorizationFailure(400, "invalid_owner_proof", "passkey assertion is invalid");
15400
15466
  if (String(credential.rawId || credential.id).trim() !== authority.factor.credentialIdB64u) return toCustodyAuthorizationFailure(401, "unauthorized", "passkey credential changed");
@@ -15410,7 +15476,7 @@ async function authorizeWalletCustodyOperation(input) {
15410
15476
  tenantId: operation.tenantId,
15411
15477
  principalId: operation.principalId,
15412
15478
  walletId,
15413
- authorityRef: await walletAuthAuthorityRef({ authority }),
15479
+ authorityRef: await walletAuthAuthorityRef({ authority: active.authority }),
15414
15480
  requestOrigin: origin,
15415
15481
  audience: origin,
15416
15482
  factorId: parseRequiredAuthorizationValue(parseAuthFactorId(`passkey:${authority.factor.credentialIdB64u}`)),
@@ -15559,7 +15625,10 @@ async function completeWalletCustodyOperation(ctx, operation, status, body, resu
15559
15625
  }
15560
15626
  async function handlePasskeyCustody(ctx) {
15561
15627
  if (!matchRoute(ctx, ROUTE_ID)) return null;
15562
- const request = parseWireRequest(await readJsonObject(ctx.request), ctx.request.headers.get("origin"));
15628
+ const body = await readJsonObject(ctx.request);
15629
+ const extensionField = unredactedWebAuthnExtensionField(body?.webauthnAuthentication);
15630
+ if (extensionField) return routeErrorResponse(400, "prf_disclosed", `webauthnAuthentication.${extensionField} must be redacted before retrieval`);
15631
+ const request = parseWireRequest(body, ctx.request.headers.get("origin"));
15563
15632
  if (!request) return routeErrorResponse(400, "invalid_request", "custody retrieval needs a locator, a challenge id, and an assertion");
15564
15633
  return toFetchRouteResponse(await ctx.service.passkeyCustody.retrieveEnvelope(request));
15565
15634
  }
@@ -15584,7 +15653,7 @@ function parseWalletCustodyEmailOtpChallengeRequest(value, actualOrigin) {
15584
15653
  const walletId = parseRequiredAuthorizationValue(parseWalletId(value.walletId));
15585
15654
  const providerSubjectId = parseRequiredAuthorizationValue(parseProviderSubject(value.providerSubjectId));
15586
15655
  const operation = value.operation;
15587
- if (operation !== "credentials_list" && operation !== "credential_label" && operation !== "recovery_rotate" && operation !== "recovery_read") throw new Error("Email OTP custody challenge operation is invalid");
15656
+ if (operation !== "credentials_list" && operation !== "credential_label" && operation !== "recovery_rotate" && operation !== "recovery_read" && operation !== "recovery_status" && operation !== "recovery_backup_acknowledge") throw new Error("Email OTP custody challenge operation is invalid");
15588
15657
  if (!isPlainObject(value.payload)) throw new Error("Email OTP custody challenge payload is invalid");
15589
15658
  if (value.requestOrigin !== void 0 && value.requestOrigin !== actualOrigin) throw new Error("Email OTP custody challenge requestOrigin does not match Origin");
15590
15659
  return {
@@ -16303,6 +16372,8 @@ function parseWalletRecoveryFinalizeBody(value) {
16303
16372
  "ecdsaMaterialPossessionProofs"
16304
16373
  ], "wallet recovery finalization");
16305
16374
  if (!isPlainObject(value.webauthnRegistration)) throw new Error("wallet recovery finalization registration is invalid");
16375
+ const extensionField = unredactedWebAuthnExtensionField(value.webauthnRegistration);
16376
+ if (extensionField) throw new Error(`wallet recovery finalization registration ${extensionField} must be redacted`);
16306
16377
  return {
16307
16378
  kind: "finalize",
16308
16379
  ...identity,
@@ -16336,23 +16407,41 @@ function authorizedOperationReplay(operation) {
16336
16407
  }
16337
16408
  async function handleWalletRecoveryBackupAcknowledge(ctx) {
16338
16409
  if (!matchRoute(ctx, RECOVERY_ACK_ROUTE_ID)) return null;
16339
- const body = await readJsonObject(ctx.request);
16340
- let walletId = "";
16410
+ let request;
16341
16411
  try {
16342
- if (!body) throw new Error("acknowledgement body is required");
16343
- requireExactObjectFields(body, ["walletId"], "recovery backup acknowledgement");
16344
- walletId = requireTrimmedString(body.walletId, "walletId");
16412
+ request = parseRecoveryBackupAcknowledgementRequest(await readJsonObject(ctx.request));
16345
16413
  } catch {
16346
- return routeErrorResponse(400, "invalid_request", "an acknowledgement needs a wallet");
16414
+ return routeErrorResponse(400, "invalid_request", "acknowledgement body is invalid");
16347
16415
  }
16348
- const result = await ctx.service.passkeyCustody.acknowledgeRecoveryBackup({ walletId });
16349
- if (result.kind === "no_recovery_set") return routeErrorResponse(404, "no_recovery_set", "this wallet has no issued recovery codes to acknowledge");
16350
- return toFetchRouteResponse({
16351
- status: 200,
16352
- body: {
16353
- ok: true,
16354
- issuedAtMs: result.issuedAtMs
16355
- }
16416
+ const authorized = await authorizeWalletCustodyOperation({
16417
+ ctx,
16418
+ walletId: request.walletId,
16419
+ operation: "recovery_backup_acknowledge",
16420
+ payload: {
16421
+ walletId: request.walletId,
16422
+ requestId: request.requestId,
16423
+ expectedIssuedAtMs: request.expectedIssuedAtMs
16424
+ },
16425
+ factorProof: request.factorProof
16426
+ });
16427
+ if (!authorized.ok) return authorized.response;
16428
+ const result = await ctx.service.passkeyCustody.acknowledgeRecoveryBackup({
16429
+ walletId: request.walletId,
16430
+ expectedIssuedAtMs: request.expectedIssuedAtMs
16431
+ });
16432
+ if (result.kind === "conflict") return await completeWalletCustodyOperation(ctx, authorized.operation, 409, {
16433
+ ok: false,
16434
+ code: "recovery_set_conflict",
16435
+ message: "the recovery set changed"
16436
+ }, "failed_before_side_effect");
16437
+ if (result.kind === "no_recovery_set") return await completeWalletCustodyOperation(ctx, authorized.operation, 404, {
16438
+ ok: false,
16439
+ code: "no_recovery_set",
16440
+ message: "this wallet has no issued recovery codes to acknowledge"
16441
+ }, "failed_before_side_effect");
16442
+ return await completeWalletCustodyOperation(ctx, authorized.operation, 200, {
16443
+ ok: true,
16444
+ issuedAtMs: result.issuedAtMs
16356
16445
  });
16357
16446
  }
16358
16447
  /**
@@ -16456,39 +16545,42 @@ async function handleWalletRecoveryRead(ctx) {
16456
16545
  storeVersion: result.storeVersion
16457
16546
  });
16458
16547
  }
16459
- /**
16460
- * Reporting recovery status to the wallet's owner.
16461
- *
16462
- * The wallet comes from the path. Origin binding keeps this count endpoint
16463
- * scoped to browser callers without exposing recovery identifiers.
16464
- *
16465
- * Counts only, never identifiers. Which codes remain is not something even
16466
- * the owner's browser needs, and a list would be one leak away from being
16467
- * useful to someone else.
16468
- */
16548
+ /** Reports current recovery-code counts after fresh owner proof. */
16469
16549
  async function handleWalletRecoveryStatus(ctx) {
16470
16550
  const route = matchRoute(ctx, RECOVERY_STATUS_ROUTE_ID);
16471
16551
  if (!route) return null;
16472
16552
  const walletId = walletIdFromPath(route.path, ctx.pathname);
16473
16553
  if (!walletId) return routeErrorResponse(400, "invalid_request", "status needs a wallet");
16474
- const origin = normalizeOptionalTrimmedString(ctx.request.headers.get("origin"));
16554
+ let request;
16475
16555
  try {
16476
- parseSessionOrigin(origin);
16556
+ request = parseRecoveryStatusRequest(await readJsonObject(ctx.request));
16477
16557
  } catch {
16478
- return routeErrorResponse(400, "invalid_origin", "request Origin is invalid");
16558
+ return routeErrorResponse(400, "invalid_request", "status body is invalid");
16479
16559
  }
16560
+ const authorized = await authorizeWalletCustodyOperation({
16561
+ ctx,
16562
+ walletId,
16563
+ operation: "recovery_status",
16564
+ payload: {
16565
+ walletId,
16566
+ requestId: request.requestId
16567
+ },
16568
+ factorProof: request.factorProof
16569
+ });
16570
+ if (!authorized.ok) return authorized.response;
16480
16571
  const result = await ctx.service.passkeyCustody.readRecoveryStatus({ walletId });
16481
- if (result.kind === "no_recovery_set") return routeErrorResponse(404, "no_recovery_set", "this wallet has no recovery codes");
16482
- return toFetchRouteResponse({
16483
- status: 200,
16484
- body: {
16485
- ok: true,
16486
- activeCodeCount: result.activeCodeCount,
16487
- totalCodeCount: result.totalCodeCount,
16488
- issuedAtMs: result.issuedAtMs,
16489
- storeVersion: result.storeVersion,
16490
- backupOutstanding: result.backupOutstanding
16491
- }
16572
+ if (result.kind === "no_recovery_set") return await completeWalletCustodyOperation(ctx, authorized.operation, 404, {
16573
+ ok: false,
16574
+ code: "no_recovery_set",
16575
+ message: "this wallet has no recovery codes"
16576
+ }, "failed_before_side_effect");
16577
+ return await completeWalletCustodyOperation(ctx, authorized.operation, 200, {
16578
+ ok: true,
16579
+ activeCodeCount: result.activeCodeCount,
16580
+ totalCodeCount: result.totalCodeCount,
16581
+ issuedAtMs: result.issuedAtMs,
16582
+ storeVersion: result.storeVersion,
16583
+ backupOutstanding: result.backupOutstanding
16492
16584
  });
16493
16585
  }
16494
16586
  function walletIdFromPath(routePath, pathname) {
@@ -22688,7 +22780,10 @@ async function handleWalletEmailOtpFactorRelease(ctx) {
22688
22780
  if (!consumed.ok) return json$3(consumed, { status: emailOtpStatusCode(consumed.code) });
22689
22781
  factorReleaseChallengeId = consumed.challengeId;
22690
22782
  }
22691
- const unsealed = await ctx.service.emailOtp.removeEmailOtpServerSeal({ wrappedCiphertext: enrollment.enrollment.serverSealedFactorCiphertextB64u });
22783
+ const unsealed = await ctx.service.emailOtp.removeEmailOtpServerSeal({
22784
+ wrappedCiphertext: enrollment.enrollment.serverSealedFactorCiphertextB64u,
22785
+ enrollmentSealKeyVersion: enrollment.enrollment.enrollmentSealKeyVersion
22786
+ });
22692
22787
  if (!unsealed.ok) return json$3(unsealed, { status: emailOtpStatusCode(unsealed.code) });
22693
22788
  if (unsealed.enrollmentSealKeyVersion !== enrollment.enrollment.enrollmentSealKeyVersion) return json$3({
22694
22789
  ok: false,
@@ -24367,7 +24462,10 @@ function buildRouterAbEd25519AuthorizedOperationWire(input) {
24367
24462
  });
24368
24463
  }
24369
24464
  async function releaseEmailOtpFactorForWorker(input) {
24370
- const unsealed = await input.emailOtp.removeEmailOtpServerSeal({ wrappedCiphertext: input.request.enrollment.serverSealedFactorCiphertextB64u });
24465
+ const unsealed = await input.emailOtp.removeEmailOtpServerSeal({
24466
+ wrappedCiphertext: input.request.enrollment.serverSealedFactorCiphertextB64u,
24467
+ enrollmentSealKeyVersion: input.request.enrollment.enrollmentSealKeyVersion
24468
+ });
24371
24469
  if (!unsealed.ok) return unsealed;
24372
24470
  if (unsealed.enrollmentSealKeyVersion !== input.request.enrollment.enrollmentSealKeyVersion) return {
24373
24471
  ok: false,
@@ -25951,6 +26049,9 @@ function assertNeverAuthProviderAction(route) {
25951
26049
  function assertNeverAuthIdentityMutation(route) {
25952
26050
  throw new Error(`Unsupported auth identity mutation: ${String(route)}`);
25953
26051
  }
26052
+ function hasFullOwnerAuthority(context) {
26053
+ return sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: context.authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }));
26054
+ }
25954
26055
  async function requireExactWalletSession(ctx) {
25955
26056
  const token = extractBearerCredential(ctx.request.headers);
25956
26057
  if (!token) return {
@@ -26016,6 +26117,7 @@ async function handleAuth(ctx) {
26016
26117
  const command = parsed.request;
26017
26118
  const sess = await requireExactWalletSession(ctx);
26018
26119
  if (!sess.ok) return sess.response;
26120
+ if (!hasFullOwnerAuthority(sess.context)) return jsonFailure(403, "forbidden", "Full owner authority is required to manage identities");
26019
26121
  const walletId = String(sess.context.authorization.session.walletId);
26020
26122
  const stepUpRequest = command.request.stepUp;
26021
26123
  const stepUp = await requireExactPasskeyStepUp({
@@ -315,7 +315,7 @@ function canonicalPendingActivationJson(value) {
315
315
  if (!exactObject(value, [
316
316
  "registration",
317
317
  "ownership_generation",
318
- "tenant_root_custody_binding_digest",
318
+ "tenant_root_custody_binding",
319
319
  "wallet_scope",
320
320
  "environment_key",
321
321
  "activation_context",
@@ -1,26 +1,8 @@
1
- import { isPlainObject } from "../../../shared-ts/src/utils/validation.js";
2
1
  import { base64UrlEncode } from "../../../shared-ts/src/utils/base64.js";
2
+ import { unredactedWebAuthnExtensionField } from "../../auth/webAuthnCredentialCodecs.js";
3
3
  import { decodeBase64UrlOrBase64 } from "../../../core/authService/webauthnOidcHelpers.js";
4
4
  import { verifyWebAuthnAuthenticationLiteWithStore } from "../../../core/authService/webauthn.js";
5
5
  //#region src/router/domains/passkeyCustody/passkeyCustodyEnvelopeRetrieval.ts
6
- /**
7
- * Detects any surviving WebAuthn extension output on an assertion.
8
- *
9
- * This checks for extension results generally rather than a `prf` key alone: a
10
- * caller that forwards raw extension outputs is not honouring the redaction
11
- * contract, and the next extension to carry secret material would otherwise
12
- * pass unnoticed.
13
- */
14
- function disclosedExtensionOutputs(credential) {
15
- const top = credential.clientExtensionResults;
16
- if (isPlainObject(top) && Object.keys(top).length > 0) return "webauthn_authentication.clientExtensionResults must be redacted before retrieval";
17
- const response = credential.response;
18
- if (isPlainObject(response)) {
19
- const nested = response.clientExtensionResults;
20
- if (isPlainObject(nested) && Object.keys(nested).length > 0) return "webauthn_authentication.response.clientExtensionResults must be redacted before retrieval";
21
- }
22
- return null;
23
- }
24
6
  function assertionCredentialIdB64u(credential) {
25
7
  const raw = credential.rawId;
26
8
  const fallback = credential.id;
@@ -35,15 +17,15 @@ function assertionCredentialIdB64u(credential) {
35
17
  async function retrievePasskeyCustodyEnvelope(input) {
36
18
  const { request } = input;
37
19
  const verifyAssertion = input.verifyAssertion || verifyWebAuthnAuthenticationLiteWithStore;
38
- const disclosed = disclosedExtensionOutputs(request.webauthnAuthentication);
39
- if (disclosed !== null) {
20
+ const extensionField = unredactedWebAuthnExtensionField(request.webauthnAuthentication);
21
+ if (extensionField) {
40
22
  input.logger.error("[passkey-custody] assertion carried unredacted extension output", {
41
23
  walletId: String(request.locator.walletId),
42
24
  rpId: String(request.rpId)
43
25
  });
44
26
  return {
45
27
  kind: "prf_disclosed",
46
- message: disclosed
28
+ message: `webauthn_authentication.${extensionField} must be redacted before retrieval`
47
29
  };
48
30
  }
49
31
  if (request.locator.factor.kind !== "passkey") return {
@@ -24,7 +24,10 @@ async function reserveRegistrationCustody(input) {
24
24
  if (admitted.kind !== "reserved") return settled(toOutcome(admitted));
25
25
  return {
26
26
  kind: "prepared",
27
- outcome: { status: "committed" },
27
+ outcome: {
28
+ status: "committed",
29
+ issuedAtMs: admitted.issuedAtMs
30
+ },
28
31
  write: { statements: admitted.statements }
29
32
  };
30
33
  } catch (error) {
@@ -50,7 +53,10 @@ function toOutcome(admitted) {
50
53
  keyManifestDigestB64u: admitted.keyManifestDigestB64u
51
54
  };
52
55
  case "custody_already_established": return { status: "custody_already_established" };
53
- case "already_exists": return { status: "committed" };
56
+ case "already_exists": return {
57
+ status: "committed",
58
+ issuedAtMs: admitted.issuedAtMs
59
+ };
54
60
  case "rejected": return {
55
61
  status: "rejected",
56
62
  reason: admitted.reason
@@ -282,7 +282,7 @@ function createRouterApiRouteDefinitions(options = {}) {
282
282
  }, ROUTER_API_WALLET_REGISTRATION_SESSION_SERVICES, {
283
283
  kind: "event",
284
284
  action: "wallet_created"
285
- }), publicRoute("wallet_recovery_status", "GET", "/wallets/:walletId/recovery/status", "Report how many recovery codes remain and whether the owner saved them", {
285
+ }), publicRoute("wallet_recovery_status", "POST", "/wallets/:walletId/recovery/status", "Report how many recovery codes remain and whether the owner saved them", {
286
286
  plane: "public",
287
287
  proof: "webauthn",
288
288
  rationale: "Fresh operation-bound owner proof is required."
@@ -296,7 +296,8 @@ function createRouterApiRouteDefinitions(options = {}) {
296
296
  rationale: "Fresh operation-bound owner proof is required."
297
297
  }, ROUTER_API_PASSKEY_CUSTODY_SERVICES), publicRoute("wallet_recovery_backup_acknowledge", "POST", "/wallets/recovery/acknowledge-backup", "Record that the owner saved their recovery codes", {
298
298
  plane: "public",
299
- rationale: "Acknowledgement only clears backup-reminder state after the client displays its locally retained codes."
299
+ proof: "webauthn",
300
+ rationale: "Fresh operation-bound owner proof is required after the client displays its locally retained codes."
300
301
  }, ["passkeyCustody"]), publicRoute("wallet_recovery_finalize", "POST", "/wallets/recovery/finalize", "Finalize an admitted wallet recovery and install its replacement credential", {
301
302
  plane: "public",
302
303
  proof: "recovery_proof",