@seams/wallet-server 0.28.0 → 0.29.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 (80) hide show
  1. package/README.md +19 -4
  2. package/artifact-manifest.json +23 -13
  3. package/cloudflare-router-ab/build/deriver-a/build-stamp.json +1 -1
  4. package/cloudflare-router-ab/build/deriver-b/build-stamp.json +1 -1
  5. package/cloudflare-router-ab/build/router/build-stamp.json +1 -1
  6. package/cloudflare-router-ab/build/signing-worker/build-stamp.json +1 -1
  7. package/cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json +1 -1
  8. package/dist/esm/application-auth.js +2 -0
  9. package/dist/esm/auth/applicationAuth.js +85 -0
  10. package/dist/esm/auth/externalAuthService.js +58 -7
  11. package/dist/esm/auth/externalAuthStore.js +157 -21
  12. package/dist/esm/auth/externalOwnerOperations.js +82 -0
  13. package/dist/esm/cloud-host.js +4 -4
  14. package/dist/esm/core/nearRelayerAccountProvisioning.js +1 -1
  15. package/dist/esm/router/cloudflare/d1/auth/d1AuthorizedOperationRouteService.js +2 -0
  16. package/dist/esm/router/cloudflare/d1/auth/d1RecordedOwnerOperation.js +43 -0
  17. package/dist/esm/router/cloudflare/d1/auth/d1RouterApiAuthService.js +22 -1
  18. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRecoveryService.js +1 -1
  19. package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRegistrationEnrollmentFinalizer.js +1 -1
  20. package/dist/esm/router/cloudflare/d1/emailOtp/d1GoogleEmailOtpSessionResolver.js +1 -1
  21. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.js +2 -2
  22. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.js +2 -2
  23. package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletRecoveryGoogleEmailOtpService.js +2 -2
  24. package/dist/esm/router/cloudflare/d1/registration/d1RegistrationCeremonyRecords.js +9 -0
  25. package/dist/esm/router/cloudflare/d1/registration/d1WalletExecutionAuthority.js +9 -16
  26. package/dist/esm/router/cloudflare/d1/registration/d1WalletRegistrationService.js +43 -1
  27. package/dist/esm/router/cloudflare/d1/registration/d1WalletRegistrationSetup.js +12 -0
  28. package/dist/esm/router/cloudflare/d1/registration/d1WalletRegistrationSetupExecution.js +1 -0
  29. package/dist/esm/router/cloudflare/d1/registration/registrationCompletionWrite.js +1 -0
  30. package/dist/esm/router/cloudflare/d1/registration/walletRegistrationSessionCommitReceipt.js +35 -0
  31. package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodBoundary.js +1 -1
  32. package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodService.js +4 -3
  33. package/dist/esm/router/cloudflare.js +1785 -1489
  34. package/dist/esm/router/domains/ed25519Yao/export/routerAbEd25519YaoExport.js +2 -2
  35. package/dist/esm/router/domains/emailOtp/emailOtpRouteHandlers.js +1 -1
  36. package/dist/esm/router/domains/passkeyCustody/walletCustodyRegistrationCommit.js +1 -1
  37. package/dist/esm/router/domains/signingOperations/routerAbEd25519NormalSigningRoute.js +35 -1
  38. package/dist/esm/router/domains/walletRegistration/walletRegistrationReservation.js +17 -2
  39. package/dist/esm/router/domains/walletRegistration/walletRegistrationRoutes.js +9 -1
  40. package/dist/esm/router/domains/walletUnlock/walletUnlockRouteHandlers.js +1 -1
  41. package/dist/esm/router/transport/fetch/routes/ecdsaOperationStepUpFactor.js +2 -2
  42. package/dist/esm/router/transport/fetch/routes/externalAuth.js +139 -32
  43. package/dist/esm/router/transport/fetch/routes/passkeyCustody.js +44 -304
  44. package/dist/esm/router/transport/fetch/routes/sessions.js +2 -2
  45. package/dist/esm/router/transport/fetch/routes/thresholdEd25519.js +2 -2
  46. package/dist/esm/router/transport/fetch/routes/walletRegistration.js +2 -2
  47. package/dist/esm/router/transport/fetch/walletOwnerOperationAuthorization.js +306 -0
  48. package/dist/esm/shared-ts/src/auth/externalAuth.js +87 -14
  49. package/dist/esm/shared-ts/src/authorization/index.js +1 -1
  50. package/dist/esm/shared-ts/src/authorization/{walletCustodyOperation.js → walletOwnerOperation.js} +4 -4
  51. package/dist/esm/shared-ts/src/wallet-recovery/index.js +3 -3
  52. package/dist/types/shared-ts/src/auth/externalAuth.d.ts +51 -8
  53. package/dist/types/shared-ts/src/authorization/index.d.ts +1 -1
  54. package/dist/types/shared-ts/src/authorization/walletOwnerOperation.d.ts +10 -0
  55. package/dist/types/shared-ts/src/utils/registrationEstablishedSession.d.ts +4 -0
  56. package/dist/types/shared-ts/src/utils/registrationIntent.d.ts +2 -0
  57. package/dist/types/shared-ts/src/utils/walletManagementProtocol.d.ts +1 -1
  58. package/dist/types/wallet-server/src/application-auth.d.ts +3 -0
  59. package/dist/types/wallet-server/src/auth/applicationAuth.d.ts +29 -0
  60. package/dist/types/wallet-server/src/auth/externalAuthService.d.ts +29 -10
  61. package/dist/types/wallet-server/src/auth/externalAuthStore.d.ts +60 -6
  62. package/dist/types/wallet-server/src/auth/externalOwnerOperations.d.ts +18 -0
  63. package/dist/types/wallet-server/src/cloud-host.d.ts +1 -1
  64. package/dist/types/wallet-server/src/core/threeRouteRegistrationContracts.d.ts +12 -0
  65. package/dist/types/wallet-server/src/router/cloudflare/d1/auth/d1AuthorizedOperationRouteService.d.ts +5 -1
  66. package/dist/types/wallet-server/src/router/cloudflare/d1/auth/d1RecordedOwnerOperation.d.ts +16 -0
  67. package/dist/types/wallet-server/src/router/cloudflare/d1/registration/d1RegistrationCeremonyRecords.d.ts +1 -0
  68. package/dist/types/wallet-server/src/router/cloudflare/d1/registration/d1WalletRegistrationSetup.d.ts +2 -0
  69. package/dist/types/wallet-server/src/router/cloudflare/d1/registration/walletRegistrationSessionCommitReceipt.d.ts +7 -1
  70. package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1WalletAuthMethodService.d.ts +1 -0
  71. package/dist/types/wallet-server/src/router/domains/signingOperations/routerAbEd25519NormalSigningRoute.d.ts +2 -2
  72. package/dist/types/wallet-server/src/router/domains/signingOperations/routerAbOperationStepUp.d.ts +1 -1
  73. package/dist/types/wallet-server/src/router/domains/walletRegistration/walletRegistrationInputs.d.ts +1 -0
  74. package/dist/types/wallet-server/src/router/domains/walletRegistration/walletRegistrationReservation.d.ts +11 -6
  75. package/dist/types/wallet-server/src/router/framework/authServicePort.d.ts +11 -0
  76. package/dist/types/wallet-server/src/router/transport/fetch/walletOwnerOperationAuthorization.d.ts +23 -0
  77. package/migrations/d1-signer/0086_application_auth.sql +67 -0
  78. package/migrations/d1-signer/0087_registration_without_session.sql +43 -0
  79. package/package.json +7 -2
  80. package/dist/types/shared-ts/src/authorization/walletCustodyOperation.d.ts +0 -10
