@effect-auth/core 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (158) hide show
  1. package/README.md +148 -13
  2. package/dist/ApiKey.d.ts +207 -0
  3. package/dist/ApiKey.d.ts.map +1 -0
  4. package/dist/ApiKey.js +275 -0
  5. package/dist/ApiKey.js.map +1 -0
  6. package/dist/AuditLog.d.ts +204 -1
  7. package/dist/AuditLog.d.ts.map +1 -1
  8. package/dist/AuditLog.js +170 -0
  9. package/dist/AuditLog.js.map +1 -1
  10. package/dist/AuthConfig.d.ts +0 -9
  11. package/dist/AuthConfig.d.ts.map +1 -1
  12. package/dist/AuthConfig.js +1 -14
  13. package/dist/AuthConfig.js.map +1 -1
  14. package/dist/AuthFlow.d.ts +220 -51
  15. package/dist/AuthFlow.d.ts.map +1 -1
  16. package/dist/AuthFlow.js +550 -22
  17. package/dist/AuthFlow.js.map +1 -1
  18. package/dist/AuthKernel.d.ts +1 -1
  19. package/dist/Challenge.d.ts +5 -1
  20. package/dist/Challenge.d.ts.map +1 -1
  21. package/dist/Challenge.js +58 -48
  22. package/dist/Challenge.js.map +1 -1
  23. package/dist/Client.d.ts +263 -1
  24. package/dist/Client.d.ts.map +1 -1
  25. package/dist/Client.js +533 -1
  26. package/dist/Client.js.map +1 -1
  27. package/dist/D1Kysely.d.ts +1 -1
  28. package/dist/D1Kysely.d.ts.map +1 -1
  29. package/dist/EffectQbSqliteStorage.d.ts +115 -3
  30. package/dist/EffectQbSqliteStorage.d.ts.map +1 -1
  31. package/dist/EffectQbSqliteStorage.js +314 -4
  32. package/dist/EffectQbSqliteStorage.js.map +1 -1
  33. package/dist/EmailOtp.d.ts +5 -2
  34. package/dist/EmailOtp.d.ts.map +1 -1
  35. package/dist/EmailOtp.js +11 -4
  36. package/dist/EmailOtp.js.map +1 -1
  37. package/dist/EmailVerification.d.ts +2 -2
  38. package/dist/EmailVerification.d.ts.map +1 -1
  39. package/dist/EmailVerification.js +2 -3
  40. package/dist/EmailVerification.js.map +1 -1
  41. package/dist/HttpApi/Api.d.ts +1363 -19
  42. package/dist/HttpApi/Api.d.ts.map +1 -1
  43. package/dist/HttpApi/Api.js +1625 -20
  44. package/dist/HttpApi/Api.js.map +1 -1
  45. package/dist/HttpApi/Endpoints.d.ts +682 -0
  46. package/dist/HttpApi/Endpoints.d.ts.map +1 -1
  47. package/dist/HttpApi/Endpoints.js +252 -1
  48. package/dist/HttpApi/Endpoints.js.map +1 -1
  49. package/dist/HttpApi/Schemas.d.ts +841 -0
  50. package/dist/HttpApi/Schemas.d.ts.map +1 -1
  51. package/dist/HttpApi/Schemas.js +513 -1
  52. package/dist/HttpApi/Schemas.js.map +1 -1
  53. package/dist/HttpApi/Security.d.ts +13 -1
  54. package/dist/HttpApi/Security.d.ts.map +1 -1
  55. package/dist/HttpApi/Security.js +107 -16
  56. package/dist/HttpApi/Security.js.map +1 -1
  57. package/dist/HttpApi/Transport.d.ts +36 -2
  58. package/dist/HttpApi/Transport.d.ts.map +1 -1
  59. package/dist/HttpApi/Transport.js +151 -9
  60. package/dist/HttpApi/Transport.js.map +1 -1
  61. package/dist/Jwt.d.ts +271 -0
  62. package/dist/Jwt.d.ts.map +1 -0
  63. package/dist/Jwt.js +512 -0
  64. package/dist/Jwt.js.map +1 -0
  65. package/dist/KyselyStorage.d.ts +151 -1
  66. package/dist/KyselyStorage.d.ts.map +1 -1
  67. package/dist/KyselyStorage.js +1021 -4
  68. package/dist/KyselyStorage.js.map +1 -1
  69. package/dist/LoginApproval.d.ts +314 -0
  70. package/dist/LoginApproval.d.ts.map +1 -0
  71. package/dist/LoginApproval.js +551 -0
  72. package/dist/LoginApproval.js.map +1 -0
  73. package/dist/LoginNotification.d.ts +183 -0
  74. package/dist/LoginNotification.d.ts.map +1 -0
  75. package/dist/LoginNotification.js +329 -0
  76. package/dist/LoginNotification.js.map +1 -0
  77. package/dist/LoginRisk.d.ts +73 -0
  78. package/dist/LoginRisk.d.ts.map +1 -0
  79. package/dist/LoginRisk.js +57 -0
  80. package/dist/LoginRisk.js.map +1 -0
  81. package/dist/MagicLink.d.ts +85 -0
  82. package/dist/MagicLink.d.ts.map +1 -0
  83. package/dist/MagicLink.js +206 -0
  84. package/dist/MagicLink.js.map +1 -0
  85. package/dist/Mailer.d.ts +33 -2
  86. package/dist/Mailer.d.ts.map +1 -1
  87. package/dist/Mailer.js +107 -0
  88. package/dist/Mailer.js.map +1 -1
  89. package/dist/OAuth.d.ts +742 -0
  90. package/dist/OAuth.d.ts.map +1 -0
  91. package/dist/OAuth.js +1387 -0
  92. package/dist/OAuth.js.map +1 -0
  93. package/dist/Passkey.d.ts +334 -0
  94. package/dist/Passkey.d.ts.map +1 -0
  95. package/dist/Passkey.js +422 -0
  96. package/dist/Passkey.js.map +1 -0
  97. package/dist/Password.d.ts +138 -8
  98. package/dist/Password.d.ts.map +1 -1
  99. package/dist/Password.js +397 -8
  100. package/dist/Password.js.map +1 -1
  101. package/dist/RecoveryCode.d.ts +169 -0
  102. package/dist/RecoveryCode.d.ts.map +1 -0
  103. package/dist/RecoveryCode.js +299 -0
  104. package/dist/RecoveryCode.js.map +1 -0
  105. package/dist/RefreshToken.d.ts +222 -0
  106. package/dist/RefreshToken.d.ts.map +1 -0
  107. package/dist/RefreshToken.js +342 -0
  108. package/dist/RefreshToken.js.map +1 -0
  109. package/dist/Sessions.d.ts +52 -2
  110. package/dist/Sessions.d.ts.map +1 -1
  111. package/dist/Sessions.js +123 -1
  112. package/dist/Sessions.js.map +1 -1
  113. package/dist/StepUp.d.ts +44 -0
  114. package/dist/StepUp.d.ts.map +1 -0
  115. package/dist/StepUp.js +82 -0
  116. package/dist/StepUp.js.map +1 -0
  117. package/dist/Storage.d.ts +14 -1
  118. package/dist/Storage.d.ts.map +1 -1
  119. package/dist/Storage.js.map +1 -1
  120. package/dist/StorageMigrations.d.ts +10 -1
  121. package/dist/StorageMigrations.d.ts.map +1 -1
  122. package/dist/StorageMigrations.js +179 -0
  123. package/dist/StorageMigrations.js.map +1 -1
  124. package/dist/Testing.d.ts +1 -1
  125. package/dist/Totp.d.ts +220 -0
  126. package/dist/Totp.d.ts.map +1 -0
  127. package/dist/Totp.js +477 -0
  128. package/dist/Totp.js.map +1 -0
  129. package/dist/TrustedDevice.d.ts +115 -0
  130. package/dist/TrustedDevice.d.ts.map +1 -0
  131. package/dist/TrustedDevice.js +222 -0
  132. package/dist/TrustedDevice.js.map +1 -0
  133. package/dist/Webhook.d.ts +72 -0
  134. package/dist/Webhook.d.ts.map +1 -0
  135. package/dist/Webhook.js +82 -0
  136. package/dist/Webhook.js.map +1 -0
  137. package/dist/alchemy/cloudflare/Email.d.ts +25 -0
  138. package/dist/alchemy/cloudflare/Email.d.ts.map +1 -0
  139. package/dist/alchemy/cloudflare/Email.js +117 -0
  140. package/dist/alchemy/cloudflare/Email.js.map +1 -0
  141. package/dist/cloudflare/Email.d.ts +25 -0
  142. package/dist/cloudflare/Email.d.ts.map +1 -0
  143. package/dist/cloudflare/Email.js +153 -0
  144. package/dist/cloudflare/Email.js.map +1 -0
  145. package/dist/index.d.ts +14 -0
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +14 -0
  148. package/dist/index.js.map +1 -1
  149. package/migrations/0003_auth_login_approval_review.sql +22 -0
  150. package/migrations/0004_auth_trusted_device.sql +12 -0
  151. package/migrations/0005_auth_passkey.sql +17 -0
  152. package/migrations/0006_auth_totp_factor.sql +17 -0
  153. package/migrations/0007_auth_recovery_code.sql +13 -0
  154. package/migrations/0008_auth_api_key.sql +17 -0
  155. package/migrations/0009_auth_refresh_token.sql +21 -0
  156. package/migrations/0010_auth_jwt_revocation.sql +8 -0
  157. package/migrations/0011_auth_oauth_account.sql +16 -0
  158. package/package.json +65 -1
@@ -1,14 +1,23 @@
1
1
  import { Effect, Layer, Option, Redacted } from "effect";
2
2
  import { HttpServerResponse } from "effect/unstable/http";
3
3
  import { HttpApi, HttpApiBuilder, HttpApiGroup } from "effect/unstable/httpapi";
4
- import { AuthResult } from "../AuthFlow.js";
4
+ import { ApiKeyId } from "../ApiKey.js";
5
+ import { AuthFlow, AuthFlowState, AuthResult, LoginApprovalFinalize, } from "../AuthFlow.js";
6
+ import { EmailOtpLogin } from "../EmailOtp.js";
5
7
  import { EmailDeliveryError, EmailVerification, EmailVerificationFlow, EmailVerificationIssueError, } from "../EmailVerification.js";
6
- import { PasswordLogin, PasswordRegistration } from "../Password.js";
7
- import { SessionCookie, Sessions } from "../Sessions.js";
8
- import { currentSessionEndpoint, emailVerificationStartEndpoint, emailVerificationVerifyEndpoint, logoutEndpoint, passwordSignInEndpoint, passwordSignUpEndpoint, refreshSessionEndpoint, } from "./Endpoints.js";
9
- import { AuthBadRequestError, AuthEmailAlreadyRegisteredError, AuthInternalError, AuthRateLimitedError, AuthSchemaErrorMiddleware, AuthSchemaErrorMiddlewareLive, AuthUnauthenticatedError, } from "./Errors.js";
10
- import { AuthApiRateLimit, AuthApiRateLimitLive, AuthApiRateLimitNoopLive, AuthOriginCheckMiddleware, AuthOriginCheckMiddlewareConfigLive, AuthRateLimitMiddleware, } from "./Security.js";
11
- import { AuthHttp } from "./Transport.js";
8
+ import { currentUnixMillis } from "../Internal.js";
9
+ import { LoginApproval, LoginApprovalReview } from "../LoginApproval.js";
10
+ import { LoginNotification } from "../LoginNotification.js";
11
+ import { MagicLinkLogin } from "../MagicLink.js";
12
+ import { PasswordLogin, PasswordManagement, PasswordRegistration, PasswordReset, } from "../Password.js";
13
+ import { PasskeyCredentialId } from "../Passkey.js";
14
+ import { SessionCookie, Sessions, sessionRequestMetadataKey } from "../Sessions.js";
15
+ import { UserStore } from "../Storage.js";
16
+ import { TrustedDevice, TrustedDeviceCookie } from "../TrustedDevice.js";
17
+ import { apiKeyCreateEndpoint, apiKeyListEndpoint, apiKeyRevokeEndpoint, currentSessionEndpoint, emailOtpStartEndpoint, emailOtpVerifyEndpoint, emailVerificationStartEndpoint, emailVerificationVerifyEndpoint, loginApprovalApproveEndpoint, loginApprovalFinalizeEndpoint, loginApprovalStatusEndpoint, loginNotificationReportEndpoint, jwtIntrospectEndpoint, jwtRevokeEndpoint, listSessionsEndpoint, logoutEndpoint, magicLinkStartEndpoint, magicLinkVerifyEndpoint, mfaOptionsEndpoint, oauthAccountUnlinkEndpoint, oauthAuthorizationStartEndpoint, passwordChangeEndpoint, passwordResetStartEndpoint, passwordResetVerifyEndpoint, passwordSetEndpoint, passwordSignInEndpoint, passwordSignUpEndpoint, passkeyMfaStartEndpoint, passkeyMfaVerifyEndpoint, recoveryCodeMfaVerifyEndpoint, recoveryCodeMfaVerifyForFlowEndpoint, refreshTokenRefreshEndpoint, refreshSessionEndpoint, revokeOtherSessionsEndpoint, revokeSessionEndpoint, totpMfaVerifyEndpoint, totpMfaVerifyForFlowEndpoint, } from "./Endpoints.js";
18
+ import { AuthBadRequestError, AuthEmailAlreadyRegisteredError, AuthInternalError, AuthInvalidCredentialsError, AuthPolicyDeniedError, AuthRateLimitedError, AuthSchemaErrorMiddleware, AuthSchemaErrorMiddlewareLive, AuthUnauthenticatedError, } from "./Errors.js";
19
+ import { AuthApiRateLimit, AuthApiRateLimitLive, AuthApiRateLimitNoopLive, AuthHttpApiConfig, AuthOriginCheckMiddleware, AuthOriginCheckMiddlewareConfigLive, AuthRateLimitMiddleware, makeAuthRequestMetadata, } from "./Security.js";
20
+ import { AuthHttp, LoginApprovalPendingCookie, makeLoginApprovalPendingCookie, } from "./Transport.js";
12
21
  const canStartRequiredEmailVerification = (result) => result._tag === "RequiresEmailVerification" &&
