@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
@@ -0,0 +1,306 @@
1
+ import { parseEmailOtpChallengeId, parseOrgId, parseProviderSubject, parseWalletId, parseWebAuthnCredentialIdB64u } from "../../../shared-ts/src/utils/domainIds.js";
2
+ import { normalizeOptionalTrimmedString } from "../../../shared-ts/src/utils/normalize.js";
3
+ import { alphabetizeStringify } from "../../../shared-ts/src/utils/digests.js";
4
+ import { sha256Utf8DigestB64u } from "../../../shared-ts/src/utils/canonicalPrimitives.js";
5
+ import { requireParsed } from "../../../shared-ts/src/passkey-custody/primitives.js";
6
+ import { computeWalletOwnerAdminChallengeDigest } from "../../../shared-ts/src/authorization/walletOwnerOperation.js";
7
+ import { buildVaultOperationRef, parseAuthFactorId, parseAuthorizationAuditEventId, parseAuthorizationEvidenceId, parseAuthorizationEvidenceSetId, parseAuthorizedOperationId, parseCapabilityId, parseCapabilityOperationId, parsePrincipalId, parseTenantId } from "../../../shared-ts/src/authorization/capabilityKinds.js";
8
+ import { buildDelegatedWalletAuthorityV1, buildFullOwnerPermissionsV1, sameDelegatedWalletAuthorityV1 } from "../../../shared-ts/src/authorization/delegatedAuthority.js";
9
+ import { buildCapabilityOperationEnvelope, computeCapabilityOperationFingerprintDigest } from "../../../shared-ts/src/authorization/operationFingerprint.js";
10
+ import { buildPasskeyWalletAuthAuthority, walletAuthAuthorityRef } from "../../../shared-ts/src/utils/walletAuthAuthority.js";
11
+ import { parseSessionOrigin, parseVerifiedOwnerProofId } from "../../../authorization/domain.js";
12
+ import { buildVerifiedWalletOperationEmailOtpFactorResult, buildVerifiedWalletOperationPasskeyFactorResult } from "../../../authorization/factorEvidence.js";
13
+ import { EMAIL_OTP_CHANNEL, WALLET_EMAIL_OTP_UNLOCK_OPERATION } from "../../../shared-ts/src/utils/emailOtpDomain.js";
14
+ import { parseWebAuthnAuthenticationCredential } from "../../auth/webAuthnCredentialCodecs.js";
15
+ import { emailOtpStatusCode, hashEmailOtpOperationBinding } from "../../domains/emailOtp/emailOtpSessionRouteHelpers.js";
16
+ import { toFetchRouteResponse } from "../../framework/routeResponses.js";
17
+ import { parseCustodyOwnerProof } from "./routes/walletCustodyRequestBoundary.js";
18
+ //#region src/router/transport/fetch/walletOwnerOperationAuthorization.ts
19
+ const CUSTODY_OPERATION_CAPABILITY_ID = "wallet-custody-admin";
20
+ const CUSTODY_PROOF_TTL_MS = 6e4;
21
+ function walletOwnerOperationAuthorizationId(operation, challenge) {
22
+ return requireParsed(parseAuthorizedOperationId(`wallet-custody:authorized:wallet-custody:${operation}:${challenge}`), "owner operation");
23
+ }
24
+ function hasFullOwnerAuthority(authority) {
25
+ return sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }));
26
+ }
27
+ async function custodyDigest(domain, value) {
28
+ return sha256Utf8DigestB64u(`${domain}|${alphabetizeStringify(value)}`);
29
+ }
30
+ async function buildWalletCustodyOperation(input) {
31
+ const tenantId = requireParsed(parseTenantId(input.tenantId), "authorization");
32
+ const principalId = requireParsed(parsePrincipalId(input.principalId), "authorization");
33
+ const capabilityId = requireParsed(parseCapabilityId(CUSTODY_OPERATION_CAPABILITY_ID), "authorization");
34
+ const operationRef = buildVaultOperationRef("vault.reveal");
35
+ const laneDigest = await custodyDigest("seams:wallet-custody:lane:v1", {
36
+ capabilityId,
37
+ operation: input.operation,
38
+ operationRef
39
+ });
40
+ const challengeDigest = await computeWalletOwnerAdminChallengeDigest({
41
+ walletId: input.walletId,
42
+ operation: input.operation,
43
+ payload: input.payload,
44
+ requestOrigin: input.requestOrigin
45
+ });
46
+ const intentDigest = challengeDigest;
47
+ const displayDigest = await custodyDigest("seams:wallet-custody:display:v1", {
48
+ walletId: input.walletId,
49
+ operation: input.operation
50
+ });
51
+ return {
52
+ operation: buildCapabilityOperationEnvelope({
53
+ tenantId,
54
+ principalId,
55
+ capabilityId,
56
+ operationId: requireParsed(parseCapabilityOperationId(`wallet-custody:${input.operation}:${intentDigest}`), "authorization"),
57
+ operation: operationRef,
58
+ digests: {
59
+ laneDigest,
60
+ intentDigest,
61
+ displayDigest
62
+ }
63
+ }),
64
+ challengeDigest
65
+ };
66
+ }
67
+ async function authorizeWalletOwnerOperation(input) {
68
+ const requestOrigin = normalizeOptionalTrimmedString(input.ctx.request.headers.get("origin"));
69
+ if (!requestOrigin) return toCustodyAuthorizationFailure(400, "invalid_origin", "request Origin is required");
70
+ let origin;
71
+ let proof;
72
+ let walletId;
73
+ try {
74
+ origin = parseSessionOrigin(requestOrigin);
75
+ walletId = requireParsed(parseWalletId(input.walletId), "authorization");
76
+ proof = parseCustodyOwnerProof(input.factorProof);
77
+ } catch (error) {
78
+ return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "owner proof is invalid");
79
+ }
80
+ if (proof.kind === "passkey" && proof.walletId !== input.walletId) return toCustodyAuthorizationFailure(403, "scope_mismatch", "owner proof wallet differs");
81
+ let principalId;
82
+ let factor;
83
+ let ownerAuthorityRef;
84
+ let ownerAuthority;
85
+ let operation;
86
+ let challengeDigest;
87
+ try {
88
+ principalId = proof.kind === "passkey" ? input.walletId : proof.provider_subject_id;
89
+ const built = await buildWalletCustodyOperation({
90
+ tenantId: input.ctx.service.authorizedOperations.tenantId,
91
+ principalId,
92
+ walletId: input.walletId,
93
+ operation: input.operation,
94
+ payload: input.payload,
95
+ requestOrigin
96
+ });
97
+ operation = built.operation;
98
+ challengeDigest = built.challengeDigest;
99
+ } catch (error) {
100
+ return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "owner proof binding is invalid");
101
+ }
102
+ const operationFingerprintDigest = await computeCapabilityOperationFingerprintDigest(operation);
103
+ const nowMs = Date.now();
104
+ if (proof.challenge_digest !== String(challengeDigest)) return toCustodyAuthorizationFailure(403, "scope_mismatch", "factor proof challenge does not match the requested operation");
105
+ if (proof.kind === "passkey") {
106
+ let authority;
107
+ try {
108
+ authority = buildPasskeyWalletAuthAuthority({
109
+ walletId,
110
+ rpId: proof.rpId,
111
+ credentialIdB64u: proof.credentialIdB64u
112
+ });
113
+ } catch (error) {
114
+ return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "passkey authority is invalid");
115
+ }
116
+ const active = await input.ctx.service.walletAuthMethods.resolveActivePasskeyAuthorityForVerifiedCredential({
117
+ walletId,
118
+ rpId: authority.verifier.rpId,
119
+ credentialIdB64u: authority.factor.credentialIdB64u
120
+ });
121
+ if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
122
+ if (!hasFullOwnerAuthority(active.walletAuthority)) return toCustodyAuthorizationFailure(403, "forbidden", "Full owner authority is required");
123
+ ownerAuthority = active.walletAuthority;
124
+ const credential = parseWebAuthnAuthenticationCredential(proof.webauthn_authentication);
125
+ if (!credential) return toCustodyAuthorizationFailure(400, "invalid_owner_proof", "passkey assertion is invalid");
126
+ if (String(credential.rawId || credential.id).trim() !== authority.factor.credentialIdB64u) return toCustodyAuthorizationFailure(401, "unauthorized", "passkey credential changed");
127
+ const verified = await input.ctx.service.webAuthn.verifyWebAuthnAuthenticationLite({
128
+ userId: input.walletId,
129
+ rpId: authority.verifier.rpId,
130
+ expectedChallenge: challengeDigest,
131
+ expected_origin: requestOrigin,
132
+ webauthn_authentication: credential
133
+ });
134
+ if (!verified.success || !verified.verified) return toCustodyAuthorizationFailure(401, verified.code || "not_verified", verified.message || "WebAuthn authentication verification failed");
135
+ ownerAuthorityRef = await walletAuthAuthorityRef({ authority: active.authority });
136
+ factor = buildVerifiedWalletOperationPasskeyFactorResult({
137
+ tenantId: operation.tenantId,
138
+ principalId: operation.principalId,
139
+ walletId,
140
+ authorityRef: ownerAuthorityRef,
141
+ requestOrigin: origin,
142
+ audience: origin,
143
+ factorId: requireParsed(parseAuthFactorId(`passkey:${authority.factor.credentialIdB64u}`), "authorization"),
144
+ credentialIdB64u: requireParsed(parseWebAuthnCredentialIdB64u(authority.factor.credentialIdB64u), "authorization"),
145
+ assertionDigest: await custodyDigest("seams:wallet-custody:assertion:v1", { credential }),
146
+ operation,
147
+ verifiedAtMs: nowMs,
148
+ expiresAtMs: nowMs + CUSTODY_PROOF_TTL_MS
149
+ });
150
+ } else {
151
+ const providerSubject = requireParsed(parseProviderSubject(proof.provider_subject_id), "authorization");
152
+ const orgId = requireParsed(parseOrgId(input.ctx.service.authorizedOperations.tenantId), "authorization");
153
+ const active = await input.ctx.service.walletAuthMethods.resolveActiveEmailOtpAuthorityForVerifiedSubject({
154
+ walletId: input.walletId,
155
+ providerUserId: proof.provider_subject_id
156
+ });
157
+ if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
158
+ const activeAuthorityRef = await walletAuthAuthorityRef({ authority: active.authority });
159
+ ownerAuthorityRef = activeAuthorityRef;
160
+ ownerAuthority = active.walletAuthority;
161
+ const ownerProofBindingDigest = await hashEmailOtpOperationBinding({
162
+ walletId: input.walletId,
163
+ providerUserId: proof.provider_subject_id,
164
+ orgId,
165
+ operation: `wallet_custody:${input.operation}`,
166
+ requestOrigin,
167
+ audience: requestOrigin,
168
+ authorityRef: activeAuthorityRef,
169
+ operationFingerprintDigest: challengeDigest
170
+ });
171
+ const verified = await input.ctx.service.emailOtp.verifyEmailOtpChallenge({
172
+ userId: proof.provider_subject_id,
173
+ walletId: input.walletId,
174
+ orgId,
175
+ challengeId: proof.challenge_id,
176
+ otpCode: proof.otp_code,
177
+ otpChannel: EMAIL_OTP_CHANNEL,
178
+ ownerProofBindingDigest,
179
+ operation: WALLET_EMAIL_OTP_UNLOCK_OPERATION
180
+ });
181
+ if (!verified.ok) return toCustodyAuthorizationFailure(verified.code === "invalid_body" ? 400 : 401, verified.code, verified.message);
182
+ const consumed = await input.ctx.service.emailOtp.consumeEmailOtpGrant({
183
+ subject: {
184
+ kind: "provider_identity",
185
+ orgId,
186
+ providerSubject,
187
+ walletId
188
+ },
189
+ loginGrant: verified.loginGrant,
190
+ otpChannel: EMAIL_OTP_CHANNEL
191
+ });
192
+ if (!consumed.ok) return toCustodyAuthorizationFailure(emailOtpStatusCode(consumed.code), consumed.code, consumed.message);
193
+ factor = buildVerifiedWalletOperationEmailOtpFactorResult({
194
+ tenantId: operation.tenantId,
195
+ principalId: operation.principalId,
196
+ walletId,
197
+ authorityRef: activeAuthorityRef,
198
+ requestOrigin: origin,
199
+ audience: origin,
200
+ factorId: requireParsed(parseAuthFactorId(`email_otp:${active.authority.factor.provider}:${proof.provider_subject_id}`), "authorization"),
201
+ operation,
202
+ challengeId: requireParsed(parseEmailOtpChallengeId(consumed.challengeId), "authorization"),
203
+ verificationReceiptDigest: await custodyDigest("seams:wallet-custody:otp-receipt:v1", {
204
+ challengeId: consumed.challengeId,
205
+ operationFingerprintDigest
206
+ }),
207
+ verifiedAtMs: nowMs,
208
+ expiresAtMs: Math.min(nowMs + CUSTODY_PROOF_TTL_MS, verified.grantExpiresAtMs)
209
+ });
210
+ }
211
+ const ownerProof = await input.ctx.service.authorizedOperations.buildVerifiedOwnerProof({
212
+ purpose: "operation",
213
+ proofId: parseVerifiedOwnerProofId(`wallet-custody:${operation.operationId}`),
214
+ factor
215
+ });
216
+ if (ownerProof.purpose !== "operation" || ownerProof.operation.operationFingerprintDigest !== operationFingerprintDigest) return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "owner proof binding is invalid");
217
+ const existing = await input.ctx.service.authorizedOperations.readAuthorizedOperation({
218
+ tenantId: operation.tenantId,
219
+ operationFingerprintDigest
220
+ });
221
+ if (existing) {
222
+ if (existing.authorization.kind !== "verified_step_up" || existing.quota.kind !== "quota_neutral") return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "Owner operation authorization differs");
223
+ return {
224
+ ok: false,
225
+ response: authorizedOperationReplay(existing)
226
+ };
227
+ }
228
+ const evidenceSet = await input.ctx.service.authorizedOperations.recordVerifiedWalletOperationFactorEvidenceSet({
229
+ operation,
230
+ evidenceId: requireParsed(parseAuthorizationEvidenceId(`wallet-custody:evidence:${operation.operationId}`), "authorization"),
231
+ evidenceSetId: requireParsed(parseAuthorizationEvidenceSetId(`wallet-custody:evidence-set:${operation.operationId}`), "authorization"),
232
+ factor
233
+ });
234
+ const admission = await input.ctx.service.authorizedOperations.admitAuthorizedOperation({ operation: {
235
+ tenantId: operation.tenantId,
236
+ authorizedOperationId: walletOwnerOperationAuthorizationId(input.operation, challengeDigest),
237
+ auditEventId: requireParsed(parseAuthorizationAuditEventId(`wallet-custody:audit:${operation.operationId}`), "authorization"),
238
+ operation,
239
+ authorization: {
240
+ kind: "verified_step_up",
241
+ evidenceSetDigest: evidenceSet.evidenceSetDigest
242
+ },
243
+ quota: { kind: "quota_neutral" },
244
+ claimedAtMs: nowMs
245
+ } });
246
+ switch (admission.kind) {
247
+ case "claimed": return {
248
+ ok: true,
249
+ operation: admission.operation,
250
+ ownerAuthority
251
+ };
252
+ case "replayed":
253
+ case "operation_in_progress": return {
254
+ ok: false,
255
+ response: authorizedOperationReplay(admission.operation)
256
+ };
257
+ case "authorization_grant_rejected":
258
+ case "verified_step_up_rejected":
259
+ case "wallet_session_quota_exhausted":
260
+ case "material_mismatch": return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "owner proof was rejected");
261
+ }
262
+ }
263
+ function toCustodyAuthorizationFailure(status, code, message) {
264
+ return {
265
+ ok: false,
266
+ response: routeErrorResponse(status, code, message)
267
+ };
268
+ }
269
+ function routeErrorResponse(status, code, message) {
270
+ return toFetchRouteResponse({
271
+ status,
272
+ body: {
273
+ ok: false,
274
+ code,
275
+ message
276
+ }
277
+ });
278
+ }
279
+ function authorizedOperationReplay(operation) {
280
+ if (operation.lifecycle !== "completed") return routeErrorResponse(409, "recovery_in_progress", "wallet recovery is still in progress");
281
+ return new Response(operation.response.bodyText, {
282
+ status: operation.response.status,
283
+ headers: { "content-type": operation.response.contentType }
284
+ });
285
+ }
286
+ async function completeWalletOwnerOperation(ctx, operation, status, body, result = "succeeded") {
287
+ const bodyText = JSON.stringify(body);
288
+ await ctx.service.authorizedOperations.completeAuthorizedOperation({
289
+ operation,
290
+ result,
291
+ response: {
292
+ status,
293
+ contentType: "application/json",
294
+ bodyText
295
+ },
296
+ completedAtMs: Date.now()
297
+ });
298
+ return toFetchRouteResponse({
299
+ status,
300
+ body
301
+ });
302
+ }
303
+ //#endregion
304
+ export { authorizeWalletOwnerOperation, completeWalletOwnerOperation, walletOwnerOperationAuthorizationId };
305
+
306
+ //# sourceMappingURL=walletOwnerOperationAuthorization.js.map
@@ -1,33 +1,106 @@
1
1
  import { parseWalletId } from "../utils/domainIds.js";