package/README.md CHANGED
@@ -4,14 +4,22 @@ This package owns the reusable signer server surface: route adapters,
4
4
  WebAuthn verification policy, threshold signing services, signer storage
5
5
  adapters, and server wasm bindings.
6
6
 
7
- The current published package is `@seams/wallet-server@0.17.0`, checked
8
- October 21, 2026. Pair it with `@seams/wallet@0.17.0` and matching hosted assets.
9
- Use `@seams/wallet-cli@0.17.0`.
7
+ The current release is `@seams/wallet-server@0.29.0`. Pair it with
8
+ `@seams/wallet@0.29.0` and matching hosted assets. Use `@seams/wallet-cli@0.29.0`.
9
+ The wallet iframe protocol is **19.0.0**.
10
10
 
11
11
  ```sh
12
- pnpm add @seams/wallet-server@0.17.0
12
+ pnpm add @seams/wallet-server@0.29.0
13
13
  ```
14
14
 
15
+ ## Google BYO authentication
16
+
17
+ Version 0.29.0 verifies a configured application's signed session assertion or the app's Google ID token or exchanges it for a browser-bound handoff.
18
+ An environment-specific Google client configuration and an owner-approved wallet association are required.
19
+ Apply the packaged signer migrations before enabling the feature.
20
+ External sessions grant settings reads and remain separate from owner and signing credentials.
21
+ See the [Google SSO guide](https://wallet.seams.sh/docs/guides/google-sso) for configuration, backend exchange, and revocation.
22
+
15
23
  ## Supported runtime and entry points
16
24
 
17
25
  The packaged Wallet backend runs on Cloudflare Workers with Durable Objects,
@@ -56,3 +64,10 @@ and [release notes](https://wallet.seams.sh/docs/releases).
56
64
 
57
65
  Hosted product services and their operational tooling live in the private
58
66
  `seams-monorepo` repository.
67
+
68
+
69
+ Use `createApplicationAuthAssertion` from `@seams/wallet-server/application-auth` on the authenticated application backend.
70
+ Configure the issuer and public key through the existing environment trust setting.
71
+ The assertion binds the stable subject, environment, wallet, Seams attempt, purpose, and expiry.
72
+ Fresh wallet-origin owner proof remains required for identity changes and NEAR transactions.
73
+ See the [application-session guide](https://wallet.seams.sh/docs/guides/application-auth).
@@ -3,17 +3,17 @@
3
3
  "walletSecurityPolicy": "scoped_seals_and_owner_proofs_v1",
4
4
  "package": {
5
5
  "name": "@seams/wallet-server",
6
- "version": "0.28.0"
6
+ "version": "0.29.0"
7
7
  },
8
8
  "workers": {
9
9
  "router": {
10
10
  "entry": "cloudflare-router-ab/build/router/worker/shim.mjs",
11
- "fingerprint": "9354a23aa130d53ead7ac72f4101910e46f52e992519f44bb50f1804c3563808",
11
+ "fingerprint": "bc4c3fb74b4869b98d280dff20ea71488b6e41d9de440d10d26b292360243673",
12
12
  "files": [
13
13
  {
14
14
  "path": "cloudflare-router-ab/build/router/build-stamp.json",
15
15
  "bytes": 119,
16
- "sha256": "fb89fa2ce50c5af95cb5464754523d31cbc827d780e3cc5767f55df49170f171"
16
+ "sha256": "f1f625e28fd7c2de2ba5d7a5f84ec06d21411d0fcb59a0d6ac687373c4a2a0fd"
17
17
  },
18
18
  {
19
19
  "path": "cloudflare-router-ab/build/router/index.js",
@@ -39,12 +39,12 @@
39
39
  },
40
40
  "deriver-a": {
41
41
  "entry": "cloudflare-router-ab/build/deriver-a/worker/shim.mjs",
42
- "fingerprint": "9aa8706dd8d77691a8afa8096f3295b1e94d6a92f67c4106d8edd950ff838bbd",
42
+ "fingerprint": "5ed3fc53704436a171fc57434e2ad732eb008cf12f1a30c3c2c0d895aaec2b36",
43
43
  "files": [
44
44
  {
45
45
  "path": "cloudflare-router-ab/build/deriver-a/build-stamp.json",
46
46
  "bytes": 122,
47
- "sha256": "b61d9f995aa1e168b0d04ccc55e41ca2adc45aac5139ec5bbca8c097b340d3dd"
47
+ "sha256": "70c82f021615ac41e7b6e68ec7f64c21ba5202d97463710f3184489c693e5d67"
48
48
  },
49
49
  {
50
50
  "path": "cloudflare-router-ab/build/deriver-a/index.js",
@@ -70,12 +70,12 @@
70
70
  },
71
71
  "deriver-b": {
72
72
  "entry": "cloudflare-router-ab/build/deriver-b/worker/shim.mjs",
73
- "fingerprint": "ff8f66cca668aba7d4d2adf0f6183731098149012461d03880f43d2155fc04e5",
73
+ "fingerprint": "8861d75c70c0660f85abdb2fb1d5e05f346c0185180de6482c676b16188e1d89",
74
74
  "files": [
75
75
  {
76
76
  "path": "cloudflare-router-ab/build/deriver-b/build-stamp.json",
77
77
  "bytes": 122,
78
- "sha256": "a0a83b79b96f30cc6b5c97f933d921a4c8842a2c138b1548971f1b09f8a58980"
78
+ "sha256": "01cd377ff0a7934d33794174f1f9ddfe3e274bcf14d850516e3b0482f7593986"
79
79
  },
80
80
  {
81
81
  "path": "cloudflare-router-ab/build/deriver-b/index.js",
@@ -101,12 +101,12 @@
101
101
  },
102
102
  "signing-worker": {
103
103
  "entry": "cloudflare-router-ab/build/signing-worker/worker/shim.mjs",
104
- "fingerprint": "7970e31e74739d47eebd1ee17d5ddde2a43c12eca8f9682cf65acd491eb4823a",
104
+ "fingerprint": "c88383e59192e1d8fe7424cd29b4d2045cac3d5fdb6be31770e825ff25c59358",
105
105
  "files": [
106
106
  {
107
107
  "path": "cloudflare-router-ab/build/signing-worker/build-stamp.json",
108
108
  "bytes": 127,
109
- "sha256": "ce676751103f5b0457884cecf38a1ecda5af7033e3c93e71d17bb8f7eecbbb7e"
109
+ "sha256": "3ed8b25f8604a6d815c5c2f83d82c7775c1d360840ea6f3d58f1335d3ddc0f23"
110
110
  },
111
111
  {
112
112
  "path": "cloudflare-router-ab/build/signing-worker/index.js",
@@ -132,12 +132,12 @@
132
132
  },
133
133
  "tenant-root-control-plane": {
134
134
  "entry": "cloudflare-router-ab/build/tenant-root-control-plane/worker/shim.mjs",
135
- "fingerprint": "70f00da01cea7d89aca4c051a85344fd72ec553bb92a63d0d54db2171a59c2c2",
135
+ "fingerprint": "87fcae5b88541e0f69333ae632568e286939ec22bf75395d490be3ba6aeb4272",
136
136
  "files": [
137
137
  {
138
138
  "path": "cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json",
139
139
  "bytes": 139,
140
- "sha256": "9782d8c4537c51cbd99632eca61ead34f7137b7fd18ea4feafd4db6f27df5255"
140
+ "sha256": "ff8878e625a5202b8d8607c590a89c2b169ae385c826eb154b5797babb3bf73d"
141
141
  },
142
142
  {
143
143
  "path": "cloudflare-router-ab/build/tenant-root-control-plane/index.js",
@@ -165,8 +165,8 @@
165
165
  "migrations": {
166
166
  "signer": {
167
167
  "directory": "migrations/d1-signer",
168
- "head": "0085_external_wallet_auth.sql",
169
- "fingerprint": "a73558668541d4d00f06178f690a239cff675b118fe39c2e8ea2b5c5ec36cc86",
168
+ "head": "0087_registration_without_session.sql",
169
+ "fingerprint": "2901c4c1b711fde38af1371311fea9e8ae8133c5cc4f47fc0cd5588e1ddf23aa",
170
170
  "files": [
171
171
  {
172
172
  "path": "migrations/d1-signer/0001_signer_d1_initial.sql",
@@ -592,6 +592,16 @@
592
592
  "path": "migrations/d1-signer/0085_external_wallet_auth.sql",
593
593
  "bytes": 9757,
594
594
  "sha256": "62e98c8200f18cc1d6bc399891f2fd7c57e5b0639f251d10ad6cf7d7b5e30461"
595
+ },
596
+ {
597
+ "path": "migrations/d1-signer/0086_application_auth.sql",
598
+ "bytes": 4354,
599
+ "sha256": "a9ecaeec04383536993cb9052aebe188a5035f766fd1ef73b51b9e85736a6bb1"
600
+ },
601
+ {
602
+ "path": "migrations/d1-signer/0087_registration_without_session.sql",
603
+ "bytes": 2906,
604
+ "sha256": "1ed454bb25fa5a9b28567bc1f18aad718c086f8b182255fabe2ee01215292ff3"
595
605
  }
596
606
  ]
597
607
  },
@@ -1 +1 @@
1
- {"profile":"release","wallet_objects":true,"features":"strict-worker-deriver-a-entrypoint","started_at_ms":1793281741952}
1
+ {"profile":"release","wallet_objects":true,"features":"strict-worker-deriver-a-entrypoint","started_at_ms":1793310989490}
@@ -1 +1 @@
1
- {"profile":"release","wallet_objects":true,"features":"strict-worker-deriver-b-entrypoint","started_at_ms":1793281927715}
1
+ {"profile":"release","wallet_objects":true,"features":"strict-worker-deriver-b-entrypoint","started_at_ms":1793311149098}
@@ -1 +1 @@
1
- {"profile":"release","wallet_objects":true,"features":"strict-worker-router-entrypoint","started_at_ms":1793282099467}
1
+ {"profile":"release","wallet_objects":true,"features":"strict-worker-router-entrypoint","started_at_ms":1793311295695}
@@ -1 +1 @@
1
- {"profile":"release","wallet_objects":true,"features":"strict-worker-signing-worker-entrypoint","started_at_ms":1793281450811}
1
+ {"profile":"release","wallet_objects":true,"features":"strict-worker-signing-worker-entrypoint","started_at_ms":1793310743369}
@@ -1 +1 @@
1
- {"profile":"release","wallet_objects":false,"features":"strict-worker-tenant-root-control-plane-entrypoint","started_at_ms":1793282301221}
1
+ {"profile":"release","wallet_objects":false,"features":"strict-worker-tenant-root-control-plane-entrypoint","started_at_ms":1793311468276}
@@ -0,0 +1,2 @@
1
+ import { createApplicationAuthAssertion } from "./auth/applicationAuth.js";
2
+ export { createApplicationAuthAssertion };
@@ -0,0 +1,85 @@
1
+ import { requireCanonicalString, requireRecord } from "../shared-ts/src/utils/validation.js";
2
+ import { base64UrlDecode, base64UrlEncode } from "../shared-ts/src/utils/base64.js";
3
+ import { externalAuthFailure } from "../shared-ts/src/auth/externalAuth.js";
4
+ //#region src/auth/applicationAuth.ts
5
+ const ASSERTION_TTL_MS = 12e4;
6
+ function parseClaims(raw) {
7
+ const value = requireRecord(raw, "application assertion claims");
8
+ if (typeof value.iat !== "number" || typeof value.exp !== "number" || typeof value.sessionExp !== "number") throw new Error("Application assertion timestamps are missing");
9
+ if (!Number.isSafeInteger(value.iat) || !Number.isSafeInteger(value.exp) || !Number.isSafeInteger(value.sessionExp)) throw new Error("Application assertion timestamps must be integer seconds");
10
+ if (value.purpose !== "authenticate" && value.purpose !== "associate") throw new Error("Application assertion purpose is invalid");
11
+ return {
12
+ iss: requireCanonicalString(value.iss, "iss"),
13
+ sub: requireCanonicalString(value.sub, "sub"),
14
+ aud: requireCanonicalString(value.aud, "aud"),
15
+ walletId: requireCanonicalString(value.walletId, "walletId"),
16
+ attemptId: requireCanonicalString(value.attemptId, "attemptId"),
17
+ iat: value.iat,
18
+ exp: value.exp,
19
+ sessionExp: value.sessionExp,
20
+ purpose: value.purpose
21
+ };
22
+ }
23
+ function encodeJson(value) {
24
+ return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
25
+ }
26
+ function decodeJson(value) {
27
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(base64UrlDecode(value)));
28
+ }
29
+ function audience(environmentId) {
30
+ return `seams:external-auth:${environmentId}`;
31
+ }
32
+ /** Called only by the application's authenticated backend with its enrolled issuer key. */
33
+ async function createApplicationAuthAssertion(input) {
34
+ if (input.privateKey.type !== "private" || input.privateKey.algorithm.name !== "Ed25519") throw new Error("Application issuer requires an Ed25519 signing key");
35
+ const claims = parseClaims({
36
+ iss: input.issuer,
37
+ sub: input.subject,
38
+ aud: audience(requireCanonicalString(input.environmentId, "environmentId")),
39
+ walletId: input.walletId,
40
+ attemptId: input.attemptId,
41
+ iat: Math.floor(input.nowMs / 1e3),
42
+ exp: Math.floor(Math.min(input.nowMs + ASSERTION_TTL_MS, input.sessionExpiresAtMs) / 1e3),
43
+ sessionExp: Math.floor(input.sessionExpiresAtMs / 1e3),
44
+ purpose: input.purpose
45
+ });
46
+ const signed = `${encodeJson({
47
+ alg: "EdDSA",
48
+ typ: "JWT"
49
+ })}.${encodeJson(claims)}`;
50
+ return `${signed}.${base64UrlEncode(await crypto.subtle.sign("Ed25519", input.privateKey, new TextEncoder().encode(signed)))}`;
51
+ }
52
+ async function verifyApplicationAuthAssertion(input) {
53
+ try {
54
+ const segments = input.assertion.split(".");
55
+ if (segments.length !== 3) return externalAuthFailure("invalid_credential");
56
+ const [headerText, claimsText, signatureText] = segments;
57
+ if (!headerText || !claimsText || !signatureText) return externalAuthFailure("invalid_credential");
58
+ const header = requireRecord(decodeJson(headerText), "application assertion header");
59
+ if (header.alg !== "EdDSA" || header.typ !== "JWT" || "crit" in header) return externalAuthFailure("invalid_credential");
60
+ const key = await crypto.subtle.importKey("raw", base64UrlDecode(input.trust.publicKeyB64u), "Ed25519", false, ["verify"]);
61
+ if (!await crypto.subtle.verify("Ed25519", key, base64UrlDecode(signatureText), new TextEncoder().encode(`${headerText}.${claimsText}`))) return externalAuthFailure("invalid_credential");
62
+ const claims = parseClaims(decodeJson(claimsText));
63
+ const expiresAtMs = claims.exp * 1e3;
64
+ const issuedAtMs = claims.iat * 1e3;
65
+ if (expiresAtMs <= input.nowMs) return externalAuthFailure("expired");
66
+ if (claims.iss !== input.trust.issuer || claims.aud !== audience(input.environmentId) || claims.walletId !== input.walletId || claims.attemptId !== input.attemptId || claims.purpose !== input.purpose || issuedAtMs > input.nowMs + 3e4 || expiresAtMs <= issuedAtMs || expiresAtMs - issuedAtMs > ASSERTION_TTL_MS || claims.sessionExp < claims.exp) return externalAuthFailure("invalid_credential");
67
+ return {
68
+ ok: true,
69
+ value: {
70
+ identity: {
71
+ provider: "application",
72
+ issuer: claims.iss,
73
+ subject: claims.sub
74
+ },
75
+ expiresAtMs: claims.sessionExp * 1e3
76
+ }
77
+ };
78
+ } catch {
79
+ return externalAuthFailure("invalid_credential");
80
+ }
81
+ }
82
+ //#endregion
83
+ export { createApplicationAuthAssertion, verifyApplicationAuthAssertion };
84
+
85
+ //# sourceMappingURL=applicationAuth.js.map
@@ -1,6 +1,7 @@
1
1
  import { base64UrlEncode } from "../shared-ts/src/utils/base64.js";
2
2
  import { sha256HexUtf8 } from "../shared-ts/src/utils/digests.js";
3
3
  import { externalAuthFailure } from "../shared-ts/src/auth/externalAuth.js";
4
+ import { verifyApplicationAuthAssertion } from "./applicationAuth.js";
4
5
  import { verifyGoogleOidcToken } from "../router/cloudflare/d1/oidc/googleOidcToken.js";
5
6
  //#region src/auth/externalAuthService.ts
6
7
  var ExternalAuthService = class {
@@ -16,9 +17,12 @@ var ExternalAuthService = class {
16
17
  return {
17
18
  ok: true,
18
19
  value: {
19
- subject: proof.sub,
20
- expiresAtMs: proof.expiresAtMs,
21
- trust
20
+ identity: {
21
+ provider: "google",
22
+ issuer: "https://accounts.google.com",
23
+ subject: proof.sub
24
+ },
25
+ expiresAtMs: proof.expiresAtMs
22
26
  }
23
27
  };
24
28
  }
@@ -26,6 +30,7 @@ var ExternalAuthService = class {
26
30
  const attemptId = crypto.randomUUID();
27
31
  const expiresAtMs = Date.now() + 12e4;
28
32
  await this.store.prepareAttempt({
33
+ purpose: input.purpose,
29
34
  attemptId,
30
35
  walletId: input.walletId,
31
36
  originBinding: input.originBinding,
@@ -36,6 +41,7 @@ var ExternalAuthService = class {
36
41
  return {
37
42
  ok: true,
38
43
  value: {
44
+ purpose: input.purpose,
39
45
  attemptId,
40
46
  walletId: input.walletId,
41
47
  expiresAtMs
@@ -43,23 +49,68 @@ var ExternalAuthService = class {
43
49
  };
44
50
  }
45
51
  async exchange(input) {
46
- const verified = await this.verifyGoogle(input.trust, input.idToken);
52
+ const purpose = await this.store.readPreparedPurpose(input.walletId, input.attemptId, Date.now());
53
+ if (purpose === null) return externalAuthFailure("access_denied");
54
+ const verified = await this.verifyIdentity({
55
+ trust: input.trust,
56
+ proof: input.proof,
57
+ walletId: input.walletId,
58
+ attemptId: input.attemptId,
59
+ purpose
60
+ });
47
61
  if (!verified.ok) return verified;
48
62
  const code = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
49
63
  const nowMs = Date.now();
50
64
  return await this.store.verifyAttempt({
51
65
  attemptId: input.attemptId,
52
66
  walletId: input.walletId,
53
- subject: verified.value.subject,
54
- trust: verified.value.trust,
67
+ identity: verified.value.identity,
68
+ trust: input.trust,
55
69
  codeHash: await sha256HexUtf8(code),
56
70
  nowMs,
57
- sessionExpiresAtMs: Math.min(nowMs + 36e5, verified.value.expiresAtMs)
71
+ sessionExpiresAtMs: Math.min(nowMs + 36e5, verified.value.expiresAtMs),
72
+ purpose
58
73
  }) ? {
59
74
  ok: true,
60
75
  value: { code }
61
76
  } : externalAuthFailure("access_denied");
62
77
  }
78
+ async prepareUnlink(input) {
79
+ const attemptId = crypto.randomUUID();
80
+ const code = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
81
+ const expiresAtMs = Date.now() + 12e4;
82
+ return await this.store.prepareUnlink({
83
+ walletId: input.walletId,
84
+ associationId: input.associationId,
85
+ attemptId,
86
+ originBinding: input.originBinding,
87
+ bindingHash: await sha256HexUtf8(input.binding),
88
+ sessionTokenHash: await sha256HexUtf8(input.sessionToken),
89
+ codeHash: await sha256HexUtf8(code),
90
+ expiresAtMs,
91
+ trustRevision: input.trustRevision
92
+ }) ? {
93
+ ok: true,
94
+ value: {
95
+ attemptId,
96
+ code,
97
+ expiresAtMs
98
+ }
99
+ } : externalAuthFailure("access_denied");
100
+ }
101
+ async verifyIdentity(input) {
102
+ if (input.trust.kind === "google" && input.proof.kind === "google") return this.verifyGoogle(input.trust, input.proof.idToken);
103
+ if (input.trust.kind === "application" && input.proof.kind === "application") return verifyApplicationAuthAssertion({
104
+ trust: input.trust,
105
+ assertion: input.proof.assertion,
106
+ environmentId: this.store.scope.envId,
107
+ walletId: input.walletId,
108
+ attemptId: input.attemptId,
109
+ nowMs: Date.now(),
110
+ purpose: input.purpose
111
+ });
112
+ return externalAuthFailure("invalid_credential");
113
+ }
63
114
  async settings(authentication) {
64
115
  const walletId = authentication.walletId;
65
116
  const [ed25519, ecdsa, methods, devices] = await Promise.all([
@@ -1,6 +1,6 @@
1
1
  import { requireCanonicalString } from "../shared-ts/src/utils/validation.js";
2
2
  import { parseUnixMs } from "../shared-ts/src/passkey-custody/primitives.js";
3
- import { parseExternalWalletAuthentication } from "../shared-ts/src/auth/externalAuth.js";
3
+ import { parseExternalWalletAuthentication, parseStoredExternalIdentity, serializeExternalIdentity } from "../shared-ts/src/auth/externalAuth.js";
4
4
  import { d1ChangedRows } from "../storage/d1Sql.js";
5
5
  import { prepareD1TenantStatement } from "../core/d1TenantStore.js";
6
6
  //#region src/auth/externalAuthStore.ts
@@ -28,6 +28,11 @@ const LIVE_ASSOCIATION = `EXISTS (
28
28
  AND w.lifecycle_state = 'active' AND w.revocation_epoch = a.authority_epoch
29
29
  AND ${ACTIVE_HOME}
30
30
  )`;
31
+ const AUTHENTICATION_COLUMNS = `attempt_id, association_id, wallet_id, session_expires_at_ms,
32
+ (SELECT a.provider_subject FROM external_auth_associations a
33
+ WHERE a.namespace = external_auth_attempts.namespace AND a.org_id = external_auth_attempts.org_id
34
+ AND a.project_id = external_auth_attempts.project_id AND a.env_id = external_auth_attempts.env_id
35
+ AND a.association_id = external_auth_attempts.association_id) AS provider_subject`;
31
36
  var ExternalAuthStore = class {
32
37
  constructor(database, scope) {
33
38
  this.database = database;
@@ -50,6 +55,7 @@ var ExternalAuthStore = class {
50
55
  ]).run()) === 1;
51
56
  }
52
57
  async associate(input) {
58
+ const subject = serializeExternalIdentity(input.identity);
53
59
  const revokePriorAuthority = this.prepare(`UPDATE external_auth_associations SET status = 'revoked'
54
60
  WHERE ${SCOPE} AND wallet_id = ?5 AND provider_subject = ?6 AND status = 'active'
55
61
  AND (authority_id != ?7 OR authority_epoch != ?8)
@@ -57,7 +63,7 @@ var ExternalAuthStore = class {
57
63
  AND wallet_id = ?5 AND authority_id = ?7 AND lifecycle_state = 'active'
58
64
  AND revocation_epoch = ?8)`, [
59
65
  input.walletId,
60
- input.subject,
66
+ subject,
61
67
  input.authorityId,
62
68
  input.authorityEpoch
63
69
  ]);
@@ -72,7 +78,7 @@ var ExternalAuthStore = class {
72
78
  WHERE status = 'active' DO NOTHING`, [
73
79
  crypto.randomUUID(),
74
80
  input.walletId,
75
- input.subject,
81
+ subject,
76
82
  input.authorityId,
77
83
  input.authorityEpoch
78
84
  ]);
@@ -86,14 +92,128 @@ var ExternalAuthStore = class {
86
92
  AND wallet_id = ?5 AND authority_id = ?7 AND lifecycle_state = 'active'
87
93
  AND revocation_epoch = ?8)`, [
88
94
  input.walletId,
89
- input.subject,
95
+ subject,
90
96
  input.authorityId,
91
97
  input.authorityEpoch
92
98
  ]).first() !== null;
93
99
  }
94
- async revokeAssociation(walletId, associationId) {
100
+ async revokeAssociation(input) {
95
101
  await this.prepare(`UPDATE external_auth_associations SET status = 'revoked'
96
- WHERE ${SCOPE} AND wallet_id = ?5 AND association_id = ?6`, [walletId, associationId]).run();
102
+ WHERE ${SCOPE} AND wallet_id = ?5 AND association_id = ?6
103
+ AND EXISTS (SELECT 1 FROM wallet_authorities WHERE ${SCOPE}
104
+ AND wallet_id = ?5 AND authority_id = ?7 AND revocation_epoch = ?8 AND lifecycle_state = 'active')`, [
105
+ input.walletId,
106
+ input.associationId,
107
+ input.owner.authorityId,
108
+ input.owner.revocationEpoch
109
+ ]).run();
110
+ }
111
+ async prepareUnlink(input) {
112
+ return d1ChangedRows(await this.prepare(`INSERT INTO external_auth_attempts
113
+ (namespace, org_id, project_id, env_id, attempt_id, wallet_id, origin, binding_hash,
114
+ session_token_hash, expires_at_ms, status, purpose, association_id, identity_subject, code_hash, trust_revision)
115
+ SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 'verified', 'unlink', ?11, provider_subject, ?12, ?13
116
+ FROM external_auth_associations WHERE ${SCOPE} AND wallet_id = ?6 AND association_id = ?11`, [
117
+ input.attemptId,
118
+ input.walletId,
119
+ input.originBinding,
120
+ input.bindingHash,
121
+ input.sessionTokenHash,
122
+ input.expiresAtMs,
123
+ input.associationId,
124
+ input.codeHash,
125
+ input.trustRevision
126
+ ]).run()) === 1;
127
+ }
128
+ async readOwnerAttempt(input) {
129
+ const row = await this.prepare(`SELECT purpose, status, identity_subject, association_id, expires_at_ms, owner_challenge_expires_at_ms
130
+ FROM external_auth_attempts WHERE ${SCOPE} AND wallet_id = ?5 AND attempt_id = ?6
131
+ AND origin = ?7 AND binding_hash = ?8 AND session_token_hash = ?9 AND code_hash = ?10
132
+ AND trust_revision = ?11 AND expires_at_ms > ?12 AND status IN ('verified', 'consumed')
133
+ AND purpose IN ('associate', 'unlink')`, [
134
+ input.walletId,
135
+ input.attemptId,
136
+ input.originBinding,
137
+ input.bindingHash,
138
+ input.sessionTokenHash,
139
+ input.codeHash,
140
+ input.trustRevision,
141
+ input.nowMs
142
+ ]).first();
143
+ if (!row) return null;
144
+ if (row.status !== "verified" && row.status !== "consumed") throw new Error("Invalid owner attempt state");
145
+ const status = row.status;
146
+ const identity = parseStoredExternalIdentity(requireCanonicalString(row.identity_subject, "identity_subject"));
147
+ const expiresAtMs = parseUnixMs(row.expires_at_ms, "attempt expiry");
148
+ const challenge = row.owner_challenge_expires_at_ms === null ? { kind: "unprepared" } : {
149
+ kind: "ready",
150
+ expiresAtMs: parseUnixMs(row.owner_challenge_expires_at_ms, "owner challenge expiry")
151
+ };
152
+ switch (row.purpose) {
153
+ case "associate": return {
154
+ purpose: "associate",
155
+ walletId: input.walletId,
156
+ attemptId: input.attemptId,
157
+ identity,
158
+ originBinding: input.originBinding,
159
+ trustRevision: input.trustRevision,
160
+ expiresAtMs,
161
+ status,
162
+ challenge
163
+ };
164
+ case "unlink": return {
165
+ purpose: "unlink",
166
+ walletId: input.walletId,
167
+ attemptId: input.attemptId,
168
+ identity,
169
+ originBinding: input.originBinding,
170
+ trustRevision: input.trustRevision,
171
+ expiresAtMs,
172
+ status,
173
+ challenge,
174
+ associationId: requireCanonicalString(row.association_id, "association_id")
175
+ };
176
+ default: throw new Error("Invalid owner attempt purpose");
177
+ }
178
+ }
179
+ async prepareOwnerChallenge(input) {
180
+ await this.prepare(`UPDATE external_auth_attempts
181
+ SET owner_challenge_expires_at_ms = COALESCE(owner_challenge_expires_at_ms, MIN(expires_at_ms, ?13))
182
+ WHERE ${SCOPE} AND wallet_id = ?5 AND attempt_id = ?6 AND origin = ?7
183
+ AND binding_hash = ?8 AND session_token_hash = ?9 AND code_hash = ?10
184
+ AND trust_revision = ?11 AND expires_at_ms > ?12 AND status = 'verified'
185
+ AND purpose IN ('associate', 'unlink')`, [
186
+ input.walletId,
187
+ input.attemptId,
188
+ input.originBinding,
189
+ input.bindingHash,
190
+ input.sessionTokenHash,
191
+ input.codeHash,
192
+ input.trustRevision,
193
+ input.nowMs,
194
+ input.nowMs + 6e4
195
+ ]).run();
196
+ return this.readOwnerAttempt(input);
197
+ }
198
+ async consumeOwnerAttempt(input, owner) {
199
+ return d1ChangedRows(await this.prepare(`UPDATE external_auth_attempts SET status = 'consumed'
200
+ WHERE ${SCOPE} AND wallet_id = ?5 AND attempt_id = ?6 AND origin = ?7
201
+ AND binding_hash = ?8 AND session_token_hash = ?9 AND code_hash = ?10
202
+ AND trust_revision = ?11 AND expires_at_ms > ?12 AND owner_challenge_expires_at_ms > ?12
203
+ AND status = 'verified' AND purpose IN ('associate', 'unlink')
204
+ AND EXISTS (SELECT 1 FROM wallet_authorities WHERE ${SCOPE}
205
+ AND wallet_id = ?5 AND authority_id = ?13 AND revocation_epoch = ?14 AND lifecycle_state = 'active')`, [
206
+ input.walletId,
207
+ input.attemptId,
208
+ input.originBinding,
209
+ input.bindingHash,
210
+ input.sessionTokenHash,
211
+ input.codeHash,
212
+ input.trustRevision,
213
+ input.nowMs,
214
+ owner.authorityId,
215
+ owner.revocationEpoch
216
+ ]).run()) === 1;
97
217
  }
98
218
  async prepareAttempt(input) {
99
219
  const nowMs = Date.now();
@@ -105,16 +225,31 @@ var ExternalAuthStore = class {
105
225
  )`).bind(nowMs)]);
106
226
  await this.prepare(`INSERT INTO external_auth_attempts
107
227
  (namespace, org_id, project_id, env_id, attempt_id, wallet_id, origin,
108
- binding_hash, session_token_hash, expires_at_ms, status)
109
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 'prepared')`, [
228
+ binding_hash, session_token_hash, expires_at_ms, status, purpose)
229
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 'prepared', ?11)`, [
110
230
  input.attemptId,
111
231
  input.walletId,
112
232
  input.originBinding,
113
233
  input.bindingHash,
114
234
  input.sessionTokenHash,
115
- input.expiresAtMs
235
+ input.expiresAtMs,
236
+ input.purpose
116
237
  ]).run();
117
238
  }
239
+ async readPreparedPurpose(walletId, attemptId, nowMs) {
240
+ const row = await this.prepare(`SELECT purpose FROM external_auth_attempts WHERE ${SCOPE} AND wallet_id = ?5
241
+ AND attempt_id = ?6 AND status = 'prepared' AND expires_at_ms > ?7`, [
242
+ walletId,
243
+ attemptId,
244
+ nowMs
245
+ ]).first();
246
+ if (!row) return null;
247
+ switch (row.purpose) {
248
+ case "authenticate":
249
+ case "associate": return row.purpose;
250
+ default: return null;
251
+ }
252
+ }
118
253
  async verifyAttempt(input) {
119
254
  return d1ChangedRows(await this.prepare(`UPDATE external_auth_attempts SET status = 'verified',
120
255
  association_id = (SELECT a.association_id FROM external_auth_associations a
@@ -125,31 +260,32 @@ var ExternalAuthStore = class {
125
260
  AND a.wallet_id = ?6 AND a.provider_subject = ?7 AND a.status = 'active'
126
261
  AND w.lifecycle_state = 'active' AND w.revocation_epoch = a.authority_epoch
127
262
  AND ${ACTIVE_HOME}),
128
- trust_revision = ?8, code_hash = ?9, session_expires_at_ms = ?11
263
+ trust_revision = ?8, code_hash = ?9, session_expires_at_ms = ?11, identity_subject = ?7
129
264
  WHERE ${SCOPE} AND attempt_id = ?5 AND wallet_id = ?6
130
- AND status = 'prepared' AND expires_at_ms > ?10
131
- AND EXISTS (SELECT 1 FROM external_auth_associations a
265
+ AND status = 'prepared' AND expires_at_ms > ?10 AND purpose = ?12
266
+ AND (purpose = 'associate' OR EXISTS (SELECT 1 FROM external_auth_associations a
132
267
  JOIN wallet_authorities w ON w.namespace = a.namespace AND w.org_id = a.org_id
133
268
  AND w.project_id = a.project_id AND w.env_id = a.env_id
134
269
  AND w.authority_id = a.authority_id AND w.wallet_id = a.wallet_id
135
270
  WHERE a.namespace = ?1 AND a.org_id = ?2 AND a.project_id = ?3 AND a.env_id = ?4
136
271
  AND a.wallet_id = ?6 AND a.provider_subject = ?7 AND a.status = 'active'
137
272
  AND w.lifecycle_state = 'active' AND w.revocation_epoch = a.authority_epoch
138
- AND ${ACTIVE_HOME})`, [
273
+ AND ${ACTIVE_HOME}))`, [
139
274
  input.attemptId,
140
275
  input.walletId,
141
- input.subject,
276
+ serializeExternalIdentity(input.identity),
142
277
  input.trust.revision,
143
278
  input.codeHash,
144
279
  input.nowMs,
145
- input.sessionExpiresAtMs
280
+ input.sessionExpiresAtMs,
281
+ input.purpose
146
282
  ]).run()) === 1;
147
283
  }
148
284
  async redeem(input) {
149
285
  await this.prepare(`UPDATE external_auth_attempts SET status = 'consumed'
150
286
  WHERE ${SCOPE} AND attempt_id = ?5 AND wallet_id = ?6 AND origin = ?7
151
287
  AND binding_hash = ?8 AND session_token_hash = ?9 AND code_hash = ?10
152
- AND status = 'verified' AND expires_at_ms > ?11 AND session_expires_at_ms > ?11
288
+ AND purpose = 'authenticate' AND status = 'verified' AND expires_at_ms > ?11 AND session_expires_at_ms > ?11
153
289
  AND trust_revision = ?12 AND ${LIVE_ASSOCIATION}`, [
154
290
  input.attemptId,
155
291
  input.walletId,
@@ -160,10 +296,10 @@ var ExternalAuthStore = class {
160
296
  input.nowMs,
161
297
  input.trustRevision
162
298
  ]).run();
163
- const row = await this.prepare(`SELECT attempt_id, association_id, wallet_id, session_expires_at_ms
299
+ const row = await this.prepare(`SELECT ${AUTHENTICATION_COLUMNS}
164
300
  FROM external_auth_attempts WHERE ${SCOPE} AND attempt_id = ?5 AND wallet_id = ?6
165
301
  AND origin = ?7 AND binding_hash = ?8 AND session_token_hash = ?9 AND code_hash = ?10
166
- AND status = 'consumed' AND session_expires_at_ms > ?11 AND trust_revision = ?12 AND ${LIVE_ASSOCIATION}`, [
302
+ AND purpose = 'authenticate' AND status = 'consumed' AND session_expires_at_ms > ?11 AND trust_revision = ?12 AND ${LIVE_ASSOCIATION}`, [
167
303
  input.attemptId,
168
304
  input.walletId,
169
305
  input.originBinding,
@@ -176,9 +312,9 @@ var ExternalAuthStore = class {
176
312
  return row ? this.authentication(row) : null;
177
313
  }
178
314
  async readSession(input) {
179
- const row = await this.prepare(`SELECT attempt_id, association_id, wallet_id, session_expires_at_ms
315
+ const row = await this.prepare(`SELECT ${AUTHENTICATION_COLUMNS}
180
316
  FROM external_auth_attempts WHERE ${SCOPE} AND session_token_hash = ?5 AND origin = ?6
181
- AND status = 'consumed' AND session_expires_at_ms > ?7
317
+ AND purpose = 'authenticate' AND status = 'consumed' AND session_expires_at_ms > ?7
182
318
  AND trust_revision = ?8 AND ${LIVE_ASSOCIATION}`, [
183
319
  input.tokenHash,
184
320
  input.originBinding,
@@ -205,7 +341,7 @@ var ExternalAuthStore = class {
205
341
  sessionId: row.attempt_id,
206
342
  associationId: row.association_id,
207
343
  environmentId: this.scope.envId,
208
- provider: "google",
344
+ provider: parseStoredExternalIdentity(requireCanonicalString(row.provider_subject, "provider_subject")).provider,
209
345
  permission: "wallet_settings_read",
210
346
  expiresAtMs: parseUnixMs(row.session_expires_at_ms, "session expiry")
211
347
  });