13
22
  result.userId !== undefined &&
14
23
  result.email !== undefined;
@@ -16,15 +25,43 @@ const noopAuthApiRateLimit = AuthApiRateLimit.make({
16
25
  request: () => Effect.void,
17
26
  passwordSignIn: () => Effect.void,
18
27
  passwordSignUp: () => Effect.void,
28
+ passwordResetStart: () => Effect.void,
29
+ emailOtpStart: () => Effect.void,
30
+ magicLinkStart: () => Effect.void,
19
31
  emailVerificationStart: () => Effect.void,
20
32
  });
21
- export const passwordSignInInput = (payload) => ({
33
+ export const passwordSignInInput = (payload, trustedDeviceToken, request) => ({
22
34
  ...payload,
23
35
  password: Redacted.make(payload.password),
36
+ ...(trustedDeviceToken === undefined ? {} : { trustedDeviceToken }),
37
+ ...(request === undefined ? {} : { request }),
24
38
  });
25
- export const passwordSignUpInput = (payload) => ({
39
+ export const passwordSignUpInput = (payload, request) => ({
26
40
  ...payload,
27
41
  password: Redacted.make(payload.password),
42
+ ...(request === undefined ? {} : { request }),
43
+ });
44
+ export const passwordResetStartInput = (payload) => {
45
+ const { secret, ...input } = payload;
46
+ return {
47
+ ...input,
48
+ ...(secret === undefined ? {} : { secret: Redacted.make(secret) }),
49
+ };
50
+ };
51
+ export const passwordResetVerifyInput = (payload) => ({
52
+ challengeId: payload.challengeId,
53
+ secret: Redacted.make(payload.secret),
54
+ password: Redacted.make(payload.password),
55
+ });
56
+ export const passwordSetInput = (userId, payload) => ({
57
+ userId,
58
+ password: Redacted.make(payload.password),
59
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
60
+ });
61
+ export const passwordChangeInput = (userId, payload) => ({
62
+ userId,
63
+ currentPassword: Redacted.make(payload.currentPassword),
64
+ newPassword: Redacted.make(payload.newPassword),
28
65
  });
29
66
  export const emailVerificationStartInput = (payload) => {
30
67
  const { secret, ...input } = payload;
@@ -40,7 +77,549 @@ export const emailVerificationVerifyInput = (payload) => {
40
77
  ...(secret === undefined ? {} : { secret: Redacted.make(secret) }),
41
78
  };
42
79
  };
80
+ export const emailOtpStartInput = (payload) => {
81
+ const { secret, ...input } = payload;
82
+ return {
83
+ ...input,
84
+ ...(secret === undefined ? {} : { secret: Redacted.make(secret) }),
85
+ };
86
+ };
87
+ export const emailOtpVerifyInput = (payload, trustedDeviceToken, request) => ({
88
+ challengeId: payload.challengeId,
89
+ secret: Redacted.make(payload.secret),
90
+ ...(trustedDeviceToken === undefined ? {} : { trustedDeviceToken }),
91
+ ...(request === undefined ? {} : { request }),
92
+ });
93
+ export const magicLinkStartInput = (payload) => {
94
+ const { secret, ...input } = payload;
95
+ return {
96
+ ...input,
97
+ ...(secret === undefined ? {} : { secret: Redacted.make(secret) }),
98
+ };
99
+ };
100
+ export const magicLinkVerifyInput = (payload, trustedDeviceToken, request) => ({
101
+ challengeId: payload.challengeId,
102
+ secret: Redacted.make(payload.secret),
103
+ ...(trustedDeviceToken === undefined ? {} : { trustedDeviceToken }),
104
+ ...(request === undefined ? {} : { request }),
105
+ });
106
+ export const oauthAuthorizationStartInput = (payload, provider, options = {}) => ({
107
+ provider,
108
+ ...(options.ttl === undefined ? {} : { ttl: options.ttl }),
109
+ ...(payload.redirectUri === undefined ? {} : { redirectUri: payload.redirectUri }),
110
+ ...(payload.scopes === undefined ? {} : { scopes: payload.scopes }),
111
+ ...(payload.authorizationParams === undefined
112
+ ? {}
113
+ : { authorizationParams: payload.authorizationParams }),
114
+ ...(payload.codeVerifier === undefined
115
+ ? {}
116
+ : { codeVerifier: Redacted.make(payload.codeVerifier) }),
117
+ ...(payload.includeNonce === undefined
118
+ ? {}
119
+ : { includeNonce: payload.includeNonce }),
120
+ ...(payload.nonce === undefined ? {} : { nonce: payload.nonce }),
121
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
122
+ });
123
+ export const oauthAuthorizationStartedHttpBody = (started) => ({
124
+ providerId: started.providerId,
125
+ authorizationUrl: started.authorizationUrl,
126
+ state: started.state,
127
+ challengeId: started.challengeId,
128
+ expiresAt: started.expiresAt,
129
+ codeChallenge: started.codeChallenge,
130
+ codeChallengeMethod: started.codeChallengeMethod,
131
+ ...(started.nonce === undefined ? {} : { nonce: started.nonce }),
132
+ redirectUri: started.redirectUri,
133
+ scopes: started.scopes,
134
+ });
135
+ const oauthAuthorizationCallbackField = (fields, name) => {
136
+ const maybeSearchParams = fields;
137
+ const value = typeof maybeSearchParams.get === "function"
138
+ ? maybeSearchParams.get(name)
139
+ : fields[name];
140
+ const stringValue = Array.isArray(value) ? value[0] : value;
141
+ return typeof stringValue === "string" && stringValue.trim() !== ""
142
+ ? stringValue
143
+ : undefined;
144
+ };
145
+ const oauthAuthorizationCallbackPayload = (input, options) => Effect.gen(function* () {
146
+ const state = oauthAuthorizationCallbackField(input.fields, "state");
147
+ const code = oauthAuthorizationCallbackField(input.fields, "code");
148
+ const error = oauthAuthorizationCallbackField(input.fields, "error");
149
+ const errorDescription = oauthAuthorizationCallbackField(input.fields, "error_description");
150
+ const errorUri = oauthAuthorizationCallbackField(input.fields, "error_uri");
151
+ if (state === undefined) {
152
+ return yield* oauthBadRequest("OAuth callback is missing state");
153
+ }
154
+ const authorizationUser = options.includeAuthorizationUser
155
+ ? oauthAuthorizationCallbackField(input.fields, "user")
156
+ : undefined;
157
+ return {
158
+ providerId: input.providerId,
159
+ state,
160
+ ...(code === undefined ? {} : { code }),
161
+ ...(error === undefined ? {} : { error }),
162
+ ...(errorDescription === undefined ? {} : { errorDescription }),
163
+ ...(errorUri === undefined ? {} : { errorUri }),
164
+ ...(authorizationUser === undefined ? {} : { authorizationUser }),
165
+ };
166
+ });
167
+ export const oauthAuthorizationCallbackQueryPayload = (input) => oauthAuthorizationCallbackPayload(input, { includeAuthorizationUser: false });
168
+ export const oauthAuthorizationCallbackFormPostPayload = (input) => oauthAuthorizationCallbackPayload(input, { includeAuthorizationUser: true });
169
+ export const oauthAuthorizationCallbackInput = (payload, options) => Effect.gen(function* () {
170
+ if (payload.providerId !== options.provider.id) {
171
+ return yield* oauthBadRequest("OAuth callback provider mismatch");
172
+ }
173
+ if (payload.error !== undefined) {
174
+ return yield* oauthBadRequest("OAuth provider returned an error");
175
+ }
176
+ if (payload.code === undefined || payload.code.trim() === "") {
177
+ return yield* oauthBadRequest("OAuth callback is missing code");
178
+ }
179
+ return {
180
+ provider: options.provider,
181
+ state: payload.state,
182
+ code: payload.code,
183
+ codeVerifier: options.codeVerifier,
184
+ ...(options.redirectUri === undefined
185
+ ? {}
186
+ : { redirectUri: options.redirectUri }),
187
+ ...(options.clientAuthentication === undefined
188
+ ? {}
189
+ : { clientAuthentication: options.clientAuthentication }),
190
+ ...(options.tokenParams === undefined
191
+ ? {}
192
+ : { tokenParams: options.tokenParams }),
193
+ };
194
+ });
195
+ const oauthLinkedAccountHttpBody = (account) => ({
196
+ accountId: account.id,
197
+ providerId: account.providerId,
198
+ providerAccountId: account.providerAccountId,
199
+ });
200
+ export const oauthAccountUnlinkResolutionHttpBody = (resolution, options = {}) => {
201
+ switch (resolution.type) {
202
+ case "allow": {
203
+ return {
204
+ type: resolution.type,
205
+ reason: resolution.reason,
206
+ account: oauthLinkedAccountHttpBody(resolution.account),
207
+ remainingAccounts: resolution.remainingAccounts.map(oauthLinkedAccountHttpBody),
208
+ unlinked: options.unlinked === true,
209
+ };
210
+ }
211
+ case "requires-step-up": {
212
+ return {
213
+ type: resolution.type,
214
+ reason: resolution.reason,
215
+ account: oauthLinkedAccountHttpBody(resolution.account),
216
+ remainingAccounts: resolution.remainingAccounts.map(oauthLinkedAccountHttpBody),
217
+ };
218
+ }
219
+ case "deny-last-account": {
220
+ return {
221
+ type: resolution.type,
222
+ reason: resolution.reason,
223
+ account: oauthLinkedAccountHttpBody(resolution.account),
224
+ };
225
+ }
226
+ case "deny-not-owner": {
227
+ return {
228
+ type: resolution.type,
229
+ reason: resolution.reason,
230
+ providerId: resolution.account.providerId,
231
+ providerAccountId: resolution.account.providerAccountId,
232
+ };
233
+ }
234
+ case "deny-not-found": {
235
+ return {
236
+ type: resolution.type,
237
+ reason: resolution.reason,
238
+ providerId: resolution.providerId,
239
+ providerAccountId: resolution.providerAccountId,
240
+ };
241
+ }
242
+ }
243
+ };
244
+ export const passkeyRegistrationStartInput = (payload, options) => ({
245
+ relyingParty: options.relyingParty,
246
+ userId: payload.userId,
247
+ userName: payload.userName,
248
+ userDisplayName: payload.userDisplayName,
249
+ ...(options.timeout === undefined ? {} : { timeout: options.timeout }),
250
+ ...(options.pubKeyCredParams === undefined
251
+ ? {}
252
+ : { pubKeyCredParams: options.pubKeyCredParams }),
253
+ ...(options.authenticatorSelection === undefined
254
+ ? {}
255
+ : { authenticatorSelection: options.authenticatorSelection }),
256
+ ...(options.attestation === undefined ? {} : { attestation: options.attestation }),
257
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
258
+ });
259
+ export const passkeyAuthenticationStartInput = (payload, options) => ({
260
+ relyingPartyId: options.relyingPartyId,
261
+ ...(payload.userId === undefined ? {} : { userId: payload.userId }),
262
+ ...(options.timeout === undefined ? {} : { timeout: options.timeout }),
263
+ ...(options.userVerification === undefined
264
+ ? {}
265
+ : { userVerification: options.userVerification }),
266
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
267
+ });
268
+ export const passkeyMfaStartInput = (userId, payload, options) => ({
269
+ relyingPartyId: options.relyingPartyId,
270
+ userId,
271
+ ...(options.timeout === undefined ? {} : { timeout: options.timeout }),
272
+ userVerification: options.userVerification ?? "required",
273
+ metadata: {
274
+ ...payload.metadata,
275
+ flowId: payload.flowId,
276
+ },
277
+ });
278
+ export const passkeyRegistrationFinishInput = (payload, options) => ({
279
+ challengeId: payload.challengeId,
280
+ userId: payload.userId,
281
+ response: payload.credential,
282
+ relyingPartyId: options.relyingPartyId,
283
+ expectedOrigin: options.expectedOrigin,
284
+ ...(options.requireUserVerification === undefined
285
+ ? {}
286
+ : { requireUserVerification: options.requireUserVerification }),
287
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
288
+ });
289
+ export const passkeyAuthenticationFinishInput = (payload, options) => ({
290
+ challengeId: payload.challengeId,
291
+ response: payload.credential,
292
+ ...(payload.userId === undefined ? {} : { userId: payload.userId }),
293
+ relyingPartyId: options.relyingPartyId,
294
+ expectedOrigin: options.expectedOrigin,
295
+ ...(options.requireUserVerification === undefined
296
+ ? {}
297
+ : { requireUserVerification: options.requireUserVerification }),
298
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
299
+ });
300
+ export const passkeyMfaVerifyInput = (payload, options) => ({
301
+ challengeId: payload.challengeId,
302
+ response: payload.credential,
303
+ ...(payload.userId === undefined ? {} : { userId: payload.userId }),
304
+ relyingPartyId: options.relyingPartyId,
305
+ expectedOrigin: options.expectedOrigin,
306
+ ...(options.requireUserVerification === undefined
307
+ ? {}
308
+ : { requireUserVerification: options.requireUserVerification }),
309
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
310
+ });
311
+ export const passkeyCredentialRevokeInput = (userId, payload) => ({
312
+ userId,
313
+ credentialId: PasskeyCredentialId(payload.credentialId),
314
+ ...(payload.reason === undefined ? {} : { reason: payload.reason }),
315
+ });
316
+ const passkeyCredentialInfoHttpBody = (credential) => ({
317
+ credentialId: credential.credentialId,
318
+ ...(credential.transports === undefined
319
+ ? {}
320
+ : { transports: credential.transports }),
321
+ ...(credential.backedUp === undefined ? {} : { backedUp: credential.backedUp }),
322
+ createdAt: credential.createdAt,
323
+ ...(credential.lastUsedAt === undefined
324
+ ? {}
325
+ : { lastUsedAt: credential.lastUsedAt }),
326
+ ...(credential.revokedAt === undefined
327
+ ? {}
328
+ : { revokedAt: credential.revokedAt }),
329
+ ...(credential.metadata === undefined ? {} : { metadata: credential.metadata }),
330
+ });
331
+ export const totpEnrollmentStartInput = (userId, payload, options) => ({
332
+ userId,
333
+ issuer: options.issuer,
334
+ accountName: payload.accountName,
335
+ ...(options.secretBytes === undefined ? {} : { secretBytes: options.secretBytes }),
336
+ ...(options.algorithm === undefined ? {} : { algorithm: options.algorithm }),
337
+ ...(options.digits === undefined ? {} : { digits: options.digits }),
338
+ ...(options.period === undefined ? {} : { period: options.period }),
339
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
340
+ });
341
+ export const totpEnrollmentConfirmInput = (userId, payload, options = {}) => ({
342
+ userId,
343
+ factorId: payload.factorId,
344
+ code: Redacted.make(payload.code),
345
+ ...(options.window === undefined ? {} : { window: options.window }),
346
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
347
+ });
348
+ export const totpVerifyInput = (userId, payload, options = {}) => ({
349
+ userId,
350
+ code: Redacted.make(payload.code),
351
+ ...(options.window === undefined ? {} : { window: options.window }),
352
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
353
+ });
354
+ export const totpFactorRevokeInput = (userId, payload) => ({
355
+ userId,
356
+ factorId: payload.factorId,
357
+ ...(payload.reason === undefined ? {} : { reason: payload.reason }),
358
+ });
359
+ const totpFactorInfoHttpBody = (factor) => ({
360
+ factorId: factor.id,
361
+ algorithm: factor.algorithm,
362
+ digits: factor.digits,
363
+ period: factor.period,
364
+ createdAt: factor.createdAt,
365
+ ...(factor.confirmedAt === undefined ? {} : { confirmedAt: factor.confirmedAt }),
366
+ ...(factor.lastUsedAt === undefined ? {} : { lastUsedAt: factor.lastUsedAt }),
367
+ ...(factor.revokedAt === undefined ? {} : { revokedAt: factor.revokedAt }),
368
+ ...(factor.metadata === undefined ? {} : { metadata: factor.metadata }),
369
+ });
370
+ const totpEnrollmentStartedHttpBody = (started) => ({
371
+ factor: totpFactorInfoHttpBody(started.factor),
372
+ secret: Redacted.value(started.secret),
373
+ uri: Redacted.value(started.uri),
374
+ });
375
+ const totpVerifyResultHttpBody = (result) => ({
376
+ valid: result.valid,
377
+ ...(result.factor === undefined
378
+ ? {}
379
+ : { factor: totpFactorInfoHttpBody(result.factor) }),
380
+ ...(result.delta === undefined ? {} : { delta: result.delta }),
381
+ });
382
+ export const recoveryCodeGenerateInput = (userId, payload, options = {}) => ({
383
+ userId,
384
+ ...(options.count === undefined ? {} : { count: options.count }),
385
+ ...(options.length === undefined ? {} : { length: options.length }),
386
+ ...(options.groupSize === undefined ? {} : { groupSize: options.groupSize }),
387
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
388
+ });
389
+ export const recoveryCodeVerifyInput = (userId, payload) => ({
390
+ userId,
391
+ code: Redacted.make(payload.code),
392
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
393
+ });
394
+ export const recoveryCodeRevokeInput = (userId, payload) => ({
395
+ userId,
396
+ codeId: payload.codeId,
397
+ ...(payload.reason === undefined ? {} : { reason: payload.reason }),
398
+ });
399
+ const recoveryCodeInfoHttpBody = (code) => ({
400
+ codeId: code.id,
401
+ createdAt: code.createdAt,
402
+ ...(code.usedAt === undefined ? {} : { usedAt: code.usedAt }),
403
+ ...(code.revokedAt === undefined ? {} : { revokedAt: code.revokedAt }),
404
+ ...(code.metadata === undefined ? {} : { metadata: code.metadata }),
405
+ });
406
+ const recoveryCodesGeneratedHttpBody = (generated) => ({
407
+ codes: generated.codes.map(Redacted.value),
408
+ records: generated.records.map(recoveryCodeInfoHttpBody),
409
+ });
410
+ const recoveryCodeVerifyResultHttpBody = (result) => ({
411
+ valid: result.valid,
412
+ ...(result.code === undefined
413
+ ? {}
414
+ : { code: recoveryCodeInfoHttpBody(result.code) }),
415
+ });
416
+ export const apiKeyCreateInput = (userId, payload) => ({
417
+ userId,
418
+ ...(payload.scopes === undefined ? {} : { scopes: payload.scopes }),
419
+ ...(payload.expiresAt === undefined ? {} : { expiresAt: payload.expiresAt }),
420
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
421
+ });
422
+ export const apiKeyRevokeInput = (userId, payload) => ({
423
+ userId,
424
+ keyId: ApiKeyId(payload.keyId),
425
+ ...(payload.reason === undefined ? {} : { reason: payload.reason }),
426
+ });
427
+ const apiKeyInfoHttpBody = (key) => ({
428
+ keyId: key.id,
429
+ prefix: key.prefix,
430
+ scopes: key.scopes,
431
+ createdAt: key.createdAt,
432
+ ...(key.expiresAt === undefined ? {} : { expiresAt: key.expiresAt }),
433
+ ...(key.lastUsedAt === undefined ? {} : { lastUsedAt: key.lastUsedAt }),
434
+ ...(key.revokedAt === undefined ? {} : { revokedAt: key.revokedAt }),
435
+ ...(key.metadata === undefined ? {} : { metadata: key.metadata }),
436
+ });
437
+ const apiKeyCreatedHttpBody = (created) => ({
438
+ secret: Redacted.value(created.secret),
439
+ key: apiKeyInfoHttpBody(created.key),
440
+ });
441
+ export const refreshTokenRefreshInput = (payload, now, expiresAt) => ({
442
+ token: Redacted.make(payload.refreshToken),
443
+ now,
444
+ expiresAt,
445
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
446
+ });
447
+ const refreshTokenInfoHttpBody = (refreshToken) => ({
448
+ tokenId: refreshToken.id,
449
+ familyId: refreshToken.familyId,
450
+ createdAt: refreshToken.createdAt,
451
+ expiresAt: refreshToken.expiresAt,
452
+ ...(refreshToken.lastUsedAt === undefined
453
+ ? {}
454
+ : { lastUsedAt: refreshToken.lastUsedAt }),
455
+ ...(refreshToken.rotatedAt === undefined
456
+ ? {}
457
+ : { rotatedAt: refreshToken.rotatedAt }),
458
+ ...(refreshToken.replacedById === undefined
459
+ ? {}
460
+ : { replacedById: refreshToken.replacedById }),
461
+ ...(refreshToken.revokedAt === undefined
462
+ ? {}
463
+ : { revokedAt: refreshToken.revokedAt }),
464
+ ...(refreshToken.reuseDetectedAt === undefined
465
+ ? {}
466
+ : { reuseDetectedAt: refreshToken.reuseDetectedAt }),
467
+ ...(refreshToken.metadata === undefined ? {} : { metadata: refreshToken.metadata }),
468
+ });
469
+ const issuedJwtExpiresAt = (issued) => typeof issued.claims.exp === "number"
470
+ ? (issued.claims.exp * 1000)
471
+ : undefined;
472
+ const refreshTokenRefreshedHttpBody = (input) => ({
473
+ accessToken: Redacted.value(input.accessToken.token),
474
+ ...(issuedJwtExpiresAt(input.accessToken) === undefined
475
+ ? {}
476
+ : { accessTokenExpiresAt: issuedJwtExpiresAt(input.accessToken) }),
477
+ refreshToken: input.refreshToken,
478
+ refreshTokenRecord: refreshTokenInfoHttpBody(input.refreshTokenRecord),
479
+ });
480
+ export const jwtIntrospectInput = (payload, options = {}) => ({
481
+ token: Redacted.make(payload.token),
482
+ ...(options.issuer === undefined ? {} : { issuer: options.issuer }),
483
+ ...(options.audience === undefined ? {} : { audience: options.audience }),
484
+ ...(options.clockTolerance === undefined
485
+ ? {}
486
+ : { clockTolerance: options.clockTolerance }),
487
+ });
488
+ export const jwtRevokeInput = (payload) => ({
489
+ jwtId: payload.jwtId,
490
+ ...(payload.expiresAt === undefined ? {} : { expiresAt: payload.expiresAt }),
491
+ ...(payload.reason === undefined ? {} : { reason: payload.reason }),
492
+ });
493
+ const jwtKeyInfoHttpBody = (result) => ({
494
+ keyId: result.key.id,
495
+ alg: result.key.alg,
496
+ status: result.key.status,
497
+ ...(result.key.createdAt === undefined ? {} : { createdAt: result.key.createdAt }),
498
+ ...(result.key.expiresAt === undefined ? {} : { expiresAt: result.key.expiresAt }),
499
+ ...(result.key.metadata === undefined ? {} : { metadata: result.key.metadata }),
500
+ });
501
+ const jwtIntrospectionHttpBody = (result) => result.active
502
+ ? {
503
+ active: true,
504
+ header: result.header,
505
+ claims: result.claims,
506
+ key: jwtKeyInfoHttpBody(result),
507
+ ...(result.jwtId === undefined ? {} : { jwtId: result.jwtId }),
508
+ ...(result.expiresAt === undefined ? {} : { expiresAt: result.expiresAt }),
509
+ revoked: false,
510
+ }
511
+ : {
512
+ active: false,
513
+ reason: result.reason,
514
+ ...(result.jwtId === undefined ? {} : { jwtId: result.jwtId }),
515
+ ...(result.expiresAt === undefined ? {} : { expiresAt: result.expiresAt }),
516
+ ...(result.revokedAt === undefined ? {} : { revokedAt: result.revokedAt }),
517
+ ...(result.revokedReason === undefined
518
+ ? {}
519
+ : { revokedReason: result.revokedReason }),
520
+ };
521
+ export const totpMfaVerifyInput = (payload, options = {}) => ({
522
+ userId: payload.userId,
523
+ code: Redacted.make(payload.code),
524
+ ...(options.window === undefined ? {} : { window: options.window }),
525
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
526
+ });
527
+ export const totpMfaVerifyForFlowInput = (userId, payload, options = {}) => ({
528
+ userId,
529
+ code: Redacted.make(payload.code),
530
+ ...(options.window === undefined ? {} : { window: options.window }),
531
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
532
+ });
533
+ export const recoveryCodeMfaVerifyInput = (payload) => ({
534
+ userId: payload.userId,
535
+ code: Redacted.make(payload.code),
536
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
537
+ });
538
+ export const recoveryCodeMfaVerifyForFlowInput = (userId, payload) => ({
539
+ userId,
540
+ code: Redacted.make(payload.code),
541
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
542
+ });
543
+ export const loginApprovalApproveInput = (payload) => ({
544
+ flowId: payload.flowId,
545
+ approval: {
546
+ challengeId: payload.approvalChallengeId,
547
+ ...(payload.secret === undefined
548
+ ? {}
549
+ : { secret: Redacted.make(payload.secret) }),
550
+ },
551
+ rememberDevice: payload.rememberDevice ?? false,
552
+ });
553
+ export const loginApprovalStatusInput = (payload) => ({
554
+ flowId: payload.flowId,
555
+ approvalChallengeId: payload.approvalChallengeId,
556
+ });
557
+ export const loginApprovalFinalizeInput = (payload) => ({
558
+ flowId: payload.flowId,
559
+ approvalChallengeId: payload.approvalChallengeId,
560
+ rememberDevice: payload.rememberDevice ?? false,
561
+ });
562
+ export const loginNotificationReportInput = (payload) => ({
563
+ challengeId: payload.challengeId,
564
+ secret: Redacted.make(payload.secret),
565
+ });
43
566
  const webRequestFromHeaders = (headers) => new Request("http://localhost", { headers });
567
+ const firstHeaderValue = (value) => {
568
+ const first = value?.split(",", 1)[0]?.trim();
569
+ return first === "" ? undefined : first;
570
+ };
571
+ const headerNumber = (value) => {
572
+ const parsed = Number(firstHeaderValue(value));
573
+ return Number.isFinite(parsed) ? parsed : undefined;
574
+ };
575
+ const authRequestMetadataOptions = Effect.gen(function* () {
576
+ const config = yield* Effect.serviceOption(AuthHttpApiConfig);
577
+ return Option.isSome(config) ? (config.value.requestMetadata ?? {}) : {};
578
+ });
579
+ const loginRequestContextFromRequest = (request, options) => {
580
+ if (request === undefined) {
581
+ return undefined;
582
+ }
583
+ const metadata = makeAuthRequestMetadata(request, options);
584
+ const trustProxyHeaders = options.trustProxyHeaders ?? false;
585
+ const { headers } = request;
586
+ const country = trustProxyHeaders
587
+ ? firstHeaderValue(headers["cf-ipcountry"])
588
+ : undefined;
589
+ const region = trustProxyHeaders
590
+ ? firstHeaderValue(headers["cf-region"])
591
+ : undefined;
592
+ const city = trustProxyHeaders
593
+ ? firstHeaderValue(headers["cf-ipcity"])
594
+ : undefined;
595
+ const latitude = trustProxyHeaders
596
+ ? headerNumber(headers["cf-iplatitude"])
597
+ : undefined;
598
+ const longitude = trustProxyHeaders
599
+ ? headerNumber(headers["cf-iplongitude"])
600
+ : undefined;
601
+ const context = {
602
+ ...(metadata.ipAddress === undefined
603
+ ? {}
604
+ : { ip: String(metadata.ipAddress) }),
605
+ ...(metadata.userAgent === undefined
606
+ ? {}
607
+ : { userAgent: metadata.userAgent }),
608
+ ...(country === undefined ? {} : { country }),
609
+ ...(region === undefined ? {} : { region }),
610
+ ...(city === undefined ? {} : { city }),
611
+ ...(latitude === undefined ? {} : { latitude }),
612
+ ...(longitude === undefined ? {} : { longitude }),
613
+ };
614
+ return Object.keys(context).length === 0 ? undefined : context;
615
+ };
616
+ const trustedDeviceTokenFromRequest = (trustedDeviceCookie, request) => trustedDeviceCookie === undefined || request === undefined
617
+ ? Effect.succeed(Option.none())
618
+ : trustedDeviceCookie.read(new Request("http://localhost", { headers: request.headers }));
619
+ const metadataString = (metadata, key) => {
620
+ const value = metadata?.[key];
621
+ return typeof value === "string" && value.length > 0 ? value : undefined;
622
+ };
44
623
  const emailVerifiedClaims = (claims) => {
45
624
  const requirements = claims?.requirements?.filter((requirement) => requirement !== "email_verification");
46
625
  return {
@@ -49,6 +628,50 @@ const emailVerifiedClaims = (claims) => {
49
628
  ...(requirements === undefined ? {} : { requirements }),
50
629
  };
51
630
  };
631
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
632
+ const metadataNumber = (metadata, key) => {
633
+ const value = metadata?.[key];
634
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
635
+ };
636
+ const sessionRequestMetadata = (metadata) => {
637
+ const value = metadata?.[sessionRequestMetadataKey];
638
+ return isRecord(value) ? value : undefined;
639
+ };
640
+ const activeSessionBody = (session, currentSessionId) => {
641
+ const request = sessionRequestMetadata(session.metadata);
642
+ const ip = metadataString(request, "ip");
643
+ const userAgent = metadataString(request, "userAgent");
644
+ const country = metadataString(request, "country");
645
+ const region = metadataString(request, "region");
646
+ const city = metadataString(request, "city");
647
+ const latitude = metadataNumber(request, "latitude");
648
+ const longitude = metadataNumber(request, "longitude");
649
+ return {
650
+ sessionId: session.sessionId,
651
+ userId: session.userId,
652
+ createdAt: session.createdAt,
653
+ authTime: session.authTime,
654
+ ...(session.lastSeenAt === undefined
655
+ ? {}
656
+ : { lastSeenAt: session.lastSeenAt }),
657
+ expiresAt: session.expiresAt,
658
+ aal: session.aal,
659
+ amr: session.amr,
660
+ ...(session.mfaVerifiedAt === undefined
661
+ ? {}
662
+ : { mfaVerifiedAt: session.mfaVerifiedAt }),
663
+ current: session.sessionId === currentSessionId,
664
+ ...(session.claims === undefined ? {} : { claims: session.claims }),
665
+ ...(ip === undefined ? {} : { ip }),
666
+ ...(userAgent === undefined ? {} : { userAgent }),
667
+ ...(country === undefined ? {} : { country }),
668
+ ...(region === undefined ? {} : { region }),
669
+ ...(city === undefined ? {} : { city }),
670
+ ...(latitude === undefined ? {} : { latitude }),
671
+ ...(longitude === undefined ? {} : { longitude }),
672
+ ...(session.metadata === undefined ? {} : { metadata: session.metadata }),
673
+ };
674
+ };
52
675
  const readOptionalSession = (dependencies, request) => Effect.gen(function* () {
53
676
  const tokenOption = yield* dependencies.sessionCookie
54
677
  .read(webRequestFromHeaders(request.headers))
@@ -68,6 +691,12 @@ const readOptionalSession = (dependencies, request) => Effect.gen(function* () {
68
691
  message: "Failed to validate session",
69
692
  }))));
70
693
  });
694
+ const readRequiredSession = (dependencies, request) => readOptionalSession(dependencies, request).pipe(Effect.flatMap((sessionOption) => Option.isSome(sessionOption)
695
+ ? Effect.succeed(sessionOption.value)
696
+ : Effect.fail(new AuthUnauthenticatedError({
697
+ code: "unauthenticated",
698
+ message: "Unauthenticated",
699
+ }))));
71
700
  const upgradeVerifiedEmailSession = (dependencies, request, verified) => Effect.gen(function* () {
72
701
  const sessionOption = yield* readOptionalSession(dependencies, request);
73
702
  if (Option.isNone(sessionOption)) {
@@ -115,28 +744,240 @@ const passwordInternalError = (message) => new AuthInternalError({
115
744
  code: "internal_error",
116
745
  message,
117
746
  });
747
+ const loginApprovalBadRequest = () => new AuthBadRequestError({
748
+ code: "bad_request",
749
+ message: "Invalid login approval request",
750
+ });
751
+ const loginApprovalSameDeviceDenied = () => new AuthPolicyDeniedError({
752
+ code: "policy_denied",
753
+ message: "Login approval must be completed on the same device",
754
+ });
755
+ const loginApprovalNoSessionBindingDenied = () => new AuthPolicyDeniedError({
756
+ code: "policy_denied",
757
+ message: "Login approval does not create a user session",
758
+ });
759
+ const loginApprovalReviewNotConfigured = () => new AuthPolicyDeniedError({
760
+ code: "policy_denied",
761
+ message: "Login approval review is not configured",
762
+ });
763
+ const loginApprovalFinalizeNotConfigured = () => new AuthPolicyDeniedError({
764
+ code: "policy_denied",
765
+ message: "Login approval finalization is not configured",
766
+ });
767
+ const loginApprovalPolicyDenied = (reason) => new AuthPolicyDeniedError({
768
+ code: "policy_denied",
769
+ message: reason ?? "Login approval denied",
770
+ });
771
+ const loginNotificationNotConfigured = () => new AuthPolicyDeniedError({
772
+ code: "policy_denied",
773
+ message: "Login notification reporting is not configured",
774
+ });
775
+ const loginNotificationBadRequest = () => new AuthBadRequestError({
776
+ code: "bad_request",
777
+ message: "Invalid login notification report",
778
+ });
779
+ const oauthBadRequest = (message = "Invalid OAuth request") => new AuthBadRequestError({
780
+ code: "bad_request",
781
+ message,
782
+ });
783
+ const oauthInternalError = (message = "Failed to complete OAuth request") => new AuthInternalError({
784
+ code: "internal_error",
785
+ message,
786
+ });
787
+ const passkeyInternalError = (message = "Failed to complete passkey request") => new AuthInternalError({
788
+ code: "internal_error",
789
+ message,
790
+ });
791
+ const passkeyBadRequest = (message = "Invalid passkey request") => new AuthBadRequestError({
792
+ code: "bad_request",
793
+ message,
794
+ });
795
+ const passkeyVerificationHttpError = (error) => error.message === "Failed to load passkey credential" ||
796
+ error.message === "Failed to store passkey credential" ||
797
+ error.message === "Failed to update passkey credential" ||
798
+ error.message === "Failed to generate passkey credential record id"
799
+ ? passkeyInternalError()
800
+ : passkeyBadRequest();
801
+ const passkeyCredentialManagementHttpError = (error) => error.message === "Passkey credential not found"
802
+ ? passkeyBadRequest("Invalid passkey credential")
803
+ : passkeyInternalError("Failed to update passkey credentials");
804
+ const totpInternalError = (message = "Failed to complete TOTP request") => new AuthInternalError({
805
+ code: "internal_error",
806
+ message,
807
+ });
808
+ const totpBadRequest = (message = "Invalid TOTP request") => new AuthBadRequestError({
809
+ code: "bad_request",
810
+ message,
811
+ });
812
+ const totpFactorManagementHttpError = (error) => {
813
+ switch (error.message) {
814
+ case "Failed to build TOTP enrollment URI":
815
+ case "TOTP factor not found":
816
+ case "TOTP factor already confirmed":
817
+ case "Invalid TOTP code": {
818
+ return totpBadRequest();
819
+ }
820
+ default: {
821
+ return totpInternalError();
822
+ }
823
+ }
824
+ };
825
+ const recoveryCodeInternalError = (message = "Failed to complete recovery code request") => new AuthInternalError({
826
+ code: "internal_error",
827
+ message,
828
+ });
829
+ const recoveryCodeBadRequest = (message = "Invalid recovery code request") => new AuthBadRequestError({
830
+ code: "bad_request",
831
+ message,
832
+ });
833
+ const recoveryCodeManagementHttpError = (error) => {
834
+ switch (error.message) {
835
+ case "Recovery code not found": {
836
+ return recoveryCodeBadRequest();
837
+ }
838
+ default: {
839
+ return recoveryCodeInternalError();
840
+ }
841
+ }
842
+ };
843
+ const apiKeyInternalError = (message = "Failed to complete API key request") => new AuthInternalError({
844
+ code: "internal_error",
845
+ message,
846
+ });
847
+ const apiKeyBadRequest = (message = "Invalid API key request") => new AuthBadRequestError({
848
+ code: "bad_request",
849
+ message,
850
+ });
851
+ const apiKeyManagementHttpError = (error) => {
852
+ switch (error.message) {
853
+ case "API key not found": {
854
+ return apiKeyBadRequest();
855
+ }
856
+ default: {
857
+ return apiKeyInternalError();
858
+ }
859
+ }
860
+ };
861
+ const refreshTokenInternalError = (message = "Failed to complete refresh token request") => new AuthInternalError({
862
+ code: "internal_error",
863
+ message,
864
+ });
865
+ const refreshTokenInvalidCredentials = (message = "Invalid refresh token") => new AuthInvalidCredentialsError({
866
+ code: "invalid_credentials",
867
+ message,
868
+ });
869
+ const jwtInternalError = (message = "Failed to complete JWT request") => new AuthInternalError({
870
+ code: "internal_error",
871
+ message,
872
+ });
873
+ const jwtBadRequest = (message = "Invalid JWT request") => new AuthBadRequestError({
874
+ code: "bad_request",
875
+ message,
876
+ });
877
+ const jwtRevocationHttpError = (error) => error.message === "JWT ID must not be empty" ? jwtBadRequest() : jwtInternalError();
878
+ const mfaInternalError = (message = "Failed to complete MFA request") => new AuthInternalError({
879
+ code: "internal_error",
880
+ message,
881
+ });
882
+ const mfaBadRequest = (message = "Invalid MFA request") => new AuthBadRequestError({
883
+ code: "bad_request",
884
+ message,
885
+ });
886
+ const mfaInvalidCredentials = () => new AuthInvalidCredentialsError({
887
+ code: "invalid_credentials",
888
+ message: "Invalid MFA code",
889
+ });
890
+ const mfaPolicyDenied = (reason) => new AuthPolicyDeniedError({
891
+ code: "policy_denied",
892
+ message: reason ?? "MFA policy denied the request",
893
+ });
894
+ const authFlowMfaHttpError = (error) => error._tag === "AuthFlowStateError" ? mfaBadRequest() : mfaInternalError();
895
+ const trustedDeviceSetCookies = (dependencies) => Effect.gen(function* () {
896
+ if (!dependencies.rememberDevice ||
897
+ dependencies.trustedDevice === undefined ||
898
+ dependencies.trustedDeviceCookie === undefined) {
899
+ return [];
900
+ }
901
+ const token = yield* dependencies.trustedDevice
902
+ .trust({ userId: dependencies.userId })
903
+ .pipe(Effect.mapError(() => passwordInternalError("Failed to remember trusted device")));
904
+ const cookie = yield* dependencies.trustedDeviceCookie
905
+ .commit({ token })
906
+ .pipe(Effect.mapError(() => passwordInternalError("Failed to remember trusted device")));
907
+ return [cookie];
908
+ });
118
909
  const catchAuthApiRateLimit = (effect, internalMessage) => effect.pipe(Effect.catchTag("RateLimitExceededError", (error) => Effect.fail(rateLimited(error.retryAfter))), Effect.catchTag("RateLimitStoreError", () => Effect.fail(passwordInternalError(internalMessage))), Effect.catchTag("PrivacyError", () => Effect.fail(passwordInternalError(internalMessage))));
119
- export const makePasswordSignInHandler = (dependencies) => Effect.fn("auth.http.password.sign_in")(function* ({ payload, }) {
910
+ export const makePasswordSignInHandler = (dependencies) => Effect.fn("auth.http.password.sign_in")(function* ({ payload, request, }) {
120
911
  yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.sign_in");
121
- const input = passwordSignInInput(payload);
912
+ const trustedDeviceToken = yield* trustedDeviceTokenFromRequest(dependencies.trustedDeviceCookie, request).pipe(Effect.mapError(() => passwordInternalError("Failed to read trusted device cookie")));
913
+ const requestMetadataOptions = yield* authRequestMetadataOptions;
914
+ const requestContext = loginRequestContextFromRequest(request, requestMetadataOptions);
915
+ const input = passwordSignInInput(payload, Option.isSome(trustedDeviceToken) ? trustedDeviceToken.value : undefined, requestContext);
122
916
  const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
123
917
  yield* catchAuthApiRateLimit(authApiRateLimit.passwordSignIn(input), "Failed to sign in with password");
124
- const result = yield* dependencies.password.signIn(input).pipe(Effect.catchTag("StorageError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("PasswordHashError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("SessionCreateError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))));
918
+ const result = yield* dependencies.password.signIn(input).pipe(Effect.catchTag("StorageError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("PasswordHashError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("TrustedDeviceError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("LoginRiskError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("SessionCreateError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("AuthFlowStateError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("LoginApprovalReviewError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))), Effect.catchTag("LoginApprovalFlowStartError", () => Effect.fail(passwordInternalError("Failed to sign in with password"))));
125
919
  yield* startRequiredEmailVerification(dependencies, result);
126
920
  return yield* dependencies.authHttp.commitPasswordSignInResult(result);
127
921
  });
128
- export const makePasswordSignUpHandler = (dependencies) => Effect.fn("auth.http.password.sign_up")(function* ({ payload, }) {
922
+ export const makePasswordSignUpHandler = (dependencies) => Effect.fn("auth.http.password.sign_up")(function* ({ payload, request, }) {
129
923
  yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.sign_up");
130
- const input = passwordSignUpInput(payload);
924
+ const requestMetadataOptions = yield* authRequestMetadataOptions;
925
+ const input = passwordSignUpInput(payload, loginRequestContextFromRequest(request, requestMetadataOptions));
131
926
  const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
132
927
  yield* catchAuthApiRateLimit(authApiRateLimit.passwordSignUp(input), "Failed to sign up with password");
133
928
  const result = yield* dependencies.registration.signUp(input).pipe(Effect.catchTag("EmailAlreadyRegisteredError", () => Effect.fail(new AuthEmailAlreadyRegisteredError({
134
929
  code: "email_already_registered",
135
930
  message: "Email is already registered",
136
- }))), Effect.catchTag("StorageError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("PasswordHashError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("CryptoError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("SessionCreateError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))));
931
+ }))), Effect.catchTag("StorageError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("PasswordHashError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("CryptoError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("TrustedDeviceError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("LoginRiskError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("SessionCreateError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("AuthFlowStateError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("LoginApprovalReviewError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))), Effect.catchTag("LoginApprovalFlowStartError", () => Effect.fail(passwordInternalError("Failed to sign up with password"))));
137
932
  yield* startRequiredEmailVerification(dependencies, result);
138
933
  return yield* dependencies.authHttp.commitPasswordSignUpResult(result);
139
934
  });
935
+ export const makePasswordResetStartHandler = (dependencies) => Effect.fn("auth.http.password.reset_start")(function* ({ payload, }) {
936
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.reset_start");
937
+ const input = passwordResetStartInput(payload);
938
+ const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
939
+ yield* catchAuthApiRateLimit(authApiRateLimit.passwordResetStart(input), "Failed to start password reset");
940
+ yield* dependencies.passwordReset.start(input).pipe(Effect.mapError(() => new AuthInternalError({
941
+ code: "internal_error",
942
+ message: "Failed to start password reset",
943
+ })));
944
+ return HttpServerResponse.empty({ status: 204 });
945
+ });
946
+ export const makePasswordResetVerifyHandler = (dependencies) => Effect.fn("auth.http.password.reset_verify")(function* ({ payload, }) {
947
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.reset_verify");
948
+ yield* dependencies.passwordReset
949
+ .verify(passwordResetVerifyInput(payload))
950
+ .pipe(Effect.mapError(() => new AuthBadRequestError({
951
+ code: "bad_request",
952
+ message: "Invalid password reset request",
953
+ })));
954
+ return HttpServerResponse.empty({ status: 204 });
955
+ });
956
+ const passwordManagementError = (error) => error.reason === "internal"
957
+ ? new AuthInternalError({
958
+ code: "internal_error",
959
+ message: "Failed to update password",
960
+ })
961
+ : new AuthBadRequestError({
962
+ code: "bad_request",
963
+ message: "Invalid password update request",
964
+ });
965
+ export const makePasswordSetHandler = (dependencies) => Effect.fn("auth.http.password.set")(function* ({ payload, request, }) {
966
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.set");
967
+ const current = yield* readRequiredSession(dependencies, request);
968
+ yield* dependencies.passwordManagement
969
+ .set(passwordSetInput(current.currentSession.userId, payload))
970
+ .pipe(Effect.mapError(passwordManagementError));
971
+ return HttpServerResponse.empty({ status: 204 });
972
+ });
973
+ export const makePasswordChangeHandler = (dependencies) => Effect.fn("auth.http.password.change")(function* ({ payload, request, }) {
974
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.change");
975
+ const current = yield* readRequiredSession(dependencies, request);
976
+ yield* dependencies.passwordManagement
977
+ .change(passwordChangeInput(current.currentSession.userId, payload))
978
+ .pipe(Effect.mapError(passwordManagementError));
979
+ return HttpServerResponse.empty({ status: 204 });
980
+ });
140
981
  export const makeCurrentSessionHandler = (dependencies) => Effect.fn("auth.http.session.current")(function* ({ request, }) {
141
982
  yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.current");
142
983
  const tokenOption = yield* dependencies.sessionCookie
@@ -234,6 +1075,66 @@ export const makeLogoutHandler = (dependencies) => Effect.fn("auth.http.session.
234
1075
  }
235
1076
  return yield* dependencies.authHttp.commitSessionCleared();
236
1077
  });
1078
+ export const makeListSessionsHandler = (dependencies) => Effect.fn("auth.http.session.list")(function* ({ request, }) {
1079
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.list");
1080
+ const current = yield* readRequiredSession(dependencies, request);
1081
+ const sessions = yield* dependencies.sessions
1082
+ .listForUser({ userId: current.currentSession.userId })
1083
+ .pipe(Effect.mapError(() => new AuthInternalError({
1084
+ code: "internal_error",
1085
+ message: "Failed to list sessions",
1086
+ })));
1087
+ return {
1088
+ sessions: sessions.map((session) => activeSessionBody(session, current.currentSession.sessionId)),
1089
+ };
1090
+ });
1091
+ export const makeRevokeSessionHandler = (dependencies) => Effect.fn("auth.http.session.revoke")(function* ({ payload, request, }) {
1092
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.revoke");
1093
+ const current = yield* readRequiredSession(dependencies, request);
1094
+ yield* dependencies.sessions
1095
+ .revokeForUser({
1096
+ userId: current.currentSession.userId,
1097
+ sessionId: payload.sessionId,
1098
+ reason: "user_revoked_session",
1099
+ })
1100
+ .pipe(Effect.mapError(() => new AuthInternalError({
1101
+ code: "internal_error",
1102
+ message: "Failed to revoke session",
1103
+ })));
1104
+ return payload.sessionId === current.currentSession.sessionId
1105
+ ? yield* dependencies.authHttp.commitSessionCleared()
1106
+ : HttpServerResponse.empty({ status: 204 });
1107
+ });
1108
+ export const makeRevokeOtherSessionsHandler = (dependencies) => Effect.fn("auth.http.session.revoke_others")(function* ({ request, }) {
1109
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.revoke_others");
1110
+ const current = yield* readRequiredSession(dependencies, request);
1111
+ yield* dependencies.sessions
1112
+ .revokeAllForUserExcept({
1113
+ userId: current.currentSession.userId,
1114
+ exceptSessionId: current.currentSession.sessionId,
1115
+ reason: "user_revoked_other_sessions",
1116
+ })
1117
+ .pipe(Effect.mapError(() => new AuthInternalError({
1118
+ code: "internal_error",
1119
+ message: "Failed to revoke other sessions",
1120
+ })));
1121
+ return HttpServerResponse.empty({ status: 204 });
1122
+ });
1123
+ export const makeLoginNotificationReportHandler = (dependencies) => Effect.fn("auth.http.security.login_notification.report")(function* ({ payload, }) {
1124
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "security.login_notification.report");
1125
+ if (dependencies.loginNotification === undefined) {
1126
+ return yield* loginNotificationNotConfigured();
1127
+ }
1128
+ yield* dependencies.loginNotification
1129
+ .report(loginNotificationReportInput(payload))
1130
+ .pipe(Effect.mapError((error) => error.reason === "invalid_report"
1131
+ ? loginNotificationBadRequest()
1132
+ : new AuthInternalError({
1133
+ code: "internal_error",
1134
+ message: "Failed to process login notification report",
1135
+ })));
1136
+ return HttpServerResponse.empty({ status: 204 });
1137
+ });
237
1138
  export const makeEmailVerificationStartHandler = (dependencies) => Effect.fn("auth.http.email_verification.start")(function* ({ payload, }) {
238
1139
  yield* Effect.annotateCurrentSpan("auth.http.endpoint", "email_verification.start");
239
1140
  const input = emailVerificationStartInput(payload);
@@ -266,14 +1167,552 @@ export const makeEmailVerificationVerifyHandler = (dependencies) => Effect.fn("a
266
1167
  yield* upgradeVerifiedEmailSession(dependencies, request, verified);
267
1168
  return HttpServerResponse.empty({ status: 204 });
268
1169
  });
1170
+ export const makeEmailOtpStartHandler = (dependencies) => Effect.fn("auth.http.email_otp.start")(function* ({ payload, }) {
1171
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "email_otp.start");
1172
+ const input = emailOtpStartInput(payload);
1173
+ const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
1174
+ yield* catchAuthApiRateLimit(authApiRateLimit.emailOtpStart(input), "Failed to start email OTP");
1175
+ return yield* dependencies.emailOtp.start(input).pipe(Effect.mapError(() => new AuthInternalError({
1176
+ code: "internal_error",
1177
+ message: "Failed to start email OTP",
1178
+ })));
1179
+ });
1180
+ export const makeEmailOtpVerifyHandler = (dependencies) => Effect.fn("auth.http.email_otp.verify")(function* ({ payload, request, }) {
1181
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "email_otp.verify");
1182
+ const trustedDeviceToken = yield* trustedDeviceTokenFromRequest(dependencies.trustedDeviceCookie, request).pipe(Effect.mapError(() => passwordInternalError("Failed to read trusted device cookie")));
1183
+ const requestMetadataOptions = yield* authRequestMetadataOptions;
1184
+ const requestContext = loginRequestContextFromRequest(request, requestMetadataOptions);
1185
+ const result = yield* dependencies.emailOtp
1186
+ .verify(emailOtpVerifyInput(payload, Option.isSome(trustedDeviceToken)
1187
+ ? trustedDeviceToken.value
1188
+ : undefined, requestContext))
1189
+ .pipe(Effect.mapError(() => new AuthInternalError({
1190
+ code: "internal_error",
1191
+ message: "Failed to verify email OTP",
1192
+ })));
1193
+ return yield* dependencies.authHttp.commitEmailOtpVerifyResult(result);
1194
+ });
1195
+ export const makeMagicLinkStartHandler = (dependencies) => Effect.fn("auth.http.magic_link.start")(function* ({ payload, }) {
1196
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "magic_link.start");
1197
+ const input = magicLinkStartInput(payload);
1198
+ const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
1199
+ yield* catchAuthApiRateLimit(authApiRateLimit.magicLinkStart(input), "Failed to start magic link");
1200
+ const started = yield* dependencies.magicLink.start(input).pipe(Effect.mapError(() => new AuthInternalError({
1201
+ code: "internal_error",
1202
+ message: "Failed to start magic link",
1203
+ })));
1204
+ return {
1205
+ email: started.email,
1206
+ expiresAt: started.expiresAt,
1207
+ };
1208
+ });
1209
+ export const makeMagicLinkVerifyHandler = (dependencies) => Effect.fn("auth.http.magic_link.verify")(function* ({ payload, request, }) {
1210
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "magic_link.verify");
1211
+ const trustedDeviceToken = yield* trustedDeviceTokenFromRequest(dependencies.trustedDeviceCookie, request).pipe(Effect.mapError(() => passwordInternalError("Failed to read trusted device cookie")));
1212
+ const requestMetadataOptions = yield* authRequestMetadataOptions;
1213
+ const requestContext = loginRequestContextFromRequest(request, requestMetadataOptions);
1214
+ const result = yield* dependencies.magicLink
1215
+ .verify(magicLinkVerifyInput(payload, Option.isSome(trustedDeviceToken)
1216
+ ? trustedDeviceToken.value
1217
+ : undefined, requestContext))
1218
+ .pipe(Effect.mapError(() => new AuthInternalError({
1219
+ code: "internal_error",
1220
+ message: "Failed to verify magic link",
1221
+ })));
1222
+ return yield* dependencies.authHttp.commitMagicLinkVerifyResult(result);
1223
+ });
1224
+ export const makeOAuthAuthorizationStartHandler = (dependencies) => Effect.fn("auth.http.oauth.authorization_start")(function* ({ payload, }) {
1225
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "oauth.authorization_start");
1226
+ const provider = yield* dependencies.oauthProviders.get(payload.providerId).pipe(Effect.mapError(() => oauthInternalError("Failed to load OAuth provider")));
1227
+ if (Option.isNone(provider)) {
1228
+ return yield* oauthBadRequest("Unknown OAuth provider");
1229
+ }
1230
+ const started = yield* dependencies.oauthState
1231
+ .start(oauthAuthorizationStartInput(payload, provider.value, dependencies))
1232
+ .pipe(Effect.mapError(() => oauthInternalError("Failed to start OAuth authorization")));
1233
+ if (dependencies.storeCodeVerifier !== undefined) {
1234
+ yield* dependencies.storeCodeVerifier({ payload, started }).pipe(Effect.mapError(() => oauthInternalError("Failed to store OAuth verifier")));
1235
+ }
1236
+ return oauthAuthorizationStartedHttpBody(started);
1237
+ });
1238
+ export const makeOAuthAccountUnlinkHandler = (dependencies) => Effect.fn("auth.http.oauth.account_unlink")(function* ({ payload, request, }) {
1239
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "oauth.account_unlink");
1240
+ const session = yield* readRequiredSession(dependencies, request);
1241
+ const stepUpSatisfied = dependencies.stepUpSatisfied === undefined
1242
+ ? undefined
1243
+ : yield* dependencies.stepUpSatisfied({ payload, request, session }).pipe(Effect.mapError(() => oauthInternalError("Failed to evaluate OAuth unlink step-up")));
1244
+ const decision = yield* dependencies.oauthAccountUnlinking
1245
+ .resolve({
1246
+ userId: session.currentSession.userId,
1247
+ providerId: payload.providerId,
1248
+ providerAccountId: payload.providerAccountId,
1249
+ ...(stepUpSatisfied === undefined ? {} : { stepUpSatisfied }),
1250
+ })
1251
+ .pipe(Effect.mapError(() => oauthInternalError("Failed to resolve OAuth account unlinking")));
1252
+ if (decision.type !== "allow") {
1253
+ return oauthAccountUnlinkResolutionHttpBody(decision);
1254
+ }
1255
+ if (dependencies.unlinkAfterAllow !== undefined) {
1256
+ yield* dependencies
1257
+ .unlinkAfterAllow({ payload, request, session, decision })
1258
+ .pipe(Effect.mapError(() => oauthInternalError("Failed to unlink OAuth account")));
1259
+ }
1260
+ return oauthAccountUnlinkResolutionHttpBody(decision, {
1261
+ unlinked: dependencies.unlinkAfterAllow !== undefined,
1262
+ });
1263
+ });
1264
+ export const makePasskeyRegistrationStartHandler = (dependencies) => Effect.fn("auth.http.passkey.registration_start")(function* ({ payload, }) {
1265
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.registration_start");
1266
+ return yield* dependencies.passkeyOptions
1267
+ .startRegistration(passkeyRegistrationStartInput(payload, dependencies))
1268
+ .pipe(Effect.mapError(() => passkeyInternalError("Failed to start passkey registration")));
1269
+ });
1270
+ export const makePasskeyRegistrationFinishHandler = (dependencies) => Effect.fn("auth.http.passkey.registration_finish")(function* ({ payload, }) {
1271
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.registration_finish");
1272
+ const finished = yield* dependencies.passkeyVerification
1273
+ .finishRegistration(passkeyRegistrationFinishInput(payload, dependencies))
1274
+ .pipe(Effect.mapError(passkeyVerificationHttpError));
1275
+ return { credentialId: finished.credential.credentialId };
1276
+ });
1277
+ export const makePasskeyAuthenticationStartHandler = (dependencies) => Effect.fn("auth.http.passkey.authentication_start")(function* ({ payload, }) {
1278
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.authentication_start");
1279
+ return yield* dependencies.passkeyOptions
1280
+ .startAuthentication(passkeyAuthenticationStartInput(payload, dependencies))
1281
+ .pipe(Effect.mapError(() => passkeyInternalError("Failed to start passkey authentication")));
1282
+ });
1283
+ export const makePasskeyAuthenticationFinishHandler = (dependencies) => Effect.fn("auth.http.passkey.authentication_finish")(function* ({ payload, request, }) {
1284
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.authentication_finish");
1285
+ const finished = yield* dependencies.passkeyVerification
1286
+ .finishAuthentication(passkeyAuthenticationFinishInput(payload, dependencies))
1287
+ .pipe(Effect.mapError(passkeyVerificationHttpError));
1288
+ const session = yield* dependencies.sessions
1289
+ .create(dependencies.makeSessionInput({
1290
+ finished,
1291
+ payload,
1292
+ ...(request === undefined ? {} : { request }),
1293
+ }))
1294
+ .pipe(Effect.mapError(() => passkeyInternalError("Failed to create passkey session")));
1295
+ return yield* dependencies.authHttp.commitAuthenticatedSession(session);
1296
+ });
1297
+ export const makePasskeyCredentialListHandler = (dependencies) => Effect.fn("auth.http.passkey.credential_list")(function* ({ request, }) {
1298
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.credential_list");
1299
+ const current = yield* readRequiredSession(dependencies, request);
1300
+ const credentials = yield* dependencies.passkeyCredentialManagement
1301
+ .listForUser({ userId: current.currentSession.userId })
1302
+ .pipe(Effect.mapError(() => passkeyInternalError("Failed to list passkey credentials")));
1303
+ return {
1304
+ credentials: credentials.map(passkeyCredentialInfoHttpBody),
1305
+ };
1306
+ });
1307
+ export const makePasskeyCredentialRevokeHandler = (dependencies) => Effect.fn("auth.http.passkey.credential_revoke")(function* ({ payload, request, }) {
1308
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.credential_revoke");
1309
+ const current = yield* readRequiredSession(dependencies, request);
1310
+ yield* dependencies.passkeyCredentialManagement
1311
+ .revokeForUser(passkeyCredentialRevokeInput(current.currentSession.userId, payload))
1312
+ .pipe(Effect.mapError(passkeyCredentialManagementHttpError));
1313
+ return HttpServerResponse.empty({ status: 204 });
1314
+ });
1315
+ export const makeTotpEnrollmentStartHandler = (dependencies) => Effect.fn("auth.http.totp.enrollment_start")(function* ({ payload, request, }) {
1316
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.enrollment_start");
1317
+ const current = yield* readRequiredSession(dependencies, request);
1318
+ const started = yield* dependencies.totpFactorManagement
1319
+ .startEnrollment(totpEnrollmentStartInput(current.currentSession.userId, payload, dependencies))
1320
+ .pipe(Effect.mapError(totpFactorManagementHttpError));
1321
+ return totpEnrollmentStartedHttpBody(started);
1322
+ });
1323
+ export const makeTotpEnrollmentConfirmHandler = (dependencies) => Effect.fn("auth.http.totp.enrollment_confirm")(function* ({ payload, request, }) {
1324
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.enrollment_confirm");
1325
+ const current = yield* readRequiredSession(dependencies, request);
1326
+ const confirmed = yield* dependencies.totpFactorManagement
1327
+ .confirmEnrollment(totpEnrollmentConfirmInput(current.currentSession.userId, payload, dependencies))
1328
+ .pipe(Effect.mapError(totpFactorManagementHttpError));
1329
+ return totpFactorInfoHttpBody(confirmed);
1330
+ });
1331
+ export const makeTotpVerifyHandler = (dependencies) => Effect.fn("auth.http.totp.verify")(function* ({ payload, request, }) {
1332
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.verify");
1333
+ const current = yield* readRequiredSession(dependencies, request);
1334
+ const result = yield* dependencies.totpFactorManagement
1335
+ .verifyForUser(totpVerifyInput(current.currentSession.userId, payload, dependencies))
1336
+ .pipe(Effect.mapError(() => totpInternalError("Failed to verify TOTP factor")));
1337
+ return totpVerifyResultHttpBody(result);
1338
+ });
1339
+ export const makeTotpFactorListHandler = (dependencies) => Effect.fn("auth.http.totp.factor_list")(function* ({ request, }) {
1340
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.factor_list");
1341
+ const current = yield* readRequiredSession(dependencies, request);
1342
+ const factors = yield* dependencies.totpFactorManagement
1343
+ .listForUser({ userId: current.currentSession.userId })
1344
+ .pipe(Effect.mapError(() => totpInternalError("Failed to list TOTP factors")));
1345
+ return { factors: factors.map(totpFactorInfoHttpBody) };
1346
+ });
1347
+ export const makeTotpFactorRevokeHandler = (dependencies) => Effect.fn("auth.http.totp.factor_revoke")(function* ({ payload, request, }) {
1348
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.factor_revoke");
1349
+ const current = yield* readRequiredSession(dependencies, request);
1350
+ yield* dependencies.totpFactorManagement
1351
+ .revokeForUser(totpFactorRevokeInput(current.currentSession.userId, payload))
1352
+ .pipe(Effect.mapError(totpFactorManagementHttpError));
1353
+ return HttpServerResponse.empty({ status: 204 });
1354
+ });
1355
+ export const makeRecoveryCodeGenerateHandler = (dependencies) => Effect.fn("auth.http.recovery_code.generate")(function* ({ payload, request, }) {
1356
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.generate");
1357
+ const current = yield* readRequiredSession(dependencies, request);
1358
+ const generated = yield* dependencies.recoveryCodeManagement
1359
+ .generateForUser(recoveryCodeGenerateInput(current.currentSession.userId, payload, dependencies))
1360
+ .pipe(Effect.mapError(recoveryCodeManagementHttpError));
1361
+ return recoveryCodesGeneratedHttpBody(generated);
1362
+ });
1363
+ export const makeRecoveryCodeVerifyHandler = (dependencies) => Effect.fn("auth.http.recovery_code.verify")(function* ({ payload, request, }) {
1364
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.verify");
1365
+ const current = yield* readRequiredSession(dependencies, request);
1366
+ const result = yield* dependencies.recoveryCodeManagement
1367
+ .verifyForUser(recoveryCodeVerifyInput(current.currentSession.userId, payload))
1368
+ .pipe(Effect.mapError(() => recoveryCodeInternalError("Failed to verify recovery code")));
1369
+ return recoveryCodeVerifyResultHttpBody(result);
1370
+ });
1371
+ export const makeRecoveryCodeListHandler = (dependencies) => Effect.fn("auth.http.recovery_code.list")(function* ({ request, }) {
1372
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.list");
1373
+ const current = yield* readRequiredSession(dependencies, request);
1374
+ const codes = yield* dependencies.recoveryCodeManagement
1375
+ .listForUser({ userId: current.currentSession.userId })
1376
+ .pipe(Effect.mapError(() => recoveryCodeInternalError("Failed to list recovery codes")));
1377
+ return { codes: codes.map(recoveryCodeInfoHttpBody) };
1378
+ });
1379
+ export const makeRecoveryCodeRevokeHandler = (dependencies) => Effect.fn("auth.http.recovery_code.revoke")(function* ({ payload, request, }) {
1380
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.revoke");
1381
+ const current = yield* readRequiredSession(dependencies, request);
1382
+ yield* dependencies.recoveryCodeManagement
1383
+ .revokeForUser(recoveryCodeRevokeInput(current.currentSession.userId, payload))
1384
+ .pipe(Effect.mapError(recoveryCodeManagementHttpError));
1385
+ return HttpServerResponse.empty({ status: 204 });
1386
+ });
1387
+ export const makeApiKeyCreateHandler = (dependencies) => Effect.fn("auth.http.api_key.create")(function* ({ payload, request, }) {
1388
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "api_key.create");
1389
+ const current = yield* readRequiredSession(dependencies, request);
1390
+ const created = yield* dependencies.apiKeyManagement
1391
+ .createForUser(apiKeyCreateInput(current.currentSession.userId, payload))
1392
+ .pipe(Effect.mapError(apiKeyManagementHttpError));
1393
+ return apiKeyCreatedHttpBody(created);
1394
+ });
1395
+ export const makeApiKeyListHandler = (dependencies) => Effect.fn("auth.http.api_key.list")(function* ({ request, }) {
1396
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "api_key.list");
1397
+ const current = yield* readRequiredSession(dependencies, request);
1398
+ const keys = yield* dependencies.apiKeyManagement
1399
+ .listForUser({ userId: current.currentSession.userId })
1400
+ .pipe(Effect.mapError(() => apiKeyInternalError("Failed to list API keys")));
1401
+ return { keys: keys.map(apiKeyInfoHttpBody) };
1402
+ });
1403
+ export const makeApiKeyRevokeHandler = (dependencies) => Effect.fn("auth.http.api_key.revoke")(function* ({ payload, request, }) {
1404
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "api_key.revoke");
1405
+ const current = yield* readRequiredSession(dependencies, request);
1406
+ yield* dependencies.apiKeyManagement
1407
+ .revokeForUser(apiKeyRevokeInput(current.currentSession.userId, payload))
1408
+ .pipe(Effect.mapError(apiKeyManagementHttpError));
1409
+ return HttpServerResponse.empty({ status: 204 });
1410
+ });
1411
+ export const makeRefreshTokenRefreshHandler = (dependencies) => Effect.fn("auth.http.refresh_token.refresh")(function* ({ payload, }) {
1412
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "refresh_token.refresh");
1413
+ const now = yield* currentUnixMillis;
1414
+ const expiresAt = dependencies.refreshTokenExpiresAt({ now });
1415
+ const rotated = yield* dependencies.refreshTokenManagement
1416
+ .rotate(refreshTokenRefreshInput(payload, now, expiresAt))
1417
+ .pipe(Effect.mapError(() => refreshTokenInternalError("Failed to rotate refresh token")));
1418
+ switch (rotated._tag) {
1419
+ case "Invalid": {
1420
+ return yield* refreshTokenInvalidCredentials();
1421
+ }
1422
+ case "ReuseDetected": {
1423
+ return yield* refreshTokenInvalidCredentials("Refresh token reuse detected");
1424
+ }
1425
+ case "Rotated": {
1426
+ const accessToken = yield* dependencies.jwtIssuer
1427
+ .issue(dependencies.accessToken({
1428
+ now,
1429
+ refreshToken: rotated.refreshToken,
1430
+ previousToken: rotated.previousToken,
1431
+ }))
1432
+ .pipe(Effect.mapError(() => refreshTokenInternalError("Failed to issue access token")));
1433
+ return refreshTokenRefreshedHttpBody({
1434
+ accessToken,
1435
+ refreshToken: Redacted.value(rotated.token),
1436
+ refreshTokenRecord: rotated.refreshToken,
1437
+ });
1438
+ }
1439
+ }
1440
+ });
1441
+ export const makeJwtIntrospectHandler = (dependencies) => Effect.fn("auth.http.jwt.introspect")(function* ({ payload, }) {
1442
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "jwt.introspect");
1443
+ const result = yield* dependencies.jwtRevocation
1444
+ .introspect(jwtIntrospectInput(payload, dependencies))
1445
+ .pipe(Effect.mapError(() => jwtInternalError("Failed to introspect JWT access token")));
1446
+ return jwtIntrospectionHttpBody(result);
1447
+ });
1448
+ export const makeJwtRevokeHandler = (dependencies) => Effect.fn("auth.http.jwt.revoke")(function* ({ payload }) {
1449
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "jwt.revoke");
1450
+ yield* dependencies.jwtRevocation
1451
+ .revokeJwtId(jwtRevokeInput(payload))
1452
+ .pipe(Effect.mapError(jwtRevocationHttpError));
1453
+ return HttpServerResponse.empty({ status: 204 });
1454
+ });
1455
+ export const makeMfaOptionsHandler = (dependencies) => Effect.fn("auth.http.mfa.options")(function* ({ payload, }) {
1456
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.options");
1457
+ const pending = yield* dependencies.authFlowState
1458
+ .inspect(payload.flowId)
1459
+ .pipe(Effect.mapError(() => mfaBadRequest()));
1460
+ const factors = [];
1461
+ if (dependencies.totpFactorManagement !== undefined) {
1462
+ const totpFactors = yield* dependencies.totpFactorManagement
1463
+ .listForUser({ userId: pending.userId })
1464
+ .pipe(Effect.mapError(() => mfaInternalError("Failed to list MFA options")));
1465
+ if (totpFactors.length > 0) {
1466
+ factors.push({ type: "totp" });
1467
+ }
1468
+ }
1469
+ if (dependencies.recoveryCodeManagement !== undefined) {
1470
+ const recoveryCodes = yield* dependencies.recoveryCodeManagement
1471
+ .listForUser({ userId: pending.userId })
1472
+ .pipe(Effect.mapError(() => mfaInternalError("Failed to list MFA options")));
1473
+ if (recoveryCodes.length > 0) {
1474
+ factors.push({ type: "backup-code" });
1475
+ }
1476
+ }
1477
+ if (dependencies.passkeyCredentialManagement !== undefined) {
1478
+ const credentials = yield* dependencies.passkeyCredentialManagement
1479
+ .listForUser({ userId: pending.userId })
1480
+ .pipe(Effect.mapError(() => mfaInternalError("Failed to list MFA options")));
1481
+ if (credentials.length > 0) {
1482
+ factors.push({ type: "passkey" });
1483
+ }
1484
+ }
1485
+ return { factors };
1486
+ });
1487
+ export const makeTotpMfaVerifyHandler = (dependencies) => Effect.fn("auth.http.mfa.totp_verify")(function* ({ payload, }) {
1488
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.totp_verify");
1489
+ const verified = yield* dependencies.totpFactorManagement
1490
+ .verifyForUser(totpMfaVerifyInput(payload, dependencies))
1491
+ .pipe(Effect.mapError(() => mfaInternalError("Failed to verify TOTP MFA factor")));
1492
+ if (!verified.valid) {
1493
+ return yield* mfaInvalidCredentials();
1494
+ }
1495
+ const result = yield* dependencies.authFlow
1496
+ .completeMfa({
1497
+ flowId: payload.flowId,
1498
+ userId: payload.userId,
1499
+ method: "totp",
1500
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
1501
+ })
1502
+ .pipe(Effect.mapError(authFlowMfaHttpError));
1503
+ if (result._tag === "PolicyDenied") {
1504
+ return yield* mfaPolicyDenied(result.reason);
1505
+ }
1506
+ return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
1507
+ });
1508
+ export const makeTotpMfaVerifyForFlowHandler = (dependencies) => Effect.fn("auth.http.mfa.totp_verify_for_flow")(function* ({ payload, }) {
1509
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.totp_verify_for_flow");
1510
+ const pending = yield* dependencies.authFlowState
1511
+ .inspect(payload.flowId)
1512
+ .pipe(Effect.mapError(() => mfaBadRequest()));
1513
+ const verified = yield* dependencies.totpFactorManagement
1514
+ .verifyForUser(totpMfaVerifyForFlowInput(pending.userId, payload, dependencies))
1515
+ .pipe(Effect.mapError(() => mfaInternalError("Failed to verify TOTP MFA factor")));
1516
+ if (!verified.valid) {
1517
+ return yield* mfaInvalidCredentials();
1518
+ }
1519
+ const result = yield* dependencies.authFlow
1520
+ .completeMfa({
1521
+ flowId: payload.flowId,
1522
+ userId: pending.userId,
1523
+ method: "totp",
1524
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
1525
+ })
1526
+ .pipe(Effect.mapError(authFlowMfaHttpError));
1527
+ if (result._tag === "PolicyDenied") {
1528
+ return yield* mfaPolicyDenied(result.reason);
1529
+ }
1530
+ return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
1531
+ });
1532
+ export const makeRecoveryCodeMfaVerifyHandler = (dependencies) => Effect.fn("auth.http.mfa.recovery_code_verify")(function* ({ payload, }) {
1533
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.recovery_code_verify");
1534
+ const verified = yield* dependencies.recoveryCodeManagement
1535
+ .verifyForUser(recoveryCodeMfaVerifyInput(payload))
1536
+ .pipe(Effect.mapError(() => mfaInternalError("Failed to verify recovery code MFA factor")));
1537
+ if (!verified.valid) {
1538
+ return yield* mfaInvalidCredentials();
1539
+ }
1540
+ const result = yield* dependencies.authFlow
1541
+ .completeMfa({
1542
+ flowId: payload.flowId,
1543
+ userId: payload.userId,
1544
+ method: "backup-code",
1545
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
1546
+ })
1547
+ .pipe(Effect.mapError(authFlowMfaHttpError));
1548
+ if (result._tag === "PolicyDenied") {
1549
+ return yield* mfaPolicyDenied(result.reason);
1550
+ }
1551
+ return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
1552
+ });
1553
+ export const makeRecoveryCodeMfaVerifyForFlowHandler = (dependencies) => Effect.fn("auth.http.mfa.recovery_code_verify_for_flow")(function* ({ payload, }) {
1554
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.recovery_code_verify_for_flow");
1555
+ const pending = yield* dependencies.authFlowState
1556
+ .inspect(payload.flowId)
1557
+ .pipe(Effect.mapError(() => mfaBadRequest()));
1558
+ const verified = yield* dependencies.recoveryCodeManagement
1559
+ .verifyForUser(recoveryCodeMfaVerifyForFlowInput(pending.userId, payload))
1560
+ .pipe(Effect.mapError(() => mfaInternalError("Failed to verify recovery code MFA factor")));
1561
+ if (!verified.valid) {
1562
+ return yield* mfaInvalidCredentials();
1563
+ }
1564
+ const result = yield* dependencies.authFlow
1565
+ .completeMfa({
1566
+ flowId: payload.flowId,
1567
+ userId: pending.userId,
1568
+ method: "backup-code",
1569
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
1570
+ })
1571
+ .pipe(Effect.mapError(authFlowMfaHttpError));
1572
+ if (result._tag === "PolicyDenied") {
1573
+ return yield* mfaPolicyDenied(result.reason);
1574
+ }
1575
+ return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
1576
+ });
1577
+ export const makePasskeyMfaStartHandler = (dependencies) => Effect.fn("auth.http.mfa.passkey_start")(function* ({ payload, }) {
1578
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.passkey_start");
1579
+ const pending = yield* dependencies.authFlowState
1580
+ .inspect(payload.flowId)
1581
+ .pipe(Effect.mapError(() => mfaBadRequest()));
1582
+ return yield* dependencies.passkeyOptions
1583
+ .startAuthentication(passkeyMfaStartInput(pending.userId, payload, dependencies))
1584
+ .pipe(Effect.mapError(() => passkeyInternalError("Failed to start passkey MFA authentication")));
1585
+ });
1586
+ export const makePasskeyMfaVerifyHandler = (dependencies) => Effect.fn("auth.http.mfa.passkey_verify")(function* ({ payload, }) {
1587
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.passkey_verify");
1588
+ const finished = yield* dependencies.passkeyVerification
1589
+ .finishAuthentication(passkeyMfaVerifyInput(payload, dependencies))
1590
+ .pipe(Effect.mapError(passkeyVerificationHttpError));
1591
+ const result = yield* dependencies.authFlow
1592
+ .completeMfa({
1593
+ flowId: payload.flowId,
1594
+ userId: finished.userId,
1595
+ method: "passkey",
1596
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
1597
+ })
1598
+ .pipe(Effect.mapError(authFlowMfaHttpError));
1599
+ if (result._tag === "PolicyDenied") {
1600
+ return yield* mfaPolicyDenied(result.reason);
1601
+ }
1602
+ return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
1603
+ });
1604
+ export const makeLoginApprovalApproveHandler = (dependencies) => Effect.fn("auth.http.login_approval.approve")(function* ({ payload, request, }) {
1605
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "login_approval.approve");
1606
+ const input = loginApprovalApproveInput(payload);
1607
+ const approval = yield* dependencies.loginApproval
1608
+ .verify(input.approval)
1609
+ .pipe(Effect.mapError(() => loginApprovalBadRequest()));
1610
+ const approvedFlowId = metadataString(approval.metadata, "flowId");
1611
+ if (approvedFlowId !== input.flowId) {
1612
+ return yield* loginApprovalBadRequest();
1613
+ }
1614
+ if (approval.sessionBinding === "none") {
1615
+ return yield* loginApprovalNoSessionBindingDenied();
1616
+ }
1617
+ const sameDeviceSecret = approval.sameDeviceRequired
1618
+ ? yield* dependencies.loginApprovalPendingCookie
1619
+ .read(new Request("http://localhost", { headers: request.headers }))
1620
+ .pipe(Effect.flatMap((secret) => Option.isSome(secret)
1621
+ ? Effect.succeed(secret.value)
1622
+ : Effect.fail(loginApprovalSameDeviceDenied())))
1623
+ : undefined;
1624
+ const consumed = yield* dependencies.authFlowState
1625
+ .consume(input.flowId, sameDeviceSecret)
1626
+ .pipe(Effect.mapError(() => approval.sameDeviceRequired
1627
+ ? loginApprovalSameDeviceDenied()
1628
+ : loginApprovalBadRequest()));
1629
+ if (consumed.userId !== approval.userId) {
1630
+ return yield* loginApprovalBadRequest();
1631
+ }
1632
+ if (approval.markEmailVerified === true) {
1633
+ const updated = yield* dependencies.users
1634
+ .markEmailVerified({
1635
+ userId: approval.userId,
1636
+ updatedAt: yield* currentUnixMillis,
1637
+ })
1638
+ .pipe(Effect.mapError(() => passwordInternalError("Failed to mark email verified")));
1639
+ if (Option.isNone(updated)) {
1640
+ return yield* loginApprovalBadRequest();
1641
+ }
1642
+ }
1643
+ const result = yield* dependencies.authFlow
1644
+ .complete(consumed)
1645
+ .pipe(Effect.mapError(() => passwordInternalError("Failed to complete login approval")));
1646
+ const setCookies = yield* trustedDeviceSetCookies({
1647
+ trustedDevice: dependencies.trustedDevice,
1648
+ trustedDeviceCookie: dependencies.trustedDeviceCookie,
1649
+ userId: consumed.userId,
1650
+ rememberDevice: input.rememberDevice,
1651
+ });
1652
+ return yield* dependencies.authHttp.commitAuthenticatedSession(result.session, { setCookies });
1653
+ });
1654
+ export const makeLoginApprovalStatusHandler = (dependencies) => Effect.fn("auth.http.login_approval.status")(function* ({ payload, }) {
1655
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "login_approval.status");
1656
+ if (dependencies.loginApprovalReview === undefined) {
1657
+ return yield* loginApprovalReviewNotConfigured();
1658
+ }
1659
+ const input = loginApprovalStatusInput(payload);
1660
+ const recordOption = yield* dependencies.loginApprovalReview
1661
+ .status({ approvalChallengeId: input.approvalChallengeId })
1662
+ .pipe(Effect.mapError(() => passwordInternalError("Failed to load login approval review")));
1663
+ if (Option.isNone(recordOption)) {
1664
+ return yield* loginApprovalBadRequest();
1665
+ }
1666
+ const record = recordOption.value;
1667
+ if (record.flowId !== input.flowId) {
1668
+ return yield* loginApprovalBadRequest();
1669
+ }
1670
+ const now = yield* currentUnixMillis;
1671
+ const status = Number(record.expiresAt) <= Number(now)
1672
+ ? "expired"
1673
+ : record.status;
1674
+ return {
1675
+ status,
1676
+ expiresAt: record.expiresAt,
1677
+ };
1678
+ });
1679
+ export const makeLoginApprovalFinalizeHandler = (dependencies) => Effect.fn("auth.http.login_approval.finalize")(function* ({ payload, request, }) {
1680
+ yield* Effect.annotateCurrentSpan("auth.http.endpoint", "login_approval.finalize");
1681
+ if (dependencies.loginApprovalFinalize === undefined) {
1682
+ return yield* loginApprovalFinalizeNotConfigured();
1683
+ }
1684
+ const input = loginApprovalFinalizeInput(payload);
1685
+ const sameDeviceSecret = yield* dependencies.loginApprovalPendingCookie.read(new Request("http://localhost", { headers: request.headers }));
1686
+ const result = yield* dependencies.loginApprovalFinalize
1687
+ .complete({
1688
+ flowId: input.flowId,
1689
+ approvalChallengeId: input.approvalChallengeId,
1690
+ ...(Option.isSome(sameDeviceSecret)
1691
+ ? { sameDeviceSecret: sameDeviceSecret.value }
1692
+ : {}),
1693
+ })
1694
+ .pipe(Effect.mapError((error) => error._tag === "AuthFlowStateError"
1695
+ ? loginApprovalSameDeviceDenied()
1696
+ : passwordInternalError("Failed to finalize login approval")));
1697
+ if (result._tag === "PolicyDenied") {
1698
+ return yield* loginApprovalPolicyDenied(result.reason);
1699
+ }
1700
+ const setCookies = yield* trustedDeviceSetCookies({
1701
+ trustedDevice: dependencies.trustedDevice,
1702
+ trustedDeviceCookie: dependencies.trustedDeviceCookie,
1703
+ userId: result.session.userId,
1704
+ rememberDevice: input.rememberDevice,
1705
+ });
1706
+ return yield* dependencies.authHttp.commitAuthenticatedSession(result.session, { setCookies });
1707
+ });
269
1708
  export class PasswordApi extends HttpApiGroup.make("password")