2
2
  import { requireCanonicalString, requireRecord } from "../utils/validation.js";
3
- import { parseUnixMs } from "../passkey-custody/primitives.js";
3
+ import { parseEd25519PublicKeyB64u, parseUnixMs } from "../passkey-custody/primitives.js";
4
4
  //#region ../shared-ts/src/auth/externalAuth.ts
5
5
  const EXTERNAL_AUTH_PATH = "/wallet/external-auth";
6
+ function parseExternalAuthProvider(raw) {
7
+ const value = requireRecord(raw, "external authentication provider");
8
+ switch (value.kind) {
9
+ case "disabled": return { kind: "disabled" };
10
+ case "google": return {
11
+ kind: "google",
12
+ clientId: requireCanonicalString(value.clientId, "clientId")
13
+ };
14
+ case "application": {
15
+ const issuer = requireCanonicalString(value.issuer, "issuer");
16
+ const url = new URL(issuer);
17
+ if (url.protocol !== "https:" || url.origin !== issuer) throw new Error("Application issuer must be a canonical HTTPS origin");
18
+ return {
19
+ kind: "application",
20
+ issuer,
21
+ publicKeyB64u: parseEd25519PublicKeyB64u(value.publicKeyB64u, "publicKeyB64u")
22
+ };
23
+ }
24
+ default: throw new Error("Unsupported external authentication provider");
25
+ }
26
+ }
27
+ function externalAuthConfiguration(provider, revision) {
28
+ switch (provider.kind) {
29
+ case "disabled": return {
30
+ kind: "disabled",
31
+ revision
32
+ };
33
+ case "google": return {
34
+ kind: "google",
35
+ revision,
36
+ clientId: provider.clientId
37
+ };
38
+ case "application": return {
39
+ kind: "application",
40
+ revision,
41
+ issuer: provider.issuer,
42
+ publicKeyB64u: provider.publicKeyB64u
43
+ };
44
+ default: return assertNever(provider);
45
+ }
46
+ }
6
47
  function parseExternalAuthConfiguration(raw) {
7
48
  const value = requireRecord(raw, "external authentication configuration");
8
49
  if (typeof value.revision !== "number" || !Number.isSafeInteger(value.revision) || value.revision < 0) throw new Error("Invalid external authentication revision");
9
- if (value.kind === "disabled") return {
10
- kind: "disabled",
11
- revision: value.revision
12
- };
13
- if (value.kind === "google") return {
14
- kind: "google",
15
- revision: value.revision,
16
- clientId: requireCanonicalString(value.clientId, "clientId")
17
- };
18
- throw new Error("Unsupported external authentication configuration");
50
+ return externalAuthConfiguration(parseExternalAuthProvider(value), value.revision);
51
+ }
52
+ function serializeExternalIdentity(identity) {
53
+ return JSON.stringify([
54
+ identity.provider,
55
+ identity.issuer,
56
+ identity.subject
57
+ ]);
58
+ }
59
+ function parseStoredExternalIdentity(serialized) {
60
+ const raw = JSON.parse(serialized);
61
+ if (!Array.isArray(raw) || raw.length !== 3) throw new Error("Invalid external identity");
62
+ const [provider, issuer, subject] = raw;
63
+ return parseExternalIdentity({
64
+ provider,
65
+ issuer,
66
+ subject
67
+ });
68
+ }
69
+ function parseExternalIdentity(raw) {
70
+ const value = requireRecord(raw, "external identity");
71
+ const provider = value.provider;
72
+ const issuer = requireCanonicalString(value.issuer, "issuer");
73
+ const subject = requireCanonicalString(value.subject, "subject");
74
+ switch (provider) {
75
+ case "google":
76
+ if (issuer !== "https://accounts.google.com") throw new Error("Invalid Google issuer");
77
+ return {
78
+ provider,
79
+ issuer,
80
+ subject
81
+ };
82
+ case "application": return {
83
+ provider,
84
+ issuer,
85
+ subject
86
+ };
87
+ default: throw new Error("Invalid external identity provider");
88
+ }
89
+ }
90
+ function assertNever(value) {
91
+ throw new Error(`Unsupported external authentication provider: ${String(value)}`);
19
92
  }
