@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
@@ -1,3 +1,4 @@
1
+ import { buildDelegatedWalletAuthorityV1, buildFullOwnerPermissionsV1, sameDelegatedWalletAuthorityV1 } from "../../../../shared-ts/src/authorization/delegatedAuthority.js";
1
2
  import { extractBearerCredential } from "../../../auth/routerApiKeyAuth.js";
2
3
  import { resolveThresholdRuntimePolicyScope } from "../../../auth/commonRouterUtils.js";
3
4
  import { json, jsonFailure, readJson } from "../../../framework/http.js";
@@ -9,6 +10,9 @@ function assertNeverAuthProviderAction(route) {
9
10
  function assertNeverAuthIdentityMutation(route) {
10
11
  throw new Error(`Unsupported auth identity mutation: ${String(route)}`);
11
12
  }
13
+ function hasFullOwnerAuthority(context) {
14
+ return sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: context.authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }));
15
+ }
12
16
  async function requireExactWalletSession(ctx) {
13
17
  const token = extractBearerCredential(ctx.request.headers);
14
18
  if (!token) return {
@@ -74,6 +78,7 @@ async function handleAuth(ctx) {
74
78
  const command = parsed.request;
75
79
  const sess = await requireExactWalletSession(ctx);
76
80
  if (!sess.ok) return sess.response;
81
+ if (!hasFullOwnerAuthority(sess.context)) return jsonFailure(403, "forbidden", "Full owner authority is required to manage identities");
77
82
  const walletId = String(sess.context.authorization.session.walletId);
78
83
  const stepUpRequest = command.request.stepUp;
79
84
  const stepUp = await requireExactPasskeyStepUp({
@@ -2,6 +2,7 @@ import { parseEmailOtpChallengeId, parseOrgId, parseProviderSubject, parseWallet
2
2
  import { buildVaultOperationRef, parseAuthFactorId, parseAuthorizationAuditEventId, parseAuthorizationEvidenceId, parseAuthorizationEvidenceSetId, parseAuthorizedOperationId, parseCapabilityId, parseCapabilityOperationId, parseDeviceId, parsePrincipalId, parseTenantId } from "../../../../shared-ts/src/authorization/capabilityKinds.js";
3
3
  import { normalizeOptionalTrimmedString } from "../../../../shared-ts/src/utils/normalize.js";
4
4
  import { isPlainObject, requireTrimmedString } from "../../../../shared-ts/src/utils/validation.js";
5
+ import { buildDelegatedWalletAuthorityV1, buildFullOwnerPermissionsV1, sameDelegatedWalletAuthorityV1 } from "../../../../shared-ts/src/authorization/delegatedAuthority.js";
5
6
  import { base64UrlDecode } from "../../../../shared-ts/src/utils/base64.js";
6
7
  import { alphabetizeStringify } from "../../../../shared-ts/src/utils/digests.js";
7
8
  import { sha256Utf8DigestB64u } from "../../../../shared-ts/src/utils/canonicalPrimitives.js";
@@ -15,11 +16,12 @@ import { toFetchRouteResponse } from "../../../framework/routeResponses.js";
15
16
  import { parsePasskeyCustodyEnvelopeRecord } from "../../../../shared-ts/src/passkey-custody/custodyEnvelope.js";
16
17
  import "../../../../shared-ts/src/passkey-custody/index.js";
17
18
  import { readJson } from "../../../framework/http.js";
19
+ import { parseWebAuthnAuthenticationCredential, unredactedWebAuthnExtensionField } from "../../../auth/webAuthnCredentialCodecs.js";
20
+ import { parseCustodyOwnerProof, parseRecoveryBackupAcknowledgementRequest, parseRecoveryStatusRequest, requireExactObjectFields } from "./walletCustodyRequestBoundary.js";
18
21
  import { isHostWithinRpId, originHostnameOrEmpty } from "../../../../core/authService/webauthnOidcHelpers.js";
19
22
  import { parseRecoveryCodeReservationId } from "../../../../shared-ts/src/wallet-recovery/recoveryCodeReservation.js";
20
23
  import { computeWalletCustodyAdminChallengeDigest } from "../../../../shared-ts/src/authorization/walletCustodyOperation.js";
21
24
  import { parseWalletRecoveryTargetV1 } from "../../../../shared-ts/src/wallet-recovery/walletRecoveryTarget.js";
22
- import { parseWebAuthnAuthenticationCredential } from "../../../auth/webAuthnCredentialCodecs.js";
23
25
  import { emailOtpStatusCode, hashEmailOtpOperationBinding } from "../../../domains/emailOtp/emailOtpSessionRouteHelpers.js";
24
26
  import { EMAIL_OTP_CHANNEL, WALLET_EMAIL_OTP_UNLOCK_OPERATION } from "../../../../shared-ts/src/utils/emailOtpDomain.js";
25
27
  import { parseDerivedWalletRecoveryKeyId } from "../../../../shared-ts/src/wallet-recovery/recoveryKeyId.js";
@@ -53,51 +55,8 @@ const EMAIL_OTP_CHALLENGE_ROUTE_ID = "wallet_custody_email_otp_challenge";
53
55
  const ENVELOPE_OWNERSHIP_UPGRADE_ROUTE_ID = "wallet_custody_envelope_ownership_upgrade";
54
56
  const CUSTODY_OPERATION_CAPABILITY_ID = "wallet-custody-admin";
55
57
  const CUSTODY_PROOF_TTL_MS = 6e4;
56
- function requireExactObjectFields(value, fields, label) {
57
- const keys = Object.keys(value).sort();
58
- const expected = [...fields].sort();
59
- if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new Error(`${label} contains unsupported fields`);
60
- }
61
- function parseCustodyOwnerProof(value) {
62
- if (!isPlainObject(value)) throw new Error("factorProof is required");
63
- const kind = normalizeOptionalTrimmedString(value.kind);
64
- if (kind === "passkey") {
65
- requireExactObjectFields(value, [
66
- "kind",
67
- "walletId",
68
- "rpId",
69
- "credentialIdB64u",
70
- "challenge_digest",
71
- "webauthn_authentication"
72
- ], "factorProof");
73
- const webauthnAuthentication = parseWebAuthnAuthenticationCredential(value.webauthn_authentication);
74
- if (!webauthnAuthentication) throw new Error("factorProof.webauthn_authentication is invalid");
75
- return {
76
- kind,
77
- walletId: requireTrimmedString(value.walletId, "factorProof.walletId"),
78
- rpId: requireTrimmedString(value.rpId, "factorProof.rpId"),
79
- credentialIdB64u: requireTrimmedString(value.credentialIdB64u, "factorProof.credentialIdB64u"),
80
- challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest"),
81
- webauthn_authentication: webauthnAuthentication
82
- };
83
- }
84
- if (kind === "email_otp") {
85
- requireExactObjectFields(value, [
86
- "kind",
87
- "provider_subject_id",
88
- "challenge_id",
89
- "otp_code",
90
- "challenge_digest"
91
- ], "factorProof");
92
- return {
93
- kind,
94
- provider_subject_id: requireTrimmedString(value.provider_subject_id, "factorProof.provider_subject_id"),
95
- challenge_id: requireTrimmedString(value.challenge_id, "factorProof.challenge_id"),
96
- otp_code: requireTrimmedString(value.otp_code, "factorProof.otp_code"),
97
- challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest")
98
- };
99
- }
100
- throw new Error("factorProof.kind must be passkey or email_otp");
58
+ function hasFullOwnerAuthority(authority) {
59
+ return sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }));
101
60
  }
102
61
  function parseRequiredAuthorizationValue(parsed) {
103
62
  if (!parsed.ok) throw new Error(parsed.error.message);
@@ -190,8 +149,13 @@ async function authorizeWalletCustodyOperation(input) {
190
149
  } catch (error) {
191
150
  return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "passkey authority is invalid");
192
151
  }
193
- const active = await input.ctx.service.walletAuthMethods.verifyActivePasskeyAuthority(authority);
152
+ const active = await input.ctx.service.walletAuthMethods.resolveActivePasskeyAuthorityForVerifiedCredential({
153
+ walletId,
154
+ rpId: authority.verifier.rpId,
155
+ credentialIdB64u: authority.factor.credentialIdB64u
156
+ });
194
157
  if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
158
+ if (!hasFullOwnerAuthority(active.walletAuthority)) return toCustodyAuthorizationFailure(403, "forbidden", "Full owner authority is required");
195
159
  const credential = parseWebAuthnAuthenticationCredential(proof.webauthn_authentication);
196
160
  if (!credential) return toCustodyAuthorizationFailure(400, "invalid_owner_proof", "passkey assertion is invalid");
197
161
  if (String(credential.rawId || credential.id).trim() !== authority.factor.credentialIdB64u) return toCustodyAuthorizationFailure(401, "unauthorized", "passkey credential changed");
@@ -207,7 +171,7 @@ async function authorizeWalletCustodyOperation(input) {
207
171
  tenantId: operation.tenantId,
208
172
  principalId: operation.principalId,
209
173
  walletId,
210
- authorityRef: await walletAuthAuthorityRef({ authority }),
174
+ authorityRef: await walletAuthAuthorityRef({ authority: active.authority }),
211
175
  requestOrigin: origin,
212
176
  audience: origin,
213
177
  factorId: parseRequiredAuthorizationValue(parseAuthFactorId(`passkey:${authority.factor.credentialIdB64u}`)),
@@ -356,7 +320,10 @@ async function completeWalletCustodyOperation(ctx, operation, status, body, resu
356
320
  }
357
321
  async function handlePasskeyCustody(ctx) {
358
322
  if (!matchRoute(ctx, ROUTE_ID)) return null;
359
- const request = parseWireRequest(await readJsonObject(ctx.request), ctx.request.headers.get("origin"));
323
+ const body = await readJsonObject(ctx.request);
324
+ const extensionField = unredactedWebAuthnExtensionField(body?.webauthnAuthentication);
325
+ if (extensionField) return routeErrorResponse(400, "prf_disclosed", `webauthnAuthentication.${extensionField} must be redacted before retrieval`);
326
+ const request = parseWireRequest(body, ctx.request.headers.get("origin"));
360
327
  if (!request) return routeErrorResponse(400, "invalid_request", "custody retrieval needs a locator, a challenge id, and an assertion");
361
328
  return toFetchRouteResponse(await ctx.service.passkeyCustody.retrieveEnvelope(request));
362
329
  }
@@ -381,7 +348,7 @@ function parseWalletCustodyEmailOtpChallengeRequest(value, actualOrigin) {
381
348
  const walletId = parseRequiredAuthorizationValue(parseWalletId(value.walletId));
382
349
  const providerSubjectId = parseRequiredAuthorizationValue(parseProviderSubject(value.providerSubjectId));
383
350
  const operation = value.operation;
384
- if (operation !== "credentials_list" && operation !== "credential_label" && operation !== "recovery_rotate" && operation !== "recovery_read") throw new Error("Email OTP custody challenge operation is invalid");
351
+ 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");
385
352
  if (!isPlainObject(value.payload)) throw new Error("Email OTP custody challenge payload is invalid");
386
353
  if (value.requestOrigin !== void 0 && value.requestOrigin !== actualOrigin) throw new Error("Email OTP custody challenge requestOrigin does not match Origin");
387
354
  return {
@@ -1100,6 +1067,8 @@ function parseWalletRecoveryFinalizeBody(value) {
1100
1067
  "ecdsaMaterialPossessionProofs"
1101
1068
  ], "wallet recovery finalization");
1102
1069
  if (!isPlainObject(value.webauthnRegistration)) throw new Error("wallet recovery finalization registration is invalid");
1070
+ const extensionField = unredactedWebAuthnExtensionField(value.webauthnRegistration);
1071
+ if (extensionField) throw new Error(`wallet recovery finalization registration ${extensionField} must be redacted`);
1103
1072
  return {
1104
1073
  kind: "finalize",
1105
1074
  ...identity,
@@ -1133,23 +1102,41 @@ function authorizedOperationReplay(operation) {
1133
1102
  }
1134
1103
  async function handleWalletRecoveryBackupAcknowledge(ctx) {
1135
1104
  if (!matchRoute(ctx, RECOVERY_ACK_ROUTE_ID)) return null;
1136
- const body = await readJsonObject(ctx.request);
1137
- let walletId = "";
1105
+ let request;
1138
1106
  try {
1139
- if (!body) throw new Error("acknowledgement body is required");
1140
- requireExactObjectFields(body, ["walletId"], "recovery backup acknowledgement");
1141
- walletId = requireTrimmedString(body.walletId, "walletId");
1107
+ request = parseRecoveryBackupAcknowledgementRequest(await readJsonObject(ctx.request));
1142
1108
  } catch {
1143
- return routeErrorResponse(400, "invalid_request", "an acknowledgement needs a wallet");
1109
+ return routeErrorResponse(400, "invalid_request", "acknowledgement body is invalid");
1144
1110
  }
1145
- const result = await ctx.service.passkeyCustody.acknowledgeRecoveryBackup({ walletId });
1146
- if (result.kind === "no_recovery_set") return routeErrorResponse(404, "no_recovery_set", "this wallet has no issued recovery codes to acknowledge");
1147
- return toFetchRouteResponse({
1148
- status: 200,
1149
- body: {
1150
- ok: true,
1151
- issuedAtMs: result.issuedAtMs
1152
- }
1111
+ const authorized = await authorizeWalletCustodyOperation({
1112
+ ctx,
1113
+ walletId: request.walletId,
1114
+ operation: "recovery_backup_acknowledge",
1115
+ payload: {
1116
+ walletId: request.walletId,
1117
+ requestId: request.requestId,
1118
+ expectedIssuedAtMs: request.expectedIssuedAtMs
1119
+ },
1120
+ factorProof: request.factorProof
1121
+ });
1122
+ if (!authorized.ok) return authorized.response;
1123
+ const result = await ctx.service.passkeyCustody.acknowledgeRecoveryBackup({
1124
+ walletId: request.walletId,
1125
+ expectedIssuedAtMs: request.expectedIssuedAtMs
1126
+ });
1127
+ if (result.kind === "conflict") return await completeWalletCustodyOperation(ctx, authorized.operation, 409, {
1128
+ ok: false,
1129
+ code: "recovery_set_conflict",
1130
+ message: "the recovery set changed"
1131
+ }, "failed_before_side_effect");
1132
+ if (result.kind === "no_recovery_set") return await completeWalletCustodyOperation(ctx, authorized.operation, 404, {
1133
+ ok: false,
1134
+ code: "no_recovery_set",
1135
+ message: "this wallet has no issued recovery codes to acknowledge"
1136
+ }, "failed_before_side_effect");
1137
+ return await completeWalletCustodyOperation(ctx, authorized.operation, 200, {
1138
+ ok: true,
1139
+ issuedAtMs: result.issuedAtMs
1153
1140
  });
1154
1141
  }
1155
1142
  /**
@@ -1253,39 +1240,42 @@ async function handleWalletRecoveryRead(ctx) {
1253
1240
  storeVersion: result.storeVersion
1254
1241
  });
1255
1242
  }
1256
- /**
1257
- * Reporting recovery status to the wallet's owner.
1258
- *
1259
- * The wallet comes from the path. Origin binding keeps this count endpoint
1260
- * scoped to browser callers without exposing recovery identifiers.
1261
- *
1262
- * Counts only, never identifiers. Which codes remain is not something even
1263
- * the owner's browser needs, and a list would be one leak away from being
1264
- * useful to someone else.
1265
- */
1243
+ /** Reports current recovery-code counts after fresh owner proof. */
1266
1244
  async function handleWalletRecoveryStatus(ctx) {
1267
1245
  const route = matchRoute(ctx, RECOVERY_STATUS_ROUTE_ID);
1268
1246
  if (!route) return null;
1269
1247
  const walletId = walletIdFromPath(route.path, ctx.pathname);
1270
1248
  if (!walletId) return routeErrorResponse(400, "invalid_request", "status needs a wallet");
1271
- const origin = normalizeOptionalTrimmedString(ctx.request.headers.get("origin"));
1249
+ let request;
1272
1250
  try {
1273
- parseSessionOrigin(origin);
1251
+ request = parseRecoveryStatusRequest(await readJsonObject(ctx.request));
1274
1252
  } catch {
1275
- return routeErrorResponse(400, "invalid_origin", "request Origin is invalid");
1253
+ return routeErrorResponse(400, "invalid_request", "status body is invalid");
1276
1254
  }
1255
+ const authorized = await authorizeWalletCustodyOperation({
1256
+ ctx,
1257
+ walletId,
1258
+ operation: "recovery_status",
1259
+ payload: {
1260
+ walletId,
1261
+ requestId: request.requestId
1262
+ },
1263
+ factorProof: request.factorProof
1264
+ });
1265
+ if (!authorized.ok) return authorized.response;
1277
1266
  const result = await ctx.service.passkeyCustody.readRecoveryStatus({ walletId });
1278
- if (result.kind === "no_recovery_set") return routeErrorResponse(404, "no_recovery_set", "this wallet has no recovery codes");
1279
- return toFetchRouteResponse({
1280
- status: 200,
1281
- body: {
1282
- ok: true,
1283
- activeCodeCount: result.activeCodeCount,
1284
- totalCodeCount: result.totalCodeCount,
1285
- issuedAtMs: result.issuedAtMs,
1286
- storeVersion: result.storeVersion,
1287
- backupOutstanding: result.backupOutstanding
1288
- }
1267
+ if (result.kind === "no_recovery_set") return await completeWalletCustodyOperation(ctx, authorized.operation, 404, {
1268
+ ok: false,
1269
+ code: "no_recovery_set",
1270
+ message: "this wallet has no recovery codes"
1271
+ }, "failed_before_side_effect");
1272
+ return await completeWalletCustodyOperation(ctx, authorized.operation, 200, {
1273
+ ok: true,
1274
+ activeCodeCount: result.activeCodeCount,
1275
+ totalCodeCount: result.totalCodeCount,
1276
+ issuedAtMs: result.issuedAtMs,
1277
+ storeVersion: result.storeVersion,
1278
+ backupOutstanding: result.backupOutstanding
1289
1279
  });
1290
1280
  }
1291
1281
  function walletIdFromPath(routePath, pathname) {
@@ -871,7 +871,10 @@ async function handleWalletEmailOtpFactorRelease(ctx) {
871
871
  if (!consumed.ok) return json(consumed, { status: emailOtpStatusCode(consumed.code) });
872
872
  factorReleaseChallengeId = consumed.challengeId;
873
873
  }
874
- const unsealed = await ctx.service.emailOtp.removeEmailOtpServerSeal({ wrappedCiphertext: enrollment.enrollment.serverSealedFactorCiphertextB64u });
874
+ const unsealed = await ctx.service.emailOtp.removeEmailOtpServerSeal({
875
+ wrappedCiphertext: enrollment.enrollment.serverSealedFactorCiphertextB64u,
876
+ enrollmentSealKeyVersion: enrollment.enrollment.enrollmentSealKeyVersion
877
+ });
875
878
  if (!unsealed.ok) return json(unsealed, { status: emailOtpStatusCode(unsealed.code) });
876
879
  if (unsealed.enrollmentSealKeyVersion !== enrollment.enrollment.enrollmentSealKeyVersion) return json({
877
880
  ok: false,
@@ -126,7 +126,10 @@ function buildRouterAbEd25519AuthorizedOperationWire(input) {
126
126
  });
127
127
  }
128
128
  async function releaseEmailOtpFactorForWorker(input) {
129
- const unsealed = await input.emailOtp.removeEmailOtpServerSeal({ wrappedCiphertext: input.request.enrollment.serverSealedFactorCiphertextB64u });
129
+ const unsealed = await input.emailOtp.removeEmailOtpServerSeal({
130
+ wrappedCiphertext: input.request.enrollment.serverSealedFactorCiphertextB64u,
131
+ enrollmentSealKeyVersion: input.request.enrollment.enrollmentSealKeyVersion
132
+ });
130
133
  if (!unsealed.ok) return unsealed;
131
134
  if (unsealed.enrollmentSealKeyVersion !== input.request.enrollment.enrollmentSealKeyVersion) return {
132
135
  ok: false,
@@ -0,0 +1,80 @@
1
+ import { normalizeOptionalTrimmedString } from "../../../../shared-ts/src/utils/normalize.js";
2
+ import { isPlainObject, requireTrimmedString } from "../../../../shared-ts/src/utils/validation.js";
3
+ import { parseUnixMs } from "../../../../shared-ts/src/passkey-custody/primitives.js";
4
+ import { parseWebAuthnAuthenticationCredential, unredactedWebAuthnExtensionField } from "../../../auth/webAuthnCredentialCodecs.js";
5
+ //#region src/router/transport/fetch/routes/walletCustodyRequestBoundary.ts
6
+ function requireExactObjectFields(value, fields, label) {
7
+ const keys = Object.keys(value).sort();
8
+ const expected = [...fields].sort();
9
+ if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new Error(`${label} contains unsupported fields`);
10
+ }
11
+ function parseCustodyOwnerProof(value) {
12
+ if (!isPlainObject(value)) throw new Error("factorProof is required");
13
+ const kind = normalizeOptionalTrimmedString(value.kind);
14
+ if (kind === "passkey") {
15
+ requireExactObjectFields(value, [
16
+ "kind",
17
+ "walletId",
18
+ "rpId",
19
+ "credentialIdB64u",
20
+ "challenge_digest",
21
+ "webauthn_authentication"
22
+ ], "factorProof");
23
+ const extensionField = unredactedWebAuthnExtensionField(value.webauthn_authentication);
24
+ if (extensionField) throw new Error(`factorProof.webauthn_authentication.${extensionField} must be redacted`);
25
+ const webauthnAuthentication = parseWebAuthnAuthenticationCredential(value.webauthn_authentication);
26
+ if (!webauthnAuthentication) throw new Error("factorProof.webauthn_authentication is invalid");
27
+ return {
28
+ kind,
29
+ walletId: requireTrimmedString(value.walletId, "factorProof.walletId"),
30
+ rpId: requireTrimmedString(value.rpId, "factorProof.rpId"),
31
+ credentialIdB64u: requireTrimmedString(value.credentialIdB64u, "factorProof.credentialIdB64u"),
32
+ challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest"),
33
+ webauthn_authentication: webauthnAuthentication
34
+ };
35
+ }
36
+ if (kind === "email_otp") {
37
+ requireExactObjectFields(value, [
38
+ "kind",
39
+ "provider_subject_id",
40
+ "challenge_id",
41
+ "otp_code",
42
+ "challenge_digest"
43
+ ], "factorProof");
44
+ return {
45
+ kind,
46
+ provider_subject_id: requireTrimmedString(value.provider_subject_id, "factorProof.provider_subject_id"),
47
+ challenge_id: requireTrimmedString(value.challenge_id, "factorProof.challenge_id"),
48
+ otp_code: requireTrimmedString(value.otp_code, "factorProof.otp_code"),
49
+ challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest")
50
+ };
51
+ }
52
+ throw new Error("factorProof.kind must be passkey or email_otp");
53
+ }
54
+ function parseRecoveryBackupAcknowledgementRequest(body) {
55
+ if (!isPlainObject(body)) throw new Error("acknowledgement body is required");
56
+ requireExactObjectFields(body, [
57
+ "expectedIssuedAtMs",
58
+ "factorProof",
59
+ "requestId",
60
+ "walletId"
61
+ ], "recovery backup acknowledgement");
62
+ return {
63
+ walletId: requireTrimmedString(body.walletId, "walletId"),
64
+ requestId: requireTrimmedString(body.requestId, "requestId"),
65
+ expectedIssuedAtMs: parseUnixMs(body.expectedIssuedAtMs, "expectedIssuedAtMs"),
66
+ factorProof: body.factorProof
67
+ };
68
+ }
69
+ function parseRecoveryStatusRequest(body) {
70
+ if (!isPlainObject(body)) throw new Error("status body is required");
71
+ requireExactObjectFields(body, ["factorProof", "requestId"], "recovery status");
72
+ return {
73
+ requestId: requireTrimmedString(body.requestId, "requestId"),
74
+ factorProof: body.factorProof
75
+ };
76
+ }
77
+ //#endregion
78
+ export { parseCustodyOwnerProof, parseRecoveryBackupAcknowledgementRequest, parseRecoveryStatusRequest, requireExactObjectFields };
79
+
80
+ //# sourceMappingURL=walletCustodyRequestBoundary.js.map
@@ -1,3 +1,4 @@
1
+ import { parseUnixMs } from "./primitives.js";
1
2
  //#region ../shared-ts/src/passkey-custody/ceremonyCommitPayload.ts
2
3
  /**
3
4
  * Reads a commit payload off the wire without judging it.
@@ -42,8 +43,11 @@ function parseWalletCustodyRegistrationOutcome(value, label) {
42
43
  const record = value;
43
44
  switch (record.status) {
44
45
  case "not_requested":
45
- case "committed":
46
46
  case "custody_already_established": return { status: record.status };
47
+ case "committed": return {
48
+ status: "committed",
49
+ issuedAtMs: parseUnixMs(record.issuedAtMs, `${label}.issuedAtMs`)
50
+ };
47
51
  case "joined": {
48
52
  const digest = record.keyManifestDigestB64u;
49
53
  if (typeof digest !== "string" || !digest) throw new Error(`${label} joined custody carries no key manifest digest`);
@@ -31,17 +31,19 @@ function decodeEvmTransactionReceipt(value) {
31
31
  if (typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid eth_getTransactionReceipt result");
32
32
  const record = value;
33
33
  const transactionHash = optionalRpcHexData(record, "transactionHash", "eth_getTransactionReceipt");
34
+ if (typeof transactionHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(transactionHash)) throw new Error("Invalid eth_getTransactionReceipt result: transactionHash");
34
35
  const from = optionalRpcAddress(record, "from", "eth_getTransactionReceipt");
35
36
  const blockNumber = optionalRpcHexQuantity(record, "blockNumber", "eth_getTransactionReceipt");
36
37
  const status = optionalRpcHexQuantity(record, "status", "eth_getTransactionReceipt");
38
+ if (typeof blockNumber !== "string" || typeof status !== "string") throw new Error("Invalid eth_getTransactionReceipt result: missing blockNumber or status");
37
39
  const gasUsed = optionalRpcHexQuantity(record, "gasUsed", "eth_getTransactionReceipt");
38
40
  const effectiveGasPrice = optionalRpcHexQuantity(record, "effectiveGasPrice", "eth_getTransactionReceipt");
39
41
  const gasPrice = optionalRpcHexQuantity(record, "gasPrice", "eth_getTransactionReceipt");
40
42
  return {
41
- ...transactionHash !== void 0 ? { transactionHash } : {},
43
+ transactionHash,
42
44
  ...from !== void 0 ? { from } : {},
43
- ...blockNumber !== void 0 ? { blockNumber } : {},
44
- ...status !== void 0 ? { status } : {},
45
+ blockNumber,
46
+ status,
45
47
  ...gasUsed !== void 0 ? { gasUsed } : {},
46
48
  ...effectiveGasPrice !== void 0 ? { effectiveGasPrice } : {},
47
49
  ...gasPrice !== void 0 ? { gasPrice } : {}
@@ -1405,6 +1405,7 @@ function parseRouterAbEcdsaOperationStepUpWebAuthnCredentialV1(value) {
1405
1405
  "signature",
1406
1406
  "userHandle"
1407
1407
  ]);
1408
+ if (credential.clientExtensionResults !== null) throw new Error("webauthn_authentication.clientExtensionResults must be redacted");
1408
1409
  const authenticatorAttachment = credential.authenticatorAttachment === null ? null : requireAsciiNonEmptyString(credential.authenticatorAttachment, "webauthn_authentication.authenticatorAttachment");
1409
1410
  const userHandle = response.userHandle === null ? null : requireAsciiNonEmptyString(response.userHandle, "webauthn_authentication.response.userHandle");
1410
1411
  return {
@@ -1418,7 +1419,7 @@ function parseRouterAbEcdsaOperationStepUpWebAuthnCredentialV1(value) {
1418
1419
  signature: requireAsciiNonEmptyString(response.signature, "webauthn_authentication.response.signature"),
1419
1420
  userHandle
1420
1421
  },
1421
- clientExtensionResults: credential.clientExtensionResults
1422
+ clientExtensionResults: null
1422
1423
  };
1423
1424
  }
1424
1425
  function parseRouterAbEcdsaOperationStepUpAuthorizationRequestV1(value) {