@seams/wallet-server 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/artifact-manifest.json +22 -21
  2. package/cloudflare-router-ab/build/deriver-a/build-stamp.json +1 -1
  3. package/cloudflare-router-ab/build/deriver-a/index_bg.wasm +0 -0
  4. package/cloudflare-router-ab/build/deriver-b/build-stamp.json +1 -1
  5. package/cloudflare-router-ab/build/deriver-b/index_bg.wasm +0 -0
  6. package/cloudflare-router-ab/build/local-tools/linux-x64/router_ab_local_init +0 -0
  7. package/cloudflare-router-ab/build/router/build-stamp.json +1 -1
  8. package/cloudflare-router-ab/build/router/index_bg.wasm +0 -0
  9. package/cloudflare-router-ab/build/signing-worker/build-stamp.json +1 -1
  10. package/cloudflare-router-ab/build/signing-worker/index_bg.wasm +0 -0
  11. package/cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json +1 -1
  12. package/cloudflare-router-ab/build/tenant-root-control-plane/index_bg.wasm +0 -0
  13. package/cloudflare-router-ab/scripts/prepare-local-runtime-config.mjs +16 -2
  14. package/dist/esm/core/deviceLinking/linkedDeviceManagement.js +11 -30
  15. package/dist/esm/core/emailOtpD1Statements.js +14 -2
  16. package/dist/esm/core/nearRelayerAccountProvisioning.js +2 -7
  17. package/dist/esm/hosted-wallet-gateway.js +10 -7
  18. package/dist/esm/router/auth/webAuthnCredentialCodecs.js +16 -1
  19. package/dist/esm/router/cloudflare/d1/auth/d1AuthorizationAssembly.js +1 -0
  20. package/dist/esm/router/cloudflare/d1/auth/d1RouterApiAuthService.js +0 -1
  21. package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceEmailOtpTargetFactor.js +4 -1
  22. package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceManagementService.js +6 -11
  23. package/dist/esm/router/cloudflare/d1/deviceLinking/d1LinkedDeviceRevocation.js +7 -0
  24. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpGrantStore.js +5 -2
  25. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRateLimitStore.js +18 -0
  26. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRecoveryService.js +28 -8
  27. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpServerSealRuntime.js +12 -8
  28. package/dist/esm/router/cloudflare/d1/oidc/d1OidcBoundary.js +1 -0
  29. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.js +19 -5
  30. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.js +26 -10
  31. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletRecoveryGoogleEmailOtpService.js +4 -1
  32. package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodService.js +6 -0
  33. package/dist/esm/router/cloudflare/d1/wallet/d1WalletRelocationApprovals.js +12 -0
  34. package/dist/esm/router/cloudflare.js +252 -150
  35. package/dist/esm/router/domains/ecdsa/routerAbEcdsaStrictRegistration.js +1 -1
  36. package/dist/esm/router/domains/passkeyCustody/passkeyCustodyEnvelopeRetrieval.js +4 -22
  37. package/dist/esm/router/domains/passkeyCustody/registrationCustodyOutcome.js +8 -2
  38. package/dist/esm/router/framework/routeDefinitions.js +3 -2
  39. package/dist/esm/router/transport/fetch/routes/auth.js +5 -0
  40. package/dist/esm/router/transport/fetch/routes/passkeyCustody.js +78 -88
  41. package/dist/esm/router/transport/fetch/routes/sessions.js +4 -1
  42. package/dist/esm/router/transport/fetch/routes/thresholdEd25519.js +4 -1
  43. package/dist/esm/router/transport/fetch/routes/walletCustodyRequestBoundary.js +80 -0
  44. package/dist/esm/shared-ts/src/passkey-custody/ceremonyCommitPayload.js +5 -1
  45. package/dist/esm/shared-ts/src/utils/evmRpcResults.js +5 -3
  46. package/dist/esm/shared-ts/src/utils/routerAbEcdsaDerivation.js +2 -1
  47. package/dist/esm/threshold/session/signingSessionSeal/crypto/cipher.js +160 -40
  48. package/dist/types/shared-ts/src/authorization/walletCustodyOperation.d.ts +1 -1
  49. package/dist/types/shared-ts/src/device-linking/contracts.d.ts +1 -7
  50. package/dist/types/shared-ts/src/passkey-custody/ceremonyCommitPayload.d.ts +5 -0
  51. package/dist/types/shared-ts/src/utils/evmRpcResults.d.ts +3 -3
  52. package/dist/types/shared-ts/src/utils/routerAbEcdsaDerivation.d.ts +1 -1
  53. package/dist/types/wallet-server/src/core/authService/emailOtpSeal.d.ts +5 -1
  54. package/dist/types/wallet-server/src/core/deviceLinking/linkedDeviceManagement.d.ts +1 -9
  55. package/dist/types/wallet-server/src/core/emailOtpD1Statements.d.ts +2 -1
  56. package/dist/types/wallet-server/src/hosted-wallet-gateway.d.ts +1 -0
  57. package/dist/types/wallet-server/src/router/auth/webAuthnCredentialCodecs.d.ts +1 -0
  58. package/dist/types/wallet-server/src/router/cloudflare/d1/deviceLinking/d1LinkedDeviceManagementService.d.ts +0 -3
  59. package/dist/types/wallet-server/src/router/cloudflare/d1/deviceLinking/d1LinkedDeviceRevocation.d.ts +1 -1
  60. package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpGrantStore.d.ts +3 -1
  61. package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpRateLimitStore.d.ts +15 -2
  62. package/dist/types/wallet-server/src/router/cloudflare/d1/emailOtp/d1EmailOtpServerSealRuntime.d.ts +3 -3
  63. package/dist/types/wallet-server/src/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.d.ts +3 -0
  64. package/dist/types/wallet-server/src/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.d.ts +5 -6
  65. package/dist/types/wallet-server/src/router/domains/passkeyCustody/walletCustodyRegistrationCommit.d.ts +4 -6
  66. package/dist/types/wallet-server/src/router/framework/authServicePort.d.ts +7 -1
  67. package/dist/types/wallet-server/src/router/transport/fetch/routes/passkeyCustody.d.ts +1 -10
  68. package/dist/types/wallet-server/src/router/transport/fetch/routes/walletCustodyRequestBoundary.d.ts +28 -0
  69. package/dist/types/wallet-server/src/threshold/session/signingSessionSeal/crypto/cipher.d.ts +3 -0
  70. package/package.json +1 -1
  71. package/src/README.md +11 -4
