@seams/wallet-server 0.20.0 → 0.22.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.
- package/artifact-manifest.json +22 -21
- package/cloudflare-router-ab/build/deriver-a/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/deriver-a/index_bg.wasm +0 -0
- package/cloudflare-router-ab/build/deriver-b/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/deriver-b/index_bg.wasm +0 -0
- package/cloudflare-router-ab/build/local-tools/linux-x64/router_ab_local_init +0 -0
- package/cloudflare-router-ab/build/router/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/router/index_bg.wasm +0 -0
- package/cloudflare-router-ab/build/signing-worker/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/signing-worker/index_bg.wasm +0 -0
- package/cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/tenant-root-control-plane/index_bg.wasm +0 -0
- package/cloudflare-router-ab/scripts/prepare-local-runtime-config.mjs +16 -2
- package/dist/esm/core/deviceLinking/linkedDeviceManagement.js +11 -30
- package/dist/esm/core/emailOtpD1Statements.js +14 -2
- package/dist/esm/core/nearRelayerAccountProvisioning.js +2 -9
- package/dist/esm/hosted-wallet-gateway.js +10 -7
- package/dist/esm/router/auth/webAuthnCredentialCodecs.js +16 -1
- package/dist/esm/router/cloudflare/d1/auth/d1AuthorizationAssembly.js +1 -0
- package/dist/esm/router/cloudflare/d1/auth/d1RouterApiAuthService.js +0 -1
- package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceEmailOtpTargetFactor.js +4 -1
- package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceManagementService.js +6 -11
- package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceRevocation.js +7 -0
- package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpGrantStore.js +5 -2
- package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRateLimitStore.js +18 -0
- package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRecoveryService.js +28 -8
- package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpServerSealRuntime.js +12 -8
- package/dist/esm/router/cloudflare/d1/oidc/d1OidcBoundary.js +1 -0
- package/dist/esm/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.js +19 -5
- package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.js +26 -10
- package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletRecoveryGoogleEmailOtpService.js +4 -1
- package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodService.js +6 -0
- package/dist/esm/router/cloudflare/d1/wallet/d1WalletRelocationApprovals.js +12 -0
- package/dist/esm/router/cloudflare.js +252 -150
- package/dist/esm/router/domains/ecdsa/routerAbEcdsaStrictRegistration.js +1 -1
- package/dist/esm/router/domains/passkeyCustody/passkeyCustodyEnvelopeRetrieval.js +4 -22
- package/dist/esm/router/domains/passkeyCustody/registrationCustodyOutcome.js +8 -2
- package/dist/esm/router/framework/routeDefinitions.js +3 -2
- package/dist/esm/router/transport/fetch/routes/auth.js +5 -0
- package/dist/esm/router/transport/fetch/routes/passkeyCustody.js +78 -88
- package/dist/esm/router/transport/fetch/routes/sessions.js +4 -1
- package/dist/esm/router/transport/fetch/routes/thresholdEd25519.js +4 -1
- package/dist/esm/router/transport/fetch/routes/walletCustodyRequestBoundary.js +80 -0
- package/dist/esm/shared-ts/src/passkey-custody/ceremonyCommitPayload.js +5 -1
- package/dist/esm/shared-ts/src/utils/evmRpcResults.js +5 -3
- package/dist/esm/shared-ts/src/utils/routerAbEcdsaDerivation.js +2 -1
- package/dist/esm/threshold/session/signingSessionSeal/crypto/cipher.js +160 -40
- package/dist/types/shared-ts/src/authorization/walletCustodyOperation.d.ts +1 -1
- package/dist/types/shared-ts/src/device-linking/contracts.d.ts +1 -7
- package/dist/types/shared-ts/src/passkey-custody/ceremonyCommitPayload.d.ts +5 -0
- package/dist/types/shared-ts/src/utils/evmRpcResults.d.ts +3 -3
- package/dist/types/shared-ts/src/utils/routerAbEcdsaDerivation.d.ts +1 -1
- package/dist/types/wallet-server/src/core/authService/emailOtpSeal.d.ts +5 -1
- package/dist/types/wallet-server/src/core/deviceLinking/linkedDeviceManagement.d.ts +1 -9
- package/dist/types/wallet-server/src/core/emailOtpD1Statements.d.ts +2 -1
- package/dist/types/wallet-server/src/hosted-wallet-gateway.d.ts +1 -0
- package/dist/types/wallet-server/src/router/auth/webAuthnCredentialCodecs.d.ts +1 -0
- package/dist/types/wallet-server/src/router/cloudflare/d1/deviceLinking/d1LinkedDeviceManagementService.d.ts +0 -3
- package/dist/types/wallet-server/src/router/cloudflare/d1/deviceLinking/d1LinkedDeviceRevocation.d.ts +1 -1
- package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpGrantStore.d.ts +3 -1
- package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpRateLimitStore.d.ts +15 -2
- package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpServerSealRuntime.d.ts +3 -3
- package/dist/types/wallet-server/src/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.d.ts +3 -0
- package/dist/types/wallet-server/src/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.d.ts +5 -6
- package/dist/types/wallet-server/src/router/domains/passkeyCustody/walletCustodyRegistrationCommit.d.ts +4 -6
- package/dist/types/wallet-server/src/router/framework/authServicePort.d.ts +7 -1
- package/dist/types/wallet-server/src/router/transport/fetch/routes/passkeyCustody.d.ts +1 -10
- package/dist/types/wallet-server/src/router/transport/fetch/routes/walletCustodyRequestBoundary.d.ts +28 -0
- package/dist/types/wallet-server/src/threshold/session/signingSessionSeal/crypto/cipher.d.ts +3 -0
- package/package.json +1 -1
- 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
|
|
57
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1137
|
-
let walletId = "";
|
|
1105
|
+
let request;
|
|
1138
1106
|
try {
|
|
1139
|
-
|
|
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", "
|
|
1109
|
+
return routeErrorResponse(400, "invalid_request", "acknowledgement body is invalid");
|
|
1144
1110
|
}
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1249
|
+
let request;
|
|
1272
1250
|
try {
|
|
1273
|
-
|
|
1251
|
+
request = parseRecoveryStatusRequest(await readJsonObject(ctx.request));
|
|
1274
1252
|
} catch {
|
|
1275
|
-
return routeErrorResponse(400, "
|
|
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
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
43
|
+
transactionHash,
|
|
42
44
|
...from !== void 0 ? { from } : {},
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
1422
|
+
clientExtensionResults: null
|
|
1422
1423
|
};
|
|
1423
1424
|
}
|
|
1424
1425
|
function parseRouterAbEcdsaOperationStepUpAuthorizationRequestV1(value) {
|