@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
@@ -12,12 +12,12 @@ import { ROUTER_AB_ED25519_YAO_EXPORT_ADMISSION_PATH_V1, ROUTER_AB_ED25519_YAO_E
12
12
  import { walletAuthAuthorityRef } from "../../../../shared-ts/src/utils/walletAuthAuthority.js";
13
13
  import { parseSessionOrigin, parseVerifiedOwnerProofId } from "../../../../authorization/domain.js";
14
14
  import { buildVerifiedWalletOperationEmailOtpFactorResult, buildVerifiedWalletOperationPasskeyFactorResult } from "../../../../authorization/factorEvidence.js";
15
+ import { WALLET_EMAIL_OTP_EXPORT_OPERATION } from "../../../../shared-ts/src/utils/emailOtpDomain.js";
16
+ import { parseWebAuthnAuthenticationCredential, webAuthnCredentialIdB64uFromCredential } from "../../../auth/webAuthnCredentialCodecs.js";
15
17
  import { normalizeCorsOrigin } from "../../../../core/SessionService.js";
16
18
  import { ROUTER_AB_TRACE_ID_HEADER_V1, createRouterAbTraceContextV1, parseRouterAbTraceContextV1 } from "../../../../shared-ts/src/utils/routerAbTraceContext.js";
17
19
  import { json, readJson } from "../../../framework/http.js";
18
20
  import { defineRoute } from "../../../framework/routeDefinitions.js";
19
- import { parseWebAuthnAuthenticationCredential, webAuthnCredentialIdB64uFromCredential } from "../../../auth/webAuthnCredentialCodecs.js";
20
- import { WALLET_EMAIL_OTP_EXPORT_OPERATION } from "../../../../shared-ts/src/utils/emailOtpDomain.js";
21
21
  import { createRouterApiModule } from "../../../framework/modules.js";
22
22
  //#region src/router/domains/ed25519Yao/export/routerAbEd25519YaoExport.ts
23
23
  const EXPORT_AUTH_MAX_TTL_MS = 6e4;
@@ -3,8 +3,8 @@ import { isPlainObject } from "../../../shared-ts/src/utils/validation.js";
3
3
  import { base64UrlDecode, base64UrlEncode } from "../../../shared-ts/src/utils/base64.js";
4
4
  import { failure } from "../../../shared-ts/src/utils/failure.js";
5
5
  import "../../../shared-ts/src/utils/encoders.js";
6
- import { emailOtpStatusCode } from "./emailOtpSessionRouteHelpers.js";
7
6
  import { EMAIL_OTP_FACTOR_RELEASE_AAD_DOMAIN_V1 } from "../../../shared-ts/src/utils/emailOtpDomain.js";
7
+ import { emailOtpStatusCode } from "./emailOtpSessionRouteHelpers.js";
8
8
  //#region src/router/domains/emailOtp/emailOtpRouteHandlers.ts
9
9
  function decodeEmailOtpFactorSecret32(value) {
10
10
  const magnitude = base64UrlDecode(value);
@@ -4,11 +4,11 @@ import { parseDigestField, parseEnvelopeCiphertextB64u, parseEnvelopeNonceB64u,
4
4
  import { parsePasskeyCustodySecretBinding } from "../../../shared-ts/src/passkey-custody/custodySecretBinding.js";
5
5
  import { buildActiveEnvelopeLifecycle, buildPasskeyCustodyEnvelopeRecord, parseWalletCustodyEnvelopeFactor, parseWalletCustodyEnvelopeOwnershipWireV1 } from "../../../shared-ts/src/passkey-custody/custodyEnvelope.js";
6
6
  import "../../../shared-ts/src/passkey-custody/index.js";
7
+ import { parseRecoveryCodeLocatorV1 } from "../../../shared-ts/src/wallet-recovery/recoveryCodeLocator.js";
7
8
  import { parseDerivedWalletRecoveryKeyId } from "../../../shared-ts/src/wallet-recovery/recoveryKeyId.js";
8
9
  import { buildWalletRecoveryBackupAcknowledgementV1 } from "../../../shared-ts/src/wallet-recovery/backupAcknowledgement.js";
9
10
  import "../../../shared-ts/src/wallet-recovery/recoveryCodes.js";
10
11
  import { buildWalletCustodySeedRecoveryEntry, buildWalletRecoveryEnvelopeSetRecord, buildWalletRecoveryManifestKekWrap } from "../../../shared-ts/src/wallet-recovery/walletRecoveryEnvelopeSet.js";
11
- import { parseRecoveryCodeLocatorV1 } from "../../../shared-ts/src/wallet-recovery/recoveryCodeLocator.js";
12
12
  import "../../../shared-ts/src/wallet-recovery/index.js";
13
13
  //#region src/router/domains/passkeyCustody/walletCustodyRegistrationCommit.ts
14
14
  function requireNonEmpty(value, label) {
@@ -1,4 +1,4 @@
1
- import { mpcMaterialActivationRefsEqual } from "../../../shared-ts/src/utils/domainIds.js";
1
+ import { mpcMaterialActivationRefsEqual, parseWalletId } from "../../../shared-ts/src/utils/domainIds.js";
2
2
  import { isPlainObject } from "../../../shared-ts/src/utils/validation.js";
3
3
  import { base64UrlEncode } from "../../../shared-ts/src/utils/base64.js";
4
4
  import { parseDigestB64u } from "../../../shared-ts/src/utils/canonicalPrimitives.js";
@@ -263,6 +263,40 @@ function isWalletSessionUnavailableStepUpError(result) {
263
263
  return isPlainObject(result.body) && result.body.code === "wallet_session_unavailable";
264
264
  }
265
265
  async function authenticateRouterAbWalletOperationStepUp(input) {
266
+ if (!extractBearerCredential(input.headers) && input.authorizedOperations) {
267
+ const origin = parseStepUpRequestOrigin(input.headers);
268
+ if (!origin.ok) return origin;
269
+ const operationId = requireAuthorizationValue(parseAuthorizedOperationId(`normal-signing-operation:${input.scope.request_id}`));
270
+ const parsedWalletId = parseWalletId(input.scope.account_id);
271
+ if (!parsedWalletId.ok) return walletSessionScopeInvalidFailure();
272
+ const walletId = parsedWalletId.value;
273
+ const recorded = await input.authorizedOperations.resolveRecordedPasskeyOwnerOperation({
274
+ authorizedOperationId: operationId,
275
+ walletId,
276
+ requestOrigin: origin.requestOrigin,
277
+ nowMs: Date.now()
278
+ });
279
+ if (!recorded || input.requestExpiresAtMs > recorded.expiresAtMs) return {
280
+ ok: false,
281
+ error: routerAbStepUpError(403, "unauthorized", "Recorded owner approval is unavailable")
282
+ };
283
+ const activeMaterial = await input.resolveEd25519MaterialActivation({
284
+ walletId,
285
+ materialActivation: input.scope.material_activation
286
+ });
287
+ if (!activeMaterial.ok) return walletSessionScopeInvalidFailure();
288
+ return authenticateRouterAbWalletOperationStepUpIdentity({
289
+ kind: "verified_owner_proof",
290
+ headers: input.headers,
291
+ walletId,
292
+ materialOwner: input.scope.material_activation.material_owner,
293
+ operationId: recorded.operation.operation.operationId,
294
+ authority: recorded.authority,
295
+ runtimePolicyScope: activeMaterial.runtimePolicyScope,
296
+ expiresAtMs: recorded.expiresAtMs,
297
+ authorizedOperations: input.authorizedOperations
298
+ });
299
+ }
266
300
  return authenticateRouterAbWalletOperationStepUpIdentity({
267
301
  kind: "wallet_session_operation_credential_v1",
268
302
  headers: input.headers,
@@ -1,6 +1,7 @@
1
1
  import { parseWalletAuthMethodId, parseWalletAuthorityId, parseWalletId } from "../../../shared-ts/src/utils/domainIds.js";
2
- import { isPlainObject, requireCanonicalString } from "../../../shared-ts/src/utils/validation.js";
2
+ import { isPlainObject, requireCanonicalString, requireRecord } from "../../../shared-ts/src/utils/validation.js";
3
3
  import { alphabetizeStringify, sha256HexUtf8 } from "../../../shared-ts/src/utils/digests.js";
4
+ import { parseDigestB64u } from "../../../shared-ts/src/utils/canonicalPrimitives.js";
4
5
  import { parseDeviceId } from "../../../shared-ts/src/authorization/capabilityKinds.js";
5
6
  import { secureRandomBase64Url } from "../../../shared-ts/src/utils/secureRandomId.js";
6
7
  import { createServerAllocatedWalletId } from "../../../shared-ts/src/utils/registrationIds.js";
@@ -44,6 +45,7 @@ async function walletRegistrationSetupRequestDigest(namespace, input) {
44
45
  runtimePolicyScope: input.runtimePolicyScope,
45
46
  expectedOrigin: input.expectedOrigin,
46
47
  wallet: input.request.wallet ?? { kind: "server_allocated" },
48
+ completion: input.request.completion,
47
49
  authMethod: input.request.authMethod,
48
50
  signerSelection: input.request.signerSelection
49
51
  }));
@@ -64,7 +66,20 @@ async function admitRegistrationReservationHome(authority, input) {
64
66
  };
65
67
  return authority.admitHome(input);
66
68
  }
69
+ function parseRegistrationSessionPublication(raw, walletId) {
70
+ const value = requireRecord(raw, "registration session publication");
71
+ if (value.kind === "none") return { kind: "none" };
72
+ if (value.kind !== "credential" || value.walletId !== String(walletId)) throw new Error("Invalid registration session publication");
73
+ const expiresAtMs = value.expiresAtMs;
74
+ if (typeof expiresAtMs !== "number" || !Number.isSafeInteger(expiresAtMs) || expiresAtMs <= 0) throw new Error("Invalid registration session publication expiry");
75
+ return {
76
+ kind: "credential",
77
+ walletId,
78
+ digest: parseDigestB64u(value.digest),
79
+ expiresAtMs
80
+ };
81
+ }
67
82
  //#endregion
68
- export { admitRegistrationReservationHome, cancelRegistrationHome, parseWalletRegistrationSetupReservation, proposeWalletRegistrationSetup, walletRegistrationSetupRequestDigest };
83
+ export { admitRegistrationReservationHome, cancelRegistrationHome, parseRegistrationSessionPublication, parseWalletRegistrationSetupReservation, proposeWalletRegistrationSetup, walletRegistrationSetupRequestDigest };
69
84
 
70
85
  //# sourceMappingURL=walletRegistrationReservation.js.map
@@ -11,6 +11,7 @@ import { normalizeRuntimePolicyScope } from "../../../shared-ts/src/threshold/si
11
11
  import { parseRouterAbEcdsaRegistrationActivationRequestV1, parseRouterAbEcdsaRegistrationRequestV1 } from "../../../shared-ts/src/utils/routerAbEcdsaDerivation.js";
12
12
  import { normalizeAddAuthMethodInput, normalizeEmailOtpRegistrationProof, normalizeRegistrationAuthMethodInput } from "../../../shared-ts/src/utils/registrationAuthMethodInput.js";
13
13
  import { parseHostedWalletSessionOperationCredentialToken, parsePrimaryWalletSessionOperationCredentialToken, parseSessionOrigin } from "../../../authorization/domain.js";
14
+ import { routeError, routeJson } from "../../framework/routeResponses.js";
14
15
  import { extractBearerCredential, extractRouterApiEnvironmentId } from "../../auth/routerApiKeyAuth.js";
15
16
  import { normalizeCorsOrigin } from "../../../core/SessionService.js";
16
17
  import { parseRouterAbTraceContextV1 } from "../../../shared-ts/src/utils/routerAbTraceContext.js";
@@ -22,7 +23,6 @@ import { publishableKeyRoutePrincipal, resolvePublishableKeyApiCredentialAuth, r
22
23
  import { resolveActiveRuntimePolicyScopeForEnvironment, resolveWalletSessionAdministrationAdmission, resolveWalletSessionOperationCredentialAdmission, resolveWalletSessionOperationCredentialAdmissionFromContext } from "../../auth/commonRouterUtils.js";
23
24
  import { enforceRoutePolicy } from "../../framework/enforceRoutePolicy.js";
24
25
  import { parseRegistrationPreparedSessionSeal } from "./registrationSessionSeal.js";
25
- import { routeError, routeJson } from "../../framework/routeResponses.js";
26
26
  import { parsePasskeyCustodyEnvelopeRecord } from "../../../shared-ts/src/passkey-custody/custodyEnvelope.js";
27
27
  import "../../../shared-ts/src/passkey-custody/index.js";
28
28
  import { computeWalletEcdsaKeyFactsInventoryChallengeDigestB64u } from "../../../shared-ts/src/utils/ecdsaKeyFactsInventory.js";
@@ -343,12 +343,19 @@ function parseWalletRegistrationSetupRequest(body) {
343
343
  code: "invalid_body",
344
344
  message: "authMethod is required"
345
345
  };
346
+ const completion = body.completion ?? "wallet_session";
347
+ if (completion !== "wallet_session" && completion !== "owner_operations") return {
348
+ ok: false,
349
+ code: "invalid_body",
350
+ message: "Invalid registration completion"
351
+ };
346
352
  const wallet = parseWalletRegistrationSetupWallet(body.wallet);
347
353
  if (!wallet.ok) return wallet;
348
354
  return {
349
355
  ok: true,
350
356
  value: {
351
357
  registrationOperationId,
358
+ completion,
352
359
  ...wallet.value ? { wallet: wallet.value } : {},
353
360
  signerSelection: signerSelection.value.selection,
354
361
  authMethod
@@ -1928,6 +1935,7 @@ async function handleRouterApiWalletRegistrationNearProvisioning(input) {
1928
1935
  if (sessionSeal.value) {
1929
1936
  if (!isPasskeyWalletRegistrationNearProvisioningSuccessV2(result)) return routeError(400, "invalid_body", "Registration session sealing requires a passkey authority");
1930
1937
  const sealStartedAt = Date.now();
1938
+ if (result.registrationEstablishedSession.kind === "owner_operations") return routeError(400, "invalid_body", "Owner registration has no session seal");
1931
1939
  const sealed = await sessionSeal.value.finish(result.registrationEstablishedSession.session);
1932
1940
  serverTiming.push(["near_finalize_session_seal", Math.max(0, Date.now() - sealStartedAt)]);
1933
1941
  if (!sealed.ok) return routeJson(503, {
@@ -7,8 +7,8 @@ import { ROUTER_AB_ED25519_YAO_EMAIL_OTP_RECOVERY_BOOTSTRAP_KIND_V1 } from "../.
7
7
  import { walletAuthAuthorityRef } from "../../../shared-ts/src/utils/walletAuthAuthority.js";
8
8
  import { parseSessionOrigin, parseVerifiedOwnerProofId, projectActiveWalletSession } from "../../../authorization/domain.js";
9
9
  import { buildVerifiedWalletSessionEmailOtpFactorResult, buildVerifiedWalletSessionPasskeyFactorResult } from "../../../authorization/factorEvidence.js";
10
- import { emailOtpFailureWebhookEventDescriptors, emailOtpLoggedInWebhookEventDescriptor } from "../emailOtp/emailOtpSessionRouteHelpers.js";
11
10
  import { EMAIL_OTP_CHANNEL } from "../../../shared-ts/src/utils/emailOtpDomain.js";
11
+ import { emailOtpFailureWebhookEventDescriptors, emailOtpLoggedInWebhookEventDescriptor } from "../emailOtp/emailOtpSessionRouteHelpers.js";
12
12
  import { thresholdEd25519StatusCode } from "../../../threshold/statusCodes.js";
13
13
  import { parseWalletUnlockBackend } from "../emailOtp/emailOtpRequestValidation.js";
14
14
  //#region src/router/domains/walletUnlock/walletUnlockRouteHandlers.ts
@@ -5,9 +5,9 @@ import { parseDigestB64u } from "../../../../shared-ts/src/utils/canonicalPrimit
5
5
  import "../../../../shared-ts/src/utils/encoders.js";
6
6
  import { parseAuthFactorId } from "../../../../shared-ts/src/authorization/capabilityKinds.js";
7
7
  import { buildVerifiedWalletOperationEmailOtpFactorResult, buildVerifiedWalletOperationPasskeyFactorResult } from "../../../../authorization/factorEvidence.js";
8
- import { json } from "../../../framework/http.js";
9
- import { hashEmailOtpOperationBinding } from "../../../domains/emailOtp/emailOtpSessionRouteHelpers.js";
10
8
  import { EMAIL_OTP_CHANNEL, WALLET_EMAIL_OTP_EXPORT_OPERATION, WALLET_EMAIL_OTP_TRANSACTION_SIGN_OPERATION } from "../../../../shared-ts/src/utils/emailOtpDomain.js";
9
+ import { hashEmailOtpOperationBinding } from "../../../domains/emailOtp/emailOtpSessionRouteHelpers.js";
10
+ import { json } from "../../../framework/http.js";
11
11
  //#region src/router/transport/fetch/routes/ecdsaOperationStepUpFactor.ts
12
12
  async function verifyEcdsaOperationStepUpFactor(input) {
13
13
  const proof = input.proof;
@@ -2,7 +2,8 @@ import { parseWalletId } from "../../../../shared-ts/src/utils/domainIds.js";
2
2
  import { requireCanonicalString, requireRecord } from "../../../../shared-ts/src/utils/validation.js";
3
3
  import { sha256HexUtf8 } from "../../../../shared-ts/src/utils/digests.js";
4
4
  import { EXTERNAL_AUTH_PATH, externalAuthFailure, parseExternalAuthConfiguration } from "../../../../shared-ts/src/auth/externalAuth.js";
5
- import { buildDelegatedWalletAuthorityV1, buildFullOwnerPermissionsV1, sameDelegatedWalletAuthorityV1 } from "../../../../shared-ts/src/authorization/delegatedAuthority.js";
5
+ import { externalOwnerChallenge, readExternalOwnerOperation } from "../../../../auth/externalOwnerOperations.js";
6
+ import { authorizeWalletOwnerOperation, completeWalletOwnerOperation, walletOwnerOperationAuthorizationId } from "../walletOwnerOperationAuthorization.js";
6
7
  import { extractBearerCredential, resolveSourceIpFromFetchHeaders } from "../../../auth/routerApiKeyAuth.js";
7
8
  import { json, readJson } from "../../../framework/http.js";
8
9
  //#region src/router/transport/fetch/routes/externalAuth.ts
@@ -10,12 +11,29 @@ const ACTIONS = [
10
11
  "associate",
11
12
  "unlink",
12
13
  "prepare",
14
+ "prepare-association",
15
+ "prepare-unlink",
16
+ "owner-challenge",
13
17
  "exchange",
14
18
  "backend-exchange",
15
19
  "redeem",
16
20
  "settings",
17
21
  "logout"
18
22
  ];
23
+ function parseIdentityProof(value) {
24
+ const proof = requireRecord(value, "external identity proof");
25
+ switch (proof.kind) {
26
+ case "google": return {
27
+ kind: "google",
28
+ idToken: requireCanonicalString(proof.idToken, "idToken")
29
+ };
30
+ case "application": return {
31
+ kind: "application",
32
+ assertion: requireCanonicalString(proof.assertion, "assertion")
33
+ };
34
+ default: throw new Error("Unsupported external identity proof");
35
+ }
36
+ }
19
37
  function parseCommand(action, body) {
20
38
  const wallet = parseWalletId(body.walletId);
21
39
  if (!wallet.ok) throw new Error("Wallet ID is invalid");
@@ -24,26 +42,41 @@ function parseCommand(action, body) {
24
42
  action,
25
43
  walletId,
26
44
  attemptId: requireCanonicalString(body.attemptId, "attemptId"),
27
- idToken: requireCanonicalString(body.idToken, "idToken")
45
+ proof: parseIdentityProof(body.proof)
28
46
  };
29
47
  const appOrigin = requireCanonicalString(body.appOrigin, "appOrigin");
30
48
  if (new URL(appOrigin).origin !== appOrigin) throw new Error("Application origin is invalid");
31
49
  switch (action) {
32
- case "associate": return {
50
+ case "associate":
51
+ case "unlink": return {
33
52
  action,
34
53
  walletId,
35
54
  appOrigin,
36
- ownerCredential: requireCanonicalString(body.ownerCredential, "ownerCredential"),
37
- idToken: requireCanonicalString(body.idToken, "idToken")
55
+ attemptId: requireCanonicalString(body.attemptId, "attemptId"),
56
+ binding: secret(body.binding, "binding"),
57
+ sessionToken: secret(body.sessionToken, "sessionToken"),
58
+ code: secret(body.code, "code"),
59
+ factorProof: body.factorProof
38
60
  };
39
- case "unlink": return {
61
+ case "owner-challenge": return {
40
62
  action,
41
63
  walletId,
42
64
  appOrigin,
43
- ownerCredential: requireCanonicalString(body.ownerCredential, "ownerCredential"),
44
- associationId: requireCanonicalString(body.associationId, "associationId")
65
+ attemptId: requireCanonicalString(body.attemptId, "attemptId"),
66
+ binding: secret(body.binding, "binding"),
67
+ sessionToken: secret(body.sessionToken, "sessionToken"),
68
+ code: secret(body.code, "code")
69
+ };
70
+ case "prepare-unlink": return {
71
+ action,
72
+ walletId,
73
+ appOrigin,
74
+ associationId: requireCanonicalString(body.associationId, "associationId"),
75
+ binding: secret(body.binding, "binding"),
76
+ sessionToken: secret(body.sessionToken, "sessionToken")
45
77
  };
46
- case "prepare": return {
78
+ case "prepare":
79
+ case "prepare-association": return {
47
80
  action,
48
81
  walletId,
49
82
  appOrigin,
@@ -143,11 +176,10 @@ async function resolveApplicationTrust(ctx, service, command) {
143
176
  actorUserId: result.principal.apiKeyId,
144
177
  roles: []
145
178
  }, { status: "ACTIVE" }))?.find(matchesEnvironment.bind(null, scope.envId));
146
- const configuration = parseExternalAuthConfiguration(environment?.externalAuth ?? {
179
+ return parseExternalAuthConfiguration(environment?.externalAuth ?? {
147
180
  kind: "disabled",
148
181
  revision: 0
149
182
  });
150
- return configuration.kind === "google" ? configuration : null;
151
183
  }
152
184
  function matchesEnvironment(environmentId, environment) {
153
185
  return environment.id === environmentId;
@@ -174,43 +206,118 @@ async function handleExternalAuth(ctx) {
174
206
  }
175
207
  async function dispatch(ctx, service, trust, command) {
176
208
  const walletId = command.walletId;
177
- const originBinding = JSON.stringify([ctx.request.headers.get("origin") || "", command.appOrigin]);
178
- if (command.action === "prepare" || command.action === "exchange" || command.action === "backend-exchange" || command.action === "associate") {
209
+ if (command.action === "prepare" || command.action === "prepare-association" || command.action === "prepare-unlink" || command.action === "exchange" || command.action === "backend-exchange" || command.action === "associate") {
179
210
  const rateKey = await sha256HexUtf8(resolveSourceIpFromFetchHeaders(ctx.request.headers) || "unknown");
180
211
  if (!await service.store.consumeAuthenticationAllowance(rateKey, Date.now())) return failure(429, "rate_limited");
181
212
  }
182
- if (command.action === "associate" || command.action === "unlink") {
183
- const token = command.ownerCredential;
184
- const owner = await ctx.service.authorizationSessions.readWalletSessionAuthorizationV2ByOperationCredential({
185
- tenantId: ctx.service.authorizationSessions.tenantId,
186
- token,
213
+ if (command.action === "backend-exchange") {
214
+ if (trust.kind === "disabled") return failure(403, "access_denied");
215
+ return json(await service.exchange({
216
+ walletId,
217
+ attemptId: command.attemptId,
218
+ proof: command.proof,
219
+ trust
220
+ }));
221
+ }
222
+ const originBinding = JSON.stringify([ctx.request.headers.get("origin") || "", command.appOrigin]);
223
+ if (command.action === "prepare-unlink") return json(await service.prepareUnlink({
224
+ walletId,
225
+ associationId: command.associationId,
226
+ originBinding,
227
+ binding: command.binding,
228
+ sessionToken: command.sessionToken,
229
+ trustRevision: trust.revision
230
+ }));
231
+ if (command.action === "owner-challenge" || command.action === "associate" || command.action === "unlink") {
232
+ const binding = {
233
+ walletId,
234
+ attemptId: command.attemptId,
235
+ originBinding,
236
+ bindingHash: await sha256HexUtf8(command.binding),
237
+ sessionTokenHash: await sha256HexUtf8(command.sessionToken),
238
+ codeHash: await sha256HexUtf8(command.code),
239
+ trustRevision: trust.revision,
187
240
  nowMs: Date.now()
241
+ };
242
+ const result = await readExternalOwnerOperation({
243
+ store: service.store,
244
+ binding,
245
+ requestOrigin: ctx.request.headers.get("origin") || "",
246
+ phase: command.action === "owner-challenge" ? "prepare" : "complete"
247
+ });
248
+ if (!result.ok) return json(result, { status: 403 });
249
+ const ownerOperation = result.value;
250
+ if (command.action === "owner-challenge") return json({
251
+ ok: true,
252
+ value: externalOwnerChallenge(ownerOperation)
188
253
  });
189
- if (!owner || owner.authorization.session.walletId !== walletId || !sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: owner.authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }))) return failure(403, "access_denied");
190
- if (command.action === "unlink") {
191
- await service.store.revokeAssociation(walletId, command.associationId);
192
- return json({ ok: true });
254
+ if (command.action !== ownerOperation.attempt.purpose) return failure(403, "access_denied");
255
+ if (ownerOperation.attempt.status === "consumed") {
256
+ const stored = await ctx.service.authorizedOperations.readAuthorizedOperationById({
257
+ tenantId: ctx.service.authorizedOperations.tenantId,
258
+ authorizedOperationId: walletOwnerOperationAuthorizationId(ownerOperation.operation, ownerOperation.challengeB64u)
259
+ });
260
+ if (!stored || stored.authorization.kind !== "verified_step_up" || stored.quota.kind !== "quota_neutral" || stored.lifecycle !== "completed") return failure(503, "unavailable");
261
+ return new Response(stored.response.bodyText, {
262
+ status: stored.response.status,
263
+ headers: {
264
+ "Content-Type": stored.response.contentType,
265
+ "Cache-Control": "no-store"
266
+ }
267
+ });
193
268
  }
194
- const proof = await service.verifyGoogle(trust, command.idToken);
195
- if (!proof.ok) return json(proof, { status: 403 });
196
- return await service.store.associate({
269
+ const authorized = await authorizeWalletOwnerOperation({
270
+ ctx,
197
271
  walletId,
198
- subject: proof.value.subject,
199
- authorityId: owner.authority.authorityId,
200
- authorityEpoch: owner.authority.revocationEpoch
201
- }) ? json({ ok: true }) : failure(409, "access_denied");
272
+ operation: ownerOperation.operation,
273
+ payload: ownerOperation.payload,
274
+ factorProof: command.factorProof
275
+ });
276
+ if (!authorized.ok) return authorized.response;
277
+ if (!await service.store.consumeOwnerAttempt({
278
+ walletId,
279
+ attemptId: binding.attemptId,
280
+ originBinding,
281
+ bindingHash: binding.bindingHash,
282
+ sessionTokenHash: binding.sessionTokenHash,
283
+ codeHash: binding.codeHash,
284
+ trustRevision: binding.trustRevision,
285
+ nowMs: Date.now()
286
+ }, authorized.ownerAuthority)) return completeWalletOwnerOperation(ctx, authorized.operation, 403, externalAuthFailure("access_denied"), "failed_before_side_effect");
287
+ const attempt = ownerOperation.attempt;
288
+ if (attempt.purpose === "associate") {
289
+ if (!await service.store.associate({
290
+ walletId,
291
+ identity: attempt.identity,
292
+ authorityId: authorized.ownerAuthority.authorityId,
293
+ authorityEpoch: authorized.ownerAuthority.revocationEpoch
294
+ })) return completeWalletOwnerOperation(ctx, authorized.operation, 403, externalAuthFailure("access_denied"), "failed_before_side_effect");
295
+ } else await service.store.revokeAssociation({
296
+ walletId,
297
+ associationId: attempt.associationId,
298
+ owner: authorized.ownerAuthority
299
+ });
300
+ return completeWalletOwnerOperation(ctx, authorized.operation, 200, {
301
+ ok: true,
302
+ value: null
303
+ });
202
304
  }
203
- if (command.action === "prepare") return json(await service.prepare({
305
+ if (trust.kind === "disabled") return failure(403, "access_denied");
306
+ if (command.action === "prepare" || command.action === "prepare-association") return json(await service.prepare({
307
+ purpose: command.action === "prepare" ? "authenticate" : "associate",
204
308
  walletId,
205
309
  originBinding,
206
310
  binding: command.binding,
207
311
  sessionToken: command.sessionToken
208
312
  }));
209
- if (command.action === "exchange" || command.action === "backend-exchange") return json(await service.exchange({
313
+ if (command.action === "exchange") return json(await service.exchange({
210
314
  trust,
211
315
  walletId,
212
316
  attemptId: command.attemptId,
213
- idToken: command.idToken
317
+ proof: {
318
+ kind: "google",
319
+ idToken: command.idToken
320
+ }
214
321
  }));
215
322
  const tokenHash = await sha256HexUtf8(command.sessionToken);
216
323
  if (command.action === "logout") {