270
- .add(passwordSignInEndpoint.middleware(AuthRateLimitMiddleware), passwordSignUpEndpoint.middleware(AuthRateLimitMiddleware))
1709
+ .add(passwordSignInEndpoint.middleware(AuthRateLimitMiddleware), passwordSignUpEndpoint.middleware(AuthRateLimitMiddleware), passwordResetStartEndpoint.middleware(AuthRateLimitMiddleware), passwordResetVerifyEndpoint.middleware(AuthRateLimitMiddleware), passwordSetEndpoint.middleware(AuthOriginCheckMiddleware), passwordChangeEndpoint.middleware(AuthOriginCheckMiddleware))
271
1710
  .prefix("/auth/password")
272
1711
  .middleware(AuthSchemaErrorMiddleware)
273
1712
  .middleware(AuthOriginCheckMiddleware) {
274
1713
  }
275
1714
  export class SessionApi extends HttpApiGroup.make("session")
276
- .add(currentSessionEndpoint, refreshSessionEndpoint.middleware(AuthOriginCheckMiddleware), logoutEndpoint.middleware(AuthOriginCheckMiddleware))
1715
+ .add(currentSessionEndpoint, refreshSessionEndpoint.middleware(AuthOriginCheckMiddleware), logoutEndpoint.middleware(AuthOriginCheckMiddleware), listSessionsEndpoint, revokeSessionEndpoint.middleware(AuthOriginCheckMiddleware), revokeOtherSessionsEndpoint.middleware(AuthOriginCheckMiddleware))
277
1716
  .prefix("/auth") {
278
1717
  }