20
93
  function parseExternalWalletAuthentication(value) {
21
94
  const record = requireRecord(value, "external authentication");
22
95
  const walletId = parseWalletId(record.walletId);
23
- if (!walletId.ok || record.kind !== "external_authenticated" || record.provider !== "google" || record.permission !== "wallet_settings_read" || "authMethod" in record) throw new Error("External authentication is invalid");
96
+ if (!walletId.ok || record.kind !== "external_authenticated" || record.provider !== "google" && record.provider !== "application" || record.permission !== "wallet_settings_read" || "authMethod" in record) throw new Error("External authentication is invalid");
24
97
  return {
25
98
  kind: "external_authenticated",
26
99
  walletId: walletId.value,
27
100
  sessionId: requireCanonicalString(record.sessionId, "sessionId"),
28
101
  associationId: requireCanonicalString(record.associationId, "associationId"),
29
102
  environmentId: requireCanonicalString(record.environmentId, "environmentId"),
30
- provider: "google",
103
+ provider: record.provider,
31
104
  permission: "wallet_settings_read",
32
105
  expiresAtMs: parseUnixMs(record.expiresAtMs, "expiresAtMs")
33
106
  };
@@ -40,6 +113,6 @@ function externalAuthFailure(code) {
40
113
  };
41
114
  }
