@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.
- 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 -7
- 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,6 +1,6 @@
|
|
|
1
1
|
import { normalizeOptionalTrimmedString } from "../../../../shared-ts/src/utils/normalize.js";
|
|
2
2
|
import { failure } from "../../../../shared-ts/src/utils/failure.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createEmailOtpEnrollmentShamir3PassCipherAdapter } from "../../../../threshold/session/signingSessionSeal/crypto/cipher.js";
|
|
4
4
|
//#region src/router/cloudflare/d1/emailOtp/d1EmailOtpServerSealRuntime.ts
|
|
5
5
|
const applyEmailOtpServerSealOperation = {
|
|
6
6
|
cipherOperation: "apply-server-seal",
|
|
@@ -19,14 +19,18 @@ var CloudflareD1EmailOtpServerSealRuntime = class {
|
|
|
19
19
|
this.cipherResult = this.createCipher();
|
|
20
20
|
}
|
|
21
21
|
async removeEmailOtpServerSeal(input) {
|
|
22
|
-
|
|
22
|
+
const keyVersion = normalizeOptionalTrimmedString(input.enrollmentSealKeyVersion);
|
|
23
|
+
if (!keyVersion) return failure("invalid_body", "Missing enrollmentSealKeyVersion");
|
|
24
|
+
return await this.runServerSealOperation(input.wrappedCiphertext, removeEmailOtpServerSealOperation, keyVersion);
|
|
23
25
|
}
|
|
24
26
|
async applyEmailOtpServerSeal(input) {
|
|
25
|
-
|
|
27
|
+
const shamir = this.cipherResult;
|
|
28
|
+
if (!shamir.ok) return shamir;
|
|
29
|
+
return await this.runServerSealOperation(input.wrappedCiphertext, applyEmailOtpServerSealOperation, shamir.keyVersion);
|
|
26
30
|
}
|
|
27
|
-
async runServerSealOperation(
|
|
31
|
+
async runServerSealOperation(wrappedCiphertextRaw, operation, keyVersion) {
|
|
28
32
|
try {
|
|
29
|
-
const wrappedCiphertext = normalizeOptionalTrimmedString(
|
|
33
|
+
const wrappedCiphertext = normalizeOptionalTrimmedString(wrappedCiphertextRaw);
|
|
30
34
|
if (!wrappedCiphertext) return failure("invalid_body", "Missing wrappedCiphertext");
|
|
31
35
|
const shamir = this.cipherResult;
|
|
32
36
|
if (!shamir.ok) return shamir;
|
|
@@ -34,14 +38,14 @@ var CloudflareD1EmailOtpServerSealRuntime = class {
|
|
|
34
38
|
operation: operation.cipherOperation,
|
|
35
39
|
thresholdSessionId: operation.thresholdSessionId,
|
|
36
40
|
ciphertext: wrappedCiphertext,
|
|
37
|
-
keyVersion
|
|
41
|
+
keyVersion,
|
|
38
42
|
auth: { userId: "email_otp" }
|
|
39
43
|
});
|
|
40
44
|
if (!result.ok) return result;
|
|
41
45
|
return {
|
|
42
46
|
ok: true,
|
|
43
47
|
ciphertext: result.ciphertext,
|
|
44
|
-
enrollmentSealKeyVersion: result.keyVersion ||
|
|
48
|
+
enrollmentSealKeyVersion: result.keyVersion || keyVersion
|
|
45
49
|
};
|
|
46
50
|
} catch (error) {
|
|
47
51
|
return failure("internal", serverSealErrorMessage(error) || operation.failureMessage);
|
|
@@ -53,7 +57,7 @@ var CloudflareD1EmailOtpServerSealRuntime = class {
|
|
|
53
57
|
return {
|
|
54
58
|
ok: true,
|
|
55
59
|
keyVersion: this.config.rootConfig.currentKeyVersion,
|
|
56
|
-
cipher:
|
|
60
|
+
cipher: createEmailOtpEnrollmentShamir3PassCipherAdapter({ config: this.config.rootConfig })
|
|
57
61
|
};
|
|
58
62
|
} catch (error) {
|
|
59
63
|
return failure("not_configured", serverSealErrorMessage(error) || "Email OTP Shamir configuration is invalid");
|
|
@@ -141,6 +141,7 @@ function validateGoogleIdTokenClaims(input) {
|
|
|
141
141
|
const givenName = normalizeOptionalTrimmedString(payload.given_name);
|
|
142
142
|
const familyName = normalizeOptionalTrimmedString(payload.family_name);
|
|
143
143
|
const emailVerified = parseBooleanJwtClaim(payload.email_verified);
|
|
144
|
+
if (email && emailVerified !== true) return failedVerification("unverified_email", "Google id_token email is not verified");
|
|
144
145
|
const hostedDomain = normalizeOptionalTrimmedString(payload.hd);
|
|
145
146
|
return {
|
|
146
147
|
ok: true,
|
|
@@ -292,8 +292,21 @@ function createD1PasskeyCustodyRouteService(assembly) {
|
|
|
292
292
|
authenticatorStore,
|
|
293
293
|
logger: assembly.logger
|
|
294
294
|
});
|
|
295
|
-
if (response.status
|
|
296
|
-
|
|
295
|
+
if (response.status !== 200) return response;
|
|
296
|
+
const method = await assembly.walletCustodyCommits.readPasskeyWalletAuthMethod({
|
|
297
|
+
rpId: String(locator.factor.rpId),
|
|
298
|
+
credentialIdB64u: String(locator.factor.credentialIdB64u)
|
|
299
|
+
});
|
|
300
|
+
if (!method || method.walletId !== locator.walletId) return {
|
|
301
|
+
status: 404,
|
|
302
|
+
body: failure("envelope_missing", "no custody envelope for this wallet and credential")
|
|
303
|
+
};
|
|
304
|
+
if (method.status !== "active") return {
|
|
305
|
+
status: 403,
|
|
306
|
+
body: failure("envelope_revoked", "this credential no longer opens the wallet")
|
|
307
|
+
};
|
|
308
|
+
await assembly.passkeyCustodyEnvelopes.recordWalletCredentialUse({
|
|
309
|
+
walletId: locator.walletId,
|
|
297
310
|
envelopeId: locator.envelopeId,
|
|
298
311
|
usedAtMs: (assembly.nowMs ?? Date.now)()
|
|
299
312
|
}).catch(() => void 0);
|
|
@@ -310,11 +323,12 @@ function createD1PasskeyCustodyRouteService(assembly) {
|
|
|
310
323
|
const stored = await assembly.walletCustodyCommits.readRecoveryEnvelopeSet(requireWalletId(request.walletId));
|
|
311
324
|
if (!stored) return { kind: "no_recovery_set" };
|
|
312
325
|
const issuedAtMs = Number(stored.record.issuedAtMs);
|
|
313
|
-
|
|
326
|
+
if (issuedAtMs !== request.expectedIssuedAtMs) return { kind: "conflict" };
|
|
327
|
+
if ((await assembly.walletCustodyCommits.writeBackupAcknowledgement(buildWalletRecoveryBackupAcknowledgementV1({
|
|
314
328
|
walletId: request.walletId,
|
|
315
|
-
issuedAtMs,
|
|
329
|
+
issuedAtMs: request.expectedIssuedAtMs,
|
|
316
330
|
acknowledgedAtMs: (assembly.nowMs ?? Date.now)()
|
|
317
|
-
}));
|
|
331
|
+
}))).kind === "conflict") return { kind: "conflict" };
|
|
318
332
|
return {
|
|
319
333
|
kind: "acknowledged",
|
|
320
334
|
issuedAtMs
|
|
@@ -375,10 +375,26 @@ var CloudflareD1WalletCustodyCommitStore = class {
|
|
|
375
375
|
kind: "inconsistent",
|
|
376
376
|
reason: "recovery code locator already exists"
|
|
377
377
|
};
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
let envelopeExists = false;
|
|
379
|
+
let existingRecordExists = false;
|
|
380
|
+
let existingRecoverySet = null;
|
|
381
|
+
for (const entry of existing) {
|
|
382
|
+
if (entry.result.kind === "missing") continue;
|
|
383
|
+
existingRecordExists = true;
|
|
384
|
+
if (entry.key === envelopeKey) envelopeExists = true;
|
|
385
|
+
if (entry.key === recoverySetKey && entry.result.value.kind === "wallet_recovery_envelope_set_v1") existingRecoverySet = entry.result.value;
|
|
386
|
+
}
|
|
387
|
+
if (envelopeExists) {
|
|
388
|
+
if (!existingRecoverySet || !sameRecoveryCodeKeys(existingRecoverySet, commit.recoverySet)) return { kind: "custody_already_established" };
|
|
389
|
+
return {
|
|
390
|
+
kind: "already_exists",
|
|
391
|
+
issuedAtMs: existingRecoverySet.issuedAtMs
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (existingRecordExists) return { kind: "custody_already_established" };
|
|
380
395
|
return {
|
|
381
396
|
kind: "reserved",
|
|
397
|
+
issuedAtMs: commit.recoverySet.issuedAtMs,
|
|
382
398
|
statements: [...this.records.prepareMutationStatements([
|
|
383
399
|
{
|
|
384
400
|
key: envelopeKey,
|
|
@@ -443,12 +459,9 @@ var CloudflareD1WalletCustodyCommitStore = class {
|
|
|
443
459
|
return parsed.ok ? parsed.record : null;
|
|
444
460
|
}
|
|
445
461
|
/**
|
|
446
|
-
* Records that the owner confirmed saving
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* cosmetic, a repeat acknowledgement is not a conflict worth surfacing to
|
|
450
|
-
* someone pressing a button, and it shares no record with the wraps a spend
|
|
451
|
-
* updates — which is exactly why it is a separate key.
|
|
462
|
+
* Records the issuance that the owner confirmed saving. The caller selects
|
|
463
|
+
* that issuance before this write, so a concurrent rotation cannot make this
|
|
464
|
+
* acknowledgement apply to newer codes the owner did not see.
|
|
452
465
|
*/
|
|
453
466
|
async writeBackupAcknowledgement(record) {
|
|
454
467
|
const key = walletRecoveryBackupAcknowledgementRecordKey(requireWalletId(record.walletId));
|
|
@@ -783,8 +796,11 @@ var CloudflareD1WalletCustodyCommitStore = class {
|
|
|
783
796
|
};
|
|
784
797
|
}
|
|
785
798
|
};
|
|
786
|
-
function
|
|
787
|
-
|
|
799
|
+
function sameRecoveryCodeKeys(left, right) {
|
|
800
|
+
if (left.walletId !== right.walletId) return false;
|
|
801
|
+
if (left.manifestKekWraps.length !== right.manifestKekWraps.length) return false;
|
|
802
|
+
for (let index = 0; index < left.manifestKekWraps.length; index += 1) if (left.manifestKekWraps[index]?.recoveryKeyId !== right.manifestKekWraps[index]?.recoveryKeyId) return false;
|
|
803
|
+
return true;
|
|
788
804
|
}
|
|
789
805
|
//#endregion
|
|
790
806
|
export { CloudflareD1WalletCustodyCommitStore };
|
package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletRecoveryGoogleEmailOtpService.js
CHANGED
|
@@ -141,7 +141,10 @@ var CloudflareD1WalletRecoveryGoogleEmailOtpService = class {
|
|
|
141
141
|
if (!workerEphemeralPublicKey65B64u) return failure("invalid_body", "Email OTP recovery factor release key is required");
|
|
142
142
|
const enrollment = await this.enrollments.readEnrollment(String(attempt.walletId));
|
|
143
143
|
if (!enrollment || enrollment.orgId !== attempt.orgId || enrollment.providerUserId !== attempt.providerSubject || enrollment.verifiedEmail !== attempt.verifiedEmail || enrollment.enrollmentId !== attempt.targetEnrollment.enrollmentId || enrollment.enrollmentSealKeyVersion !== attempt.targetEnrollment.enrollmentSealKeyVersion) return failure("recovery_conflict", "Email OTP recovery enrollment changed after verification");
|
|
144
|
-
const unsealed = await this.serverSeal.removeEmailOtpServerSeal({
|
|
144
|
+
const unsealed = await this.serverSeal.removeEmailOtpServerSeal({
|
|
145
|
+
wrappedCiphertext: enrollment.serverSealedFactorCiphertextB64u,
|
|
146
|
+
enrollmentSealKeyVersion: enrollment.enrollmentSealKeyVersion
|
|
147
|
+
});
|
|
145
148
|
if (!unsealed.ok) return unsealed;
|
|
146
149
|
if (unsealed.enrollmentSealKeyVersion !== enrollment.enrollmentSealKeyVersion) return failure("recovery_conflict", "Email OTP recovery enrollment seal changed after verification");
|
|
147
150
|
const sealed = await sealEmailOtpFactorSecretForWorker({
|
|
@@ -1390,6 +1390,12 @@ var CloudflareD1WalletAuthMethodService = class {
|
|
|
1390
1390
|
code: "unauthorized",
|
|
1391
1391
|
message: "Wallet Email OTP authority is unavailable"
|
|
1392
1392
|
};
|
|
1393
|
+
const walletAuthority = await this.walletAuthorityStore.readById(record.walletAuthorityId);
|
|
1394
|
+
if (!walletAuthority || walletAuthority.state !== "active" || walletAuthority.walletId !== record.walletId || !isFullOwnerAuthorityV1(walletAuthority)) return {
|
|
1395
|
+
ok: false,
|
|
1396
|
+
code: "forbidden",
|
|
1397
|
+
message: "Full owner authority is required"
|
|
1398
|
+
};
|
|
1393
1399
|
const provider = providerUserId.startsWith("google:") ? "google" : "email";
|
|
1394
1400
|
try {
|
|
1395
1401
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { base64UrlEncode } from "../../../../shared-ts/src/utils/base64.js";
|
|
1
2
|
import { parseDigestB64u } from "../../../../shared-ts/src/utils/canonicalPrimitives.js";
|
|
2
3
|
import { D1_BATCH_CAS_GUARD_SQL } from "../../../../storage/d1Sql.js";
|
|
3
4
|
import { readWalletRelocationOwner } from "./d1WalletRelocationOwner.js";
|
|
@@ -99,6 +100,12 @@ var D1WalletRelocationApprovals = class {
|
|
|
99
100
|
if (!approval || !Number.isSafeInteger(nowMs) || nowMs < approval.approvedAtMs || nowMs >= approval.expiresAtMs) return null;
|
|
100
101
|
const owner = await readWalletRelocationOwner(this.database, this.scope, binding);
|
|
101
102
|
if (!owner || owner.authority.authorityDigestB64u !== approval.authorityDigestB64u || owner.authority.revocationEpoch !== approval.authorityRevocationEpoch) return null;
|
|
103
|
+
if (!await this.database.prepare(`SELECT 1 FROM wallet_session_authorizations_v2
|
|
104
|
+
WHERE namespace = ?1 AND org_id = ?2 AND project_id = ?3 AND env_id = ?4
|
|
105
|
+
AND wallet_id = ?5 AND authority_id = ?6 AND wallet_auth_method_id = ?7
|
|
106
|
+
AND operation_credential_hash = ?8 AND expires_at_ms = ?9
|
|
107
|
+
AND retired_at_ms IS NULL AND expires_at_ms > ?10
|
|
108
|
+
AND expires_at_ms > CAST(unixepoch('subsec') * 1000 AS INTEGER)`).bind(this.scope.namespace, this.scope.orgId, this.scope.projectId, this.scope.envId, binding.walletId, binding.authorityId, binding.walletAuthMethodId, credentialDigestB64u(binding.credentialDigestHex), binding.credentialExpiresAtMs, nowMs).first()) return null;
|
|
102
109
|
return approval;
|
|
103
110
|
}
|
|
104
111
|
async read(binding) {
|
|
@@ -134,6 +141,11 @@ var D1WalletRelocationApprovals = class {
|
|
|
134
141
|
function failedStatement(result) {
|
|
135
142
|
return result.success !== true;
|
|
136
143
|
}
|
|
144
|
+
function credentialDigestB64u(hex) {
|
|
145
|
+
const bytes = new Uint8Array(32);
|
|
146
|
+
for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
147
|
+
return parseDigestB64u(base64UrlEncode(bytes));
|
|
148
|
+
}
|
|
137
149
|
//#endregion
|
|
138
150
|
export { D1WalletRelocationApprovals };
|
|
139
151
|
|