279
1718
  export class EmailVerificationApi extends HttpApiGroup.make("emailVerification")
@@ -282,29 +1721,110 @@ export class EmailVerificationApi extends HttpApiGroup.make("emailVerification")
282
1721
  .middleware(AuthSchemaErrorMiddleware)
283
1722
  .middleware(AuthOriginCheckMiddleware) {
284
1723
  }
1724
+ export class EmailOtpApi extends HttpApiGroup.make("emailOtp")
1725
+ .add(emailOtpStartEndpoint.middleware(AuthRateLimitMiddleware), emailOtpVerifyEndpoint.middleware(AuthRateLimitMiddleware))
1726
+ .prefix("/auth/email-otp")
1727
+ .middleware(AuthSchemaErrorMiddleware)
1728
+ .middleware(AuthOriginCheckMiddleware) {
1729
+ }
1730
+ export class MagicLinkApi extends HttpApiGroup.make("magicLink")
1731
+ .add(magicLinkStartEndpoint.middleware(AuthRateLimitMiddleware), magicLinkVerifyEndpoint.middleware(AuthRateLimitMiddleware))
1732
+ .prefix("/auth/magic-link")
1733
+ .middleware(AuthSchemaErrorMiddleware)
1734
+ .middleware(AuthOriginCheckMiddleware) {
1735
+ }
1736
+ export class OAuthApi extends HttpApiGroup.make("oauth")
1737
+ .add(oauthAuthorizationStartEndpoint, oauthAccountUnlinkEndpoint)
1738
+ .prefix("/auth/oauth")
1739
+ .middleware(AuthSchemaErrorMiddleware)
1740
+ .middleware(AuthOriginCheckMiddleware) {
1741
+ }
1742
+ export class LoginApprovalApi extends HttpApiGroup.make("loginApproval")
1743
+ .add(loginApprovalApproveEndpoint, loginApprovalStatusEndpoint, loginApprovalFinalizeEndpoint)
1744
+ .prefix("/auth/login-approval")
1745
+ .middleware(AuthSchemaErrorMiddleware)
1746
+ .middleware(AuthOriginCheckMiddleware) {
1747
+ }
1748
+ export class MfaApi extends HttpApiGroup.make("mfa")
1749
+ .add(mfaOptionsEndpoint, totpMfaVerifyEndpoint, totpMfaVerifyForFlowEndpoint, recoveryCodeMfaVerifyEndpoint, recoveryCodeMfaVerifyForFlowEndpoint, passkeyMfaStartEndpoint, passkeyMfaVerifyEndpoint)
1750
+ .prefix("/auth/mfa")
1751
+ .middleware(AuthSchemaErrorMiddleware)
1752
+ .middleware(AuthOriginCheckMiddleware) {
1753
+ }
1754
+ export class ApiKeyApi extends HttpApiGroup.make("apiKey")
1755
+ .add(apiKeyCreateEndpoint, apiKeyListEndpoint, apiKeyRevokeEndpoint)
1756
+ .prefix("/auth/api-keys")
1757
+ .middleware(AuthSchemaErrorMiddleware)
1758
+ .middleware(AuthOriginCheckMiddleware) {
1759
+ }
1760
+ export class RefreshTokenApi extends HttpApiGroup.make("refreshToken")
1761
+ .add(refreshTokenRefreshEndpoint)
1762
+ .prefix("/auth/token")
1763
+ .middleware(AuthSchemaErrorMiddleware) {
1764
+ }
1765
+ export class JwtApi extends HttpApiGroup.make("jwt")
1766
+ .add(jwtIntrospectEndpoint, jwtRevokeEndpoint)
1767
+ .prefix("/auth/jwt")
1768
+ .middleware(AuthSchemaErrorMiddleware) {
1769
+ }
1770
+ export class SecurityApi extends HttpApiGroup.make("security")
1771
+ .add(loginNotificationReportEndpoint)
1772
+ .prefix("/auth/security")
1773
+ .middleware(AuthSchemaErrorMiddleware)
1774
+ .middleware(AuthOriginCheckMiddleware) {
1775
+ }
285
1776
  export class AuthApi extends HttpApi.make("AuthApi")