42
115
  //#endregion
43
- export { EXTERNAL_AUTH_PATH, externalAuthFailure, parseExternalAuthConfiguration, parseExternalWalletAuthentication };
116
+ export { EXTERNAL_AUTH_PATH, externalAuthFailure, parseExternalAuthConfiguration, parseExternalAuthProvider, parseExternalWalletAuthentication, parseStoredExternalIdentity, serializeExternalIdentity };
44
117
 
45
118
  //# sourceMappingURL=externalAuth.js.map
@@ -1,6 +1,6 @@
1
+ import "./walletOwnerOperation.js";
1
2
  import "./capabilityKinds.js";
2
3
  import "./delegatedAuthority.js";
3
4
  import "./operationFingerprint.js";
4
5
  import "./walletAuthority.js";
5
- import "./walletCustodyOperation.js";
6
6
  import "./walletSessionIssuance.js";
@@ -1,8 +1,8 @@
1
1
  import { alphabetizeStringify } from "../utils/digests.js";
2
2
  import { sha256Utf8DigestB64u } from "../utils/canonicalPrimitives.js";
3
- //#region ../shared-ts/src/authorization/walletCustodyOperation.ts
3
+ //#region ../shared-ts/src/authorization/walletOwnerOperation.ts
4
4
  /** Public challenge digest for a single wallet-administration factor proof. */
