@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
|
@@ -1245,7 +1245,9 @@ function wireLabeled(parse) {
|
|
|
1245
1245
|
}
|
|
1246
1246
|
};
|
|
1247
1247
|
}
|
|
1248
|
-
|
|
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:
|
|
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", "
|
|
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
|
-
|
|
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/
|
|
14823
|
-
function
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
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
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
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
|
|
15260
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
16340
|
-
let walletId = "";
|
|
16410
|
+
let request;
|
|
16341
16411
|
try {
|
|
16342
|
-
|
|
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", "
|
|
16414
|
+
return routeErrorResponse(400, "invalid_request", "acknowledgement body is invalid");
|
|
16347
16415
|
}
|
|
16348
|
-
const
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
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
|
-
|
|
16554
|
+
let request;
|
|
16475
16555
|
try {
|
|
16476
|
-
|
|
16556
|
+
request = parseRecoveryStatusRequest(await readJsonObject(ctx.request));
|
|
16477
16557
|
} catch {
|
|
16478
|
-
return routeErrorResponse(400, "
|
|
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
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
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({
|
|
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({
|
|
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
|
-
"
|
|
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
|
|
39
|
-
if (
|
|
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:
|
|
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: {
|
|
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 {
|
|
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", "
|
|
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
|
-
|
|
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",
|