286
1777
  .add(PasswordApi)
287
1778
  .add(SessionApi)
288
- .add(EmailVerificationApi) {
1779
+ .add(EmailVerificationApi)
1780
+ .add(EmailOtpApi)
1781
+ .add(MagicLinkApi)
1782
+ .add(LoginApprovalApi)
1783
+ .add(SecurityApi) {
289
1784
  }
290
1785
  export const PasswordApiLive = HttpApiBuilder.group(AuthApi, "password", Effect.fn("auth.http.password.group")(function* (handlers) {
291
1786
  const password = yield* PasswordLogin;
292
1787
  const registration = yield* PasswordRegistration;
1788
+ const passwordReset = yield* PasswordReset;
1789
+ const passwordManagement = yield* PasswordManagement;
293
1790
  const authHttp = yield* AuthHttp;
294
1791
  const emailVerificationFlow = yield* EmailVerificationFlow;
1792
+ const sessions = yield* Sessions;
1793
+ const sessionCookie = yield* SessionCookie;
295
1794
  const authApiRateLimit = yield* AuthApiRateLimit;
1795
+ const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
296
1796
  return handlers
297
1797
  .handle("signIn", makePasswordSignInHandler({
298
1798
  password,
299
1799
  authHttp,
300
1800
  emailVerificationFlow,
301
1801
  authApiRateLimit,
1802
+ ...(Option.isSome(trustedDeviceCookie)
1803
+ ? { trustedDeviceCookie: trustedDeviceCookie.value }
1804
+ : {}),
302
1805
  }))
303
1806
  .handle("signUp", makePasswordSignUpHandler({
304
1807
  registration,
305
1808
  authHttp,
306
1809
  emailVerificationFlow,
307
1810
  authApiRateLimit,
1811
+ }))
1812
+ .handle("resetStart", makePasswordResetStartHandler({
1813
+ passwordReset,
1814
+ authApiRateLimit,
1815
+ }))
1816
+ .handle("resetVerify", makePasswordResetVerifyHandler({
1817
+ passwordReset,
1818
+ }))
1819
+ .handle("set", makePasswordSetHandler({
1820
+ passwordManagement,
1821
+ sessions,
1822
+ sessionCookie,
1823
+ }))
1824
+ .handle("change", makePasswordChangeHandler({
1825
+ passwordManagement,
1826
+ sessions,
1827
+ sessionCookie,
308
1828
  }));
309
1829
  }));
310
1830
  export const SessionApiLive = HttpApiBuilder.group(AuthApi, "session", Effect.fn("auth.http.session.group")(function* (handlers) {
@@ -314,7 +1834,10 @@ export const SessionApiLive = HttpApiBuilder.group(AuthApi, "session", Effect.fn
314
1834
  return handlers
315
1835
  .handle("current", makeCurrentSessionHandler({ sessions, sessionCookie, authHttp }))
316
1836
  .handle("refresh", makeRefreshSessionHandler({ sessions, sessionCookie, authHttp }))
317
- .handle("logout", makeLogoutHandler({ sessions, sessionCookie, authHttp }));
1837
+ .handle("logout", makeLogoutHandler({ sessions, sessionCookie, authHttp }))
1838
+ .handle("list", makeListSessionsHandler({ sessions, sessionCookie }))
1839
+ .handle("revoke", makeRevokeSessionHandler({ sessions, sessionCookie, authHttp }))
1840
+ .handle("revokeOthers", makeRevokeOtherSessionsHandler({ sessions, sessionCookie }));
318
1841
  }));
319
1842
  export const EmailVerificationApiLive = HttpApiBuilder.group(AuthApi, "emailVerification", Effect.fn("auth.http.email_verification.group")(function* (handlers) {
320
1843
  const emailVerificationFlow = yield* EmailVerificationFlow;
@@ -333,7 +1856,89 @@ export const EmailVerificationApiLive = HttpApiBuilder.group(AuthApi, "emailVeri
333
1856
  sessionCookie,
334
1857
  }));
335
1858
  }));