@@ -1,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 { createSigningSessionSealShamir3PassCipherAdapter } from "../../../../threshold/session/signingSessionSeal/crypto/cipher.js";
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
- return await this.runServerSealOperation(input, removeEmailOtpServerSealOperation);
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
- return await this.runServerSealOperation(input, applyEmailOtpServerSealOperation);
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(input, operation) {
31
+ async runServerSealOperation(wrappedCiphertextRaw, operation, keyVersion) {
28
32
  try {
29
- const wrappedCiphertext = normalizeOptionalTrimmedString(input.wrappedCiphertext);
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: shamir.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 || shamir.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: createSigningSessionSealShamir3PassCipherAdapter({ config: this.config.rootConfig })
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 === 200 && request.locator.factor.kind === "passkey") await assembly.passkeyCustodyEnvelopes.recordWalletCredentialUse({
296
- walletId: request.locator.walletId,
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
- await assembly.walletCustodyCommits.writeBackupAcknowledgement(buildWalletRecoveryBackupAcknowledgementV1({
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
- for (const entry of existing) if (entry.key === envelopeKey && entry.result.kind !== "missing") return { kind: "already_exists" };
379
- if (existing.some(custodyRecordExists)) return { kind: "custody_already_established" };
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 their codes.
447
- *
448
- * Written with no expected version, unlike the recovery set: this row is
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 custodyRecordExists(entry) {
787
- return entry.result.kind !== "missing";
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 };
@@ -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({ wrappedCiphertext: enrollment.serverSealedFactorCiphertextB64u });
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