5
- async function computeWalletCustodyAdminChallengeDigest(input) {
5
+ async function computeWalletOwnerAdminChallengeDigest(input) {
6
6
  return sha256Utf8DigestB64u(`seams:wallet-custody:challenge:v1|${alphabetizeStringify({
7
7
  walletId: input.walletId,
8
8
  operation: input.operation,
@@ -11,6 +11,6 @@ async function computeWalletCustodyAdminChallengeDigest(input) {
11
11
  })}`);
12
12
  }
13
13
  //#endregion
14
- export { computeWalletCustodyAdminChallengeDigest };
14
+ export { computeWalletOwnerAdminChallengeDigest };
15
15
 
16
- //# sourceMappingURL=walletCustodyOperation.js.map
16
+ //# sourceMappingURL=walletOwnerOperation.js.map
@@ -1,10 +1,10 @@
1
- import "./recoveryCodeReservation.js";
2
1
  import "../utils/emailOtpRecoveryKey.js";
2
+ import "./recoveryCodeLocator.js";
3
+ import "./recoveryCodeReservation.js";
3
4
  import "./recoveryKeyId.js";
5
+ import "./walletRecoveryEcdsaPossession.js";
4
6
  import "./backupAcknowledgement.js";
5
7
  import "./recoveryCodes.js";
6
8
  import "./walletRecoveryEnvelopeSet.js";
7
- import "./recoveryCodeLocator.js";
8
- import "./walletRecoveryEcdsaPossession.js";
9
9
  import "./walletRecoveryRotation.js";
10
10
  import "./walletRecoveryCommittedProjection.js";
@@ -1,16 +1,55 @@
1
1
  import { type WalletId } from '../utils/domainIds';
2
+ import { type Ed25519PublicKeyB64u } from '../passkey-custody/primitives';
2
3
  import type { ReadonlyExclusiveUnion } from '../utils/variant';
4
+ import { type DigestB64u } from '../utils/canonicalPrimitives';
3
5
  export declare const EXTERNAL_AUTH_PATH = "/wallet/external-auth";
4
- export type ExternalAuthConfiguration = ReadonlyExclusiveUnion<{
6
+ export type ExternalAuthProvider = ReadonlyExclusiveUnion<{
5
7
  kind: 'disabled';
6
- revision: number;
7
8
  } | {
8
9
  kind: 'google';
9
- revision: number;
10
10
  clientId: string;
11
+ } | {
12
+ kind: 'application';
13
+ issuer: string;
14
+ publicKeyB64u: Ed25519PublicKeyB64u;
15
+ }>;
16
+ export type ExternalAuthConfiguration = ExternalAuthProvider & Readonly<{
17
+ revision: number;
18
+ }>;
19
+ export type ExternalIdentity = ReadonlyExclusiveUnion<{
20
+ provider: 'google';
21
+ issuer: 'https://accounts.google.com';
22
+ subject: string;
23
+ } | {
24
+ provider: 'application';
25
+ issuer: string;
26
+ subject: string;
27
+ }>;
28
+ export type ExternalIdentityProof = ReadonlyExclusiveUnion<{
29
+ kind: 'google';
30
+ idToken: string;
31
+ } | {
32
+ kind: 'application';
33
+ assertion: string;
11
34
  }>;
35
+ export declare function parseExternalAuthProvider(raw: unknown): ExternalAuthProvider;
12
36
  export declare function parseExternalAuthConfiguration(raw: unknown): ExternalAuthConfiguration;
13
- type ExternalAuthFailureCode = 'invalid_request' | 'invalid_credential' | 'access_denied' | 'expired' | 'superseded' | 'unavailable' | 'rate_limited';
37
+ export declare function serializeExternalIdentity(identity: ExternalIdentity): string;
38
+ export declare function parseStoredExternalIdentity(serialized: string): ExternalIdentity;
39
+ export type ExternalOwnerChallenge = Readonly<{
40
+ walletId: WalletId;
41
+ attemptId: string;
42
+ identity: ExternalIdentity;
43
+ challengeB64u: DigestB64u;
44
+ expiresAtMs: number;
45
+ }> & ReadonlyExclusiveUnion<{
46
+ kind: 'associate';
47
+ } | {
48
+ kind: 'unlink';
49
+ associationId: string;
50
+ }>;
51
+ export declare function parseExternalOwnerChallenge(raw: unknown): ExternalOwnerChallenge;
52
+ type ExternalAuthFailureCode = 'invalid_request' | 'invalid_credential' | 'access_denied' | 'expired' | 'superseded' | 'unavailable' | 'rate_limited' | 'cancelled';
14
53
  export type ExternalAuthResult<T> = ReadonlyExclusiveUnion<{
15
54
  ok: true;
16
55
  value: T;
@@ -29,7 +68,9 @@ export type ExternalAuthenticateInput = ReadonlyExclusiveUnion<{
29
68
  attemptId: string;
30
69
  code: string;
31
70
  }>;
32
- export type ExternalAuthAttempt = Readonly<{
71
+ export type ExternalAuthPurpose = 'authenticate' | 'associate';
72
+ export type ExternalAuthAttempt<Purpose extends ExternalAuthPurpose = 'authenticate'> = Readonly<{
73
+ purpose: Purpose;
33
74
  attemptId: string;
34
75
  walletId: WalletId;
35
76
  expiresAtMs: number;
@@ -41,9 +82,11 @@ export type ExternalAuthCommand = ReadonlyExclusiveUnion<{
41
82
  action: 'prepare';
42
83
  walletId: string;
43
84
  } | {
44
- action: 'associate';
85
+ action: 'prepare_association';
45
86
  walletId: string;
46
- idToken: string;
87
+ } | {
88
+ action: 'associate';
89
+ input: ExternalAuthenticateInput;
47
90
  } | {
48
91
  action: 'unlink';
49
92
  walletId: string;
@@ -56,7 +99,7 @@ export type ExternalWalletAuthentication = Readonly<{
56
99
  sessionId: string;
57
100
  associationId: string;
58
101
  environmentId: string;
59
- provider: 'google';
102
+ provider: ExternalIdentity['provider'];
60
103
  permission: 'wallet_settings_read';
61
104
  expiresAtMs: number;
62
105
  authMethod?: never;
@@ -1,7 +1,7 @@
1
1
  export * from './capabilityKinds';
2
2
  export * from './delegatedAuthority';
3
3
  export * from './operationFingerprint';
4
- export * from './walletCustodyOperation';
4
+ export * from './walletOwnerOperation';
5
5
  export * from './walletAuthority';
6
6
  export * from './walletSessionIssuance';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { type DigestB64u } from '../utils/canonicalPrimitives';
2
+ export type WalletOwnerAdminOperation = 'external_identity_associate' | 'external_identity_unlink' | 'credentials_list' | 'credential_label' | 'recovery_rotate' | 'recovery_read' | 'recovery_status' | 'recovery_backup_acknowledge';
3
+ /** Public challenge digest for a single wallet-administration factor proof. */
4
+ export declare function computeWalletOwnerAdminChallengeDigest(input: {
5
+ readonly walletId: string;
6
+ readonly operation: WalletOwnerAdminOperation;
7
+ readonly payload: Record<string, unknown>;
8
+ readonly requestOrigin: string;
9
+ }): Promise<DigestB64u>;
10
+ //# sourceMappingURL=walletOwnerOperation.d.ts.map
@@ -72,6 +72,10 @@ export type RegistrationEstablishedSessionV2 = {
72
72
  readonly tokens: RegistrationEstablishedSessionProjectionTokensV2;
73
73
  };
74
74
  export type RegistrationEstablishedSessionResultV2 = {
75
+ readonly kind: 'owner_operations';
76
+ readonly walletId: WalletId;
77
+ readonly session?: never;
78
+ } | {
75
79
  readonly kind: 'issued';
76
80
  readonly session: RegistrationEstablishedSessionV2;
77
81
  } | {
@@ -18,7 +18,9 @@ export type RuntimePolicyScopeLike = {
18
18
  envId: string;
19
19
  signingRootVersion?: string;
20
20
  };
21
+ export type RegistrationCompletion = 'wallet_session' | 'owner_operations';
21
22
  export type RegistrationIntentV1 = {
23
+ completion: RegistrationCompletion;
22
24
  version: 'registration_intent_v1';
23
25
  walletId: WalletId;
24
26
  authMethod: RegistrationAuthMethodInput;
@@ -1,3 +1,3 @@
1
1
  export declare const WALLET_MANAGEMENT_PROTOCOL_HEADER = "X-Seams-Wallet-Protocol";
2
- export declare const WALLET_MANAGEMENT_PROTOCOL_VERSION = "4";
2
+ export declare const WALLET_MANAGEMENT_PROTOCOL_VERSION = "5";
3
3
  //# sourceMappingURL=walletManagementProtocol.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Backend-only application-session assertions for the external-auth exchange. */
2
+ export { createApplicationAuthAssertion } from './auth/applicationAuth';
3
+ //# sourceMappingURL=application-auth.d.ts.map
@@ -0,0 +1,29 @@
1
+ import type { ExternalAuthConfiguration, ExternalAuthPurpose, ExternalAuthResult, ExternalIdentity } from '../../../shared-ts/src/auth/externalAuth';
2
+ import type { Variant } from '../../../shared-ts/src/utils/variant';
3
+ type ApplicationTrust = Variant<ExternalAuthConfiguration, 'kind', 'application'>;
4
+ /** Called only by the application's authenticated backend with its enrolled issuer key. */
5
+ export declare function createApplicationAuthAssertion(input: {
6
+ readonly privateKey: CryptoKey;
7
+ readonly issuer: string;
8
+ readonly subject: string;
9
+ readonly environmentId: string;
10
+ readonly walletId: string;
11
+ readonly attemptId: string;
12
+ readonly nowMs: number;
13
+ readonly sessionExpiresAtMs: number;
14
+ readonly purpose: ExternalAuthPurpose;
15
+ }): Promise<string>;
16
+ export declare function verifyApplicationAuthAssertion(input: {
17
+ readonly trust: ApplicationTrust;
18
+ readonly assertion: string;
19
+ readonly environmentId: string;
20
+ readonly walletId: string;
21
+ readonly attemptId: string;
22
+ readonly nowMs: number;
23
+ readonly purpose: ExternalAuthPurpose;
24
+ }): Promise<ExternalAuthResult<{
25
+ identity: Variant<ExternalIdentity, 'provider', 'application'>;
26
+ expiresAtMs: number;
27
+ }>>;
28
+ export {};
29
+ //# sourceMappingURL=applicationAuth.d.ts.map