336
- const AuthHttpApiBaseLive = HttpApiBuilder.layer(AuthApi).pipe(Layer.provide(PasswordApiLive), Layer.provide(SessionApiLive), Layer.provide(EmailVerificationApiLive), Layer.provide(AuthOriginCheckMiddlewareConfigLive), Layer.provide(AuthSchemaErrorMiddlewareLive));
1859
+ export const EmailOtpApiLive = HttpApiBuilder.group(AuthApi, "emailOtp", Effect.fn("auth.http.email_otp.group")(function* (handlers) {
1860
+ const emailOtp = yield* EmailOtpLogin;
1861
+ const authHttp = yield* AuthHttp;
1862
+ const authApiRateLimit = yield* AuthApiRateLimit;
1863
+ const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
1864
+ return handlers
1865
+ .handle("start", makeEmailOtpStartHandler({ emailOtp, authApiRateLimit }))
1866
+ .handle("verify", makeEmailOtpVerifyHandler({
1867
+ emailOtp,
1868
+ authHttp,
1869
+ ...(Option.isSome(trustedDeviceCookie)
1870
+ ? { trustedDeviceCookie: trustedDeviceCookie.value }
1871
+ : {}),
1872
+ }));
1873
+ }));
1874
+ export const MagicLinkApiLive = HttpApiBuilder.group(AuthApi, "magicLink", Effect.fn("auth.http.magic_link.group")(function* (handlers) {
1875
+ const magicLink = yield* MagicLinkLogin;
1876
+ const authHttp = yield* AuthHttp;
1877
+ const authApiRateLimit = yield* AuthApiRateLimit;
1878
+ const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
1879
+ return handlers
1880
+ .handle("start", makeMagicLinkStartHandler({ magicLink, authApiRateLimit }))
1881
+ .handle("verify", makeMagicLinkVerifyHandler({
1882
+ magicLink,
1883
+ authHttp,
1884
+ ...(Option.isSome(trustedDeviceCookie)
1885
+ ? { trustedDeviceCookie: trustedDeviceCookie.value }
1886
+ : {}),
1887
+ }));
1888
+ }));
1889
+ export const LoginApprovalApiLive = HttpApiBuilder.group(AuthApi, "loginApproval", Effect.fn("auth.http.login_approval.group")(function* (handlers) {
1890
+ const loginApproval = yield* LoginApproval;
1891
+ const authFlowState = yield* AuthFlowState;
1892
+ const authFlow = yield* AuthFlow;
1893
+ const users = yield* UserStore;
1894
+ const authHttp = yield* AuthHttp;
1895
+ const loginApprovalReview = yield* Effect.serviceOption(LoginApprovalReview);
1896
+ const loginApprovalFinalize = yield* Effect.serviceOption(LoginApprovalFinalize);
1897
+ const pendingCookieOption = yield* Effect.serviceOption(LoginApprovalPendingCookie);
1898
+ const trustedDevice = yield* Effect.serviceOption(TrustedDevice);
1899
+ const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
1900
+ const loginApprovalPendingCookie = Option.isSome(pendingCookieOption)
1901
+ ? pendingCookieOption.value
1902
+ : makeLoginApprovalPendingCookie();
1903
+ return handlers
1904
+ .handle("approve", makeLoginApprovalApproveHandler({
1905
+ loginApproval,
1906
+ authFlowState,
1907
+ authFlow,
1908
+ users,
1909
+ authHttp,
1910
+ loginApprovalPendingCookie,
1911
+ ...(Option.isSome(trustedDevice)
1912
+ ? { trustedDevice: trustedDevice.value }
1913
+ : {}),
1914
+ ...(Option.isSome(trustedDeviceCookie)
1915
+ ? { trustedDeviceCookie: trustedDeviceCookie.value }
1916
+ : {}),
1917
+ }))
1918
+ .handle("status", makeLoginApprovalStatusHandler(Option.isSome(loginApprovalReview)
1919
+ ? { loginApprovalReview: loginApprovalReview.value }
1920
+ : {}))
1921
+ .handle("finalize", makeLoginApprovalFinalizeHandler({
1922
+ authHttp,
1923
+ loginApprovalPendingCookie,
1924
+ ...(Option.isSome(loginApprovalFinalize)
1925
+ ? { loginApprovalFinalize: loginApprovalFinalize.value }
1926
+ : {}),
1927
+ ...(Option.isSome(trustedDevice)
1928
+ ? { trustedDevice: trustedDevice.value }
1929
+ : {}),
1930
+ ...(Option.isSome(trustedDeviceCookie)
1931
+ ? { trustedDeviceCookie: trustedDeviceCookie.value }
1932
+ : {}),
1933
+ }));
1934
+ }));
1935
+ export const SecurityApiLive = HttpApiBuilder.group(AuthApi, "security", Effect.fn("auth.http.security.group")(function* (handlers) {
1936
+ const loginNotification = yield* Effect.serviceOption(LoginNotification);
1937
+ return handlers.handle("reportLogin", makeLoginNotificationReportHandler(Option.isSome(loginNotification)
1938
+ ? { loginNotification: loginNotification.value }
1939
+ : {}));
1940
+ }));
1941
+ const AuthHttpApiBaseLive = HttpApiBuilder.layer(AuthApi).pipe(Layer.provide(PasswordApiLive), Layer.provide(SessionApiLive), Layer.provide(EmailVerificationApiLive), Layer.provide(EmailOtpApiLive), Layer.provide(MagicLinkApiLive), Layer.provide(LoginApprovalApiLive), Layer.provide(SecurityApiLive), Layer.provide(AuthOriginCheckMiddlewareConfigLive), Layer.provide(AuthSchemaErrorMiddlewareLive));
337
1942
  export const AuthHttpApiBareLive = AuthHttpApiBaseLive.pipe(Layer.provide(AuthApiRateLimitNoopLive));
338
1943
  export const AuthHttpApiLive = AuthHttpApiBaseLive.pipe(Layer.provide(AuthApiRateLimitLive));
339
1944
  export { AuthResult };