@effect-auth/core 0.1.0-alpha.2 → 0.1.0-alpha.4
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.
- package/README.md +148 -13
- package/dist/ApiKey.d.ts +207 -0
- package/dist/ApiKey.d.ts.map +1 -0
- package/dist/ApiKey.js +275 -0
- package/dist/ApiKey.js.map +1 -0
- package/dist/AuditLog.d.ts +204 -1
- package/dist/AuditLog.d.ts.map +1 -1
- package/dist/AuditLog.js +170 -0
- package/dist/AuditLog.js.map +1 -1
- package/dist/AuthConfig.d.ts +0 -9
- package/dist/AuthConfig.d.ts.map +1 -1
- package/dist/AuthConfig.js +1 -14
- package/dist/AuthConfig.js.map +1 -1
- package/dist/AuthFlow.d.ts +220 -51
- package/dist/AuthFlow.d.ts.map +1 -1
- package/dist/AuthFlow.js +550 -22
- package/dist/AuthFlow.js.map +1 -1
- package/dist/AuthKernel.d.ts +1 -1
- package/dist/Challenge.d.ts +5 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +58 -48
- package/dist/Challenge.js.map +1 -1
- package/dist/Client.d.ts +242 -1
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +489 -1
- package/dist/Client.js.map +1 -1
- package/dist/D1Kysely.d.ts +1 -1
- package/dist/D1Kysely.d.ts.map +1 -1
- package/dist/EffectQbSqliteStorage.d.ts +156 -41
- package/dist/EffectQbSqliteStorage.d.ts.map +1 -1
- package/dist/EffectQbSqliteStorage.js +336 -33
- package/dist/EffectQbSqliteStorage.js.map +1 -1
- package/dist/EmailOtp.d.ts +5 -2
- package/dist/EmailOtp.d.ts.map +1 -1
- package/dist/EmailOtp.js +11 -4
- package/dist/EmailOtp.js.map +1 -1
- package/dist/EmailVerification.d.ts +2 -2
- package/dist/EmailVerification.d.ts.map +1 -1
- package/dist/EmailVerification.js +2 -3
- package/dist/EmailVerification.js.map +1 -1
- package/dist/HttpApi/Api.d.ts +1161 -19
- package/dist/HttpApi/Api.d.ts.map +1 -1
- package/dist/HttpApi/Api.js +1433 -20
- package/dist/HttpApi/Api.js.map +1 -1
- package/dist/HttpApi/Endpoints.d.ts +611 -0
- package/dist/HttpApi/Endpoints.d.ts.map +1 -1
- package/dist/HttpApi/Endpoints.js +242 -1
- package/dist/HttpApi/Endpoints.js.map +1 -1
- package/dist/HttpApi/Schemas.d.ts +704 -0
- package/dist/HttpApi/Schemas.d.ts.map +1 -1
- package/dist/HttpApi/Schemas.js +435 -1
- package/dist/HttpApi/Schemas.js.map +1 -1
- package/dist/HttpApi/Security.d.ts +13 -1
- package/dist/HttpApi/Security.d.ts.map +1 -1
- package/dist/HttpApi/Security.js +107 -16
- package/dist/HttpApi/Security.js.map +1 -1
- package/dist/HttpApi/Transport.d.ts +36 -2
- package/dist/HttpApi/Transport.d.ts.map +1 -1
- package/dist/HttpApi/Transport.js +151 -9
- package/dist/HttpApi/Transport.js.map +1 -1
- package/dist/Jwt.d.ts +271 -0
- package/dist/Jwt.d.ts.map +1 -0
- package/dist/Jwt.js +512 -0
- package/dist/Jwt.js.map +1 -0
- package/dist/KyselyStorage.d.ts +134 -1
- package/dist/KyselyStorage.d.ts.map +1 -1
- package/dist/KyselyStorage.js +918 -4
- package/dist/KyselyStorage.js.map +1 -1
- package/dist/LoginApproval.d.ts +314 -0
- package/dist/LoginApproval.d.ts.map +1 -0
- package/dist/LoginApproval.js +551 -0
- package/dist/LoginApproval.js.map +1 -0
- package/dist/LoginNotification.d.ts +183 -0
- package/dist/LoginNotification.d.ts.map +1 -0
- package/dist/LoginNotification.js +329 -0
- package/dist/LoginNotification.js.map +1 -0
- package/dist/LoginRisk.d.ts +73 -0
- package/dist/LoginRisk.d.ts.map +1 -0
- package/dist/LoginRisk.js +57 -0
- package/dist/LoginRisk.js.map +1 -0
- package/dist/MagicLink.d.ts +85 -0
- package/dist/MagicLink.d.ts.map +1 -0
- package/dist/MagicLink.js +206 -0
- package/dist/MagicLink.js.map +1 -0
- package/dist/Mailer.d.ts +33 -2
- package/dist/Mailer.d.ts.map +1 -1
- package/dist/Mailer.js +107 -0
- package/dist/Mailer.js.map +1 -1
- package/dist/Passkey.d.ts +334 -0
- package/dist/Passkey.d.ts.map +1 -0
- package/dist/Passkey.js +422 -0
- package/dist/Passkey.js.map +1 -0
- package/dist/Password.d.ts +138 -8
- package/dist/Password.d.ts.map +1 -1
- package/dist/Password.js +397 -8
- package/dist/Password.js.map +1 -1
- package/dist/RecoveryCode.d.ts +169 -0
- package/dist/RecoveryCode.d.ts.map +1 -0
- package/dist/RecoveryCode.js +299 -0
- package/dist/RecoveryCode.js.map +1 -0
- package/dist/RefreshToken.d.ts +222 -0
- package/dist/RefreshToken.d.ts.map +1 -0
- package/dist/RefreshToken.js +342 -0
- package/dist/RefreshToken.js.map +1 -0
- package/dist/Sessions.d.ts +52 -2
- package/dist/Sessions.d.ts.map +1 -1
- package/dist/Sessions.js +123 -1
- package/dist/Sessions.js.map +1 -1
- package/dist/StepUp.d.ts +44 -0
- package/dist/StepUp.d.ts.map +1 -0
- package/dist/StepUp.js +82 -0
- package/dist/StepUp.js.map +1 -0
- package/dist/Storage.d.ts +14 -1
- package/dist/Storage.d.ts.map +1 -1
- package/dist/Storage.js.map +1 -1
- package/dist/StorageMigrations.d.ts +9 -1
- package/dist/StorageMigrations.d.ts.map +1 -1
- package/dist/StorageMigrations.js +159 -0
- package/dist/StorageMigrations.js.map +1 -1
- package/dist/Testing.d.ts +1 -1
- package/dist/Totp.d.ts +220 -0
- package/dist/Totp.d.ts.map +1 -0
- package/dist/Totp.js +477 -0
- package/dist/Totp.js.map +1 -0
- package/dist/TrustedDevice.d.ts +115 -0
- package/dist/TrustedDevice.d.ts.map +1 -0
- package/dist/TrustedDevice.js +222 -0
- package/dist/TrustedDevice.js.map +1 -0
- package/dist/alchemy/cloudflare/Email.d.ts +25 -0
- package/dist/alchemy/cloudflare/Email.d.ts.map +1 -0
- package/dist/alchemy/cloudflare/Email.js +117 -0
- package/dist/alchemy/cloudflare/Email.js.map +1 -0
- package/dist/cloudflare/Email.d.ts +25 -0
- package/dist/cloudflare/Email.d.ts.map +1 -0
- package/dist/cloudflare/Email.js +153 -0
- package/dist/cloudflare/Email.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/migrations/0003_auth_login_approval_review.sql +22 -0
- package/migrations/0004_auth_trusted_device.sql +12 -0
- package/migrations/0005_auth_passkey.sql +17 -0
- package/migrations/0006_auth_totp_factor.sql +17 -0
- package/migrations/0007_auth_recovery_code.sql +13 -0
- package/migrations/0008_auth_api_key.sql +17 -0
- package/migrations/0009_auth_refresh_token.sql +21 -0
- package/migrations/0010_auth_jwt_revocation.sql +8 -0
- package/package.json +57 -1
package/dist/HttpApi/Api.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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, 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,411 @@ 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 passkeyRegistrationStartInput = (payload, options) => ({
|
|
107
|
+
relyingParty: options.relyingParty,
|
|
108
|
+
userId: payload.userId,
|
|
109
|
+
userName: payload.userName,
|
|
110
|
+
userDisplayName: payload.userDisplayName,
|
|
111
|
+
...(options.timeout === undefined ? {} : { timeout: options.timeout }),
|
|
112
|
+
...(options.pubKeyCredParams === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: { pubKeyCredParams: options.pubKeyCredParams }),
|
|
115
|
+
...(options.authenticatorSelection === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { authenticatorSelection: options.authenticatorSelection }),
|
|
118
|
+
...(options.attestation === undefined ? {} : { attestation: options.attestation }),
|
|
119
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
120
|
+
});
|
|
121
|
+
export const passkeyAuthenticationStartInput = (payload, options) => ({
|
|
122
|
+
relyingPartyId: options.relyingPartyId,
|
|
123
|
+
...(payload.userId === undefined ? {} : { userId: payload.userId }),
|
|
124
|
+
...(options.timeout === undefined ? {} : { timeout: options.timeout }),
|
|
125
|
+
...(options.userVerification === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { userVerification: options.userVerification }),
|
|
128
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
129
|
+
});
|
|
130
|
+
export const passkeyMfaStartInput = (userId, payload, options) => ({
|
|
131
|
+
relyingPartyId: options.relyingPartyId,
|
|
132
|
+
userId,
|
|
133
|
+
...(options.timeout === undefined ? {} : { timeout: options.timeout }),
|
|
134
|
+
userVerification: options.userVerification ?? "required",
|
|
135
|
+
metadata: {
|
|
136
|
+
...payload.metadata,
|
|
137
|
+
flowId: payload.flowId,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
export const passkeyRegistrationFinishInput = (payload, options) => ({
|
|
141
|
+
challengeId: payload.challengeId,
|
|
142
|
+
userId: payload.userId,
|
|
143
|
+
response: payload.credential,
|
|
144
|
+
relyingPartyId: options.relyingPartyId,
|
|
145
|
+
expectedOrigin: options.expectedOrigin,
|
|
146
|
+
...(options.requireUserVerification === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { requireUserVerification: options.requireUserVerification }),
|
|
149
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
150
|
+
});
|
|
151
|
+
export const passkeyAuthenticationFinishInput = (payload, options) => ({
|
|
152
|
+
challengeId: payload.challengeId,
|
|
153
|
+
response: payload.credential,
|
|
154
|
+
...(payload.userId === undefined ? {} : { userId: payload.userId }),
|
|
155
|
+
relyingPartyId: options.relyingPartyId,
|
|
156
|
+
expectedOrigin: options.expectedOrigin,
|
|
157
|
+
...(options.requireUserVerification === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { requireUserVerification: options.requireUserVerification }),
|
|
160
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
161
|
+
});
|
|
162
|
+
export const passkeyMfaVerifyInput = (payload, options) => ({
|
|
163
|
+
challengeId: payload.challengeId,
|
|
164
|
+
response: payload.credential,
|
|
165
|
+
...(payload.userId === undefined ? {} : { userId: payload.userId }),
|
|
166
|
+
relyingPartyId: options.relyingPartyId,
|
|
167
|
+
expectedOrigin: options.expectedOrigin,
|
|
168
|
+
...(options.requireUserVerification === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: { requireUserVerification: options.requireUserVerification }),
|
|
171
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
172
|
+
});
|
|
173
|
+
export const passkeyCredentialRevokeInput = (userId, payload) => ({
|
|
174
|
+
userId,
|
|
175
|
+
credentialId: PasskeyCredentialId(payload.credentialId),
|
|
176
|
+
...(payload.reason === undefined ? {} : { reason: payload.reason }),
|
|
177
|
+
});
|
|
178
|
+
const passkeyCredentialInfoHttpBody = (credential) => ({
|
|
179
|
+
credentialId: credential.credentialId,
|
|
180
|
+
...(credential.transports === undefined
|
|
181
|
+
? {}
|
|
182
|
+
: { transports: credential.transports }),
|
|
183
|
+
...(credential.backedUp === undefined ? {} : { backedUp: credential.backedUp }),
|
|
184
|
+
createdAt: credential.createdAt,
|
|
185
|
+
...(credential.lastUsedAt === undefined
|
|
186
|
+
? {}
|
|
187
|
+
: { lastUsedAt: credential.lastUsedAt }),
|
|
188
|
+
...(credential.revokedAt === undefined
|
|
189
|
+
? {}
|
|
190
|
+
: { revokedAt: credential.revokedAt }),
|
|
191
|
+
...(credential.metadata === undefined ? {} : { metadata: credential.metadata }),
|
|
192
|
+
});
|
|
193
|
+
export const totpEnrollmentStartInput = (userId, payload, options) => ({
|
|
194
|
+
userId,
|
|
195
|
+
issuer: options.issuer,
|
|
196
|
+
accountName: payload.accountName,
|
|
197
|
+
...(options.secretBytes === undefined ? {} : { secretBytes: options.secretBytes }),
|
|
198
|
+
...(options.algorithm === undefined ? {} : { algorithm: options.algorithm }),
|
|
199
|
+
...(options.digits === undefined ? {} : { digits: options.digits }),
|
|
200
|
+
...(options.period === undefined ? {} : { period: options.period }),
|
|
201
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
202
|
+
});
|
|
203
|
+
export const totpEnrollmentConfirmInput = (userId, payload, options = {}) => ({
|
|
204
|
+
userId,
|
|
205
|
+
factorId: payload.factorId,
|
|
206
|
+
code: Redacted.make(payload.code),
|
|
207
|
+
...(options.window === undefined ? {} : { window: options.window }),
|
|
208
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
209
|
+
});
|
|
210
|
+
export const totpVerifyInput = (userId, payload, options = {}) => ({
|
|
211
|
+
userId,
|
|
212
|
+
code: Redacted.make(payload.code),
|
|
213
|
+
...(options.window === undefined ? {} : { window: options.window }),
|
|
214
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
215
|
+
});
|
|
216
|
+
export const totpFactorRevokeInput = (userId, payload) => ({
|
|
217
|
+
userId,
|
|
218
|
+
factorId: payload.factorId,
|
|
219
|
+
...(payload.reason === undefined ? {} : { reason: payload.reason }),
|
|
220
|
+
});
|
|
221
|
+
const totpFactorInfoHttpBody = (factor) => ({
|
|
222
|
+
factorId: factor.id,
|
|
223
|
+
algorithm: factor.algorithm,
|
|
224
|
+
digits: factor.digits,
|
|
225
|
+
period: factor.period,
|
|
226
|
+
createdAt: factor.createdAt,
|
|
227
|
+
...(factor.confirmedAt === undefined ? {} : { confirmedAt: factor.confirmedAt }),
|
|
228
|
+
...(factor.lastUsedAt === undefined ? {} : { lastUsedAt: factor.lastUsedAt }),
|
|
229
|
+
...(factor.revokedAt === undefined ? {} : { revokedAt: factor.revokedAt }),
|
|
230
|
+
...(factor.metadata === undefined ? {} : { metadata: factor.metadata }),
|
|
231
|
+
});
|
|
232
|
+
const totpEnrollmentStartedHttpBody = (started) => ({
|
|
233
|
+
factor: totpFactorInfoHttpBody(started.factor),
|
|
234
|
+
secret: Redacted.value(started.secret),
|
|
235
|
+
uri: Redacted.value(started.uri),
|
|
236
|
+
});
|
|
237
|
+
const totpVerifyResultHttpBody = (result) => ({
|
|
238
|
+
valid: result.valid,
|
|
239
|
+
...(result.factor === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: { factor: totpFactorInfoHttpBody(result.factor) }),
|
|
242
|
+
...(result.delta === undefined ? {} : { delta: result.delta }),
|
|
243
|
+
});
|
|
244
|
+
export const recoveryCodeGenerateInput = (userId, payload, options = {}) => ({
|
|
245
|
+
userId,
|
|
246
|
+
...(options.count === undefined ? {} : { count: options.count }),
|
|
247
|
+
...(options.length === undefined ? {} : { length: options.length }),
|
|
248
|
+
...(options.groupSize === undefined ? {} : { groupSize: options.groupSize }),
|
|
249
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
250
|
+
});
|
|
251
|
+
export const recoveryCodeVerifyInput = (userId, payload) => ({
|
|
252
|
+
userId,
|
|
253
|
+
code: Redacted.make(payload.code),
|
|
254
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
255
|
+
});
|
|
256
|
+
export const recoveryCodeRevokeInput = (userId, payload) => ({
|
|
257
|
+
userId,
|
|
258
|
+
codeId: payload.codeId,
|
|
259
|
+
...(payload.reason === undefined ? {} : { reason: payload.reason }),
|
|
260
|
+
});
|
|
261
|
+
const recoveryCodeInfoHttpBody = (code) => ({
|
|
262
|
+
codeId: code.id,
|
|
263
|
+
createdAt: code.createdAt,
|
|
264
|
+
...(code.usedAt === undefined ? {} : { usedAt: code.usedAt }),
|
|
265
|
+
...(code.revokedAt === undefined ? {} : { revokedAt: code.revokedAt }),
|
|
266
|
+
...(code.metadata === undefined ? {} : { metadata: code.metadata }),
|
|
267
|
+
});
|
|
268
|
+
const recoveryCodesGeneratedHttpBody = (generated) => ({
|
|
269
|
+
codes: generated.codes.map(Redacted.value),
|
|
270
|
+
records: generated.records.map(recoveryCodeInfoHttpBody),
|
|
271
|
+
});
|
|
272
|
+
const recoveryCodeVerifyResultHttpBody = (result) => ({
|
|
273
|
+
valid: result.valid,
|
|
274
|
+
...(result.code === undefined
|
|
275
|
+
? {}
|
|
276
|
+
: { code: recoveryCodeInfoHttpBody(result.code) }),
|
|
277
|
+
});
|
|
278
|
+
export const apiKeyCreateInput = (userId, payload) => ({
|
|
279
|
+
userId,
|
|
280
|
+
...(payload.scopes === undefined ? {} : { scopes: payload.scopes }),
|
|
281
|
+
...(payload.expiresAt === undefined ? {} : { expiresAt: payload.expiresAt }),
|
|
282
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
283
|
+
});
|
|
284
|
+
export const apiKeyRevokeInput = (userId, payload) => ({
|
|
285
|
+
userId,
|
|
286
|
+
keyId: ApiKeyId(payload.keyId),
|
|
287
|
+
...(payload.reason === undefined ? {} : { reason: payload.reason }),
|
|
288
|
+
});
|
|
289
|
+
const apiKeyInfoHttpBody = (key) => ({
|
|
290
|
+
keyId: key.id,
|
|
291
|
+
prefix: key.prefix,
|
|
292
|
+
scopes: key.scopes,
|
|
293
|
+
createdAt: key.createdAt,
|
|
294
|
+
...(key.expiresAt === undefined ? {} : { expiresAt: key.expiresAt }),
|
|
295
|
+
...(key.lastUsedAt === undefined ? {} : { lastUsedAt: key.lastUsedAt }),
|
|
296
|
+
...(key.revokedAt === undefined ? {} : { revokedAt: key.revokedAt }),
|
|
297
|
+
...(key.metadata === undefined ? {} : { metadata: key.metadata }),
|
|
298
|
+
});
|
|
299
|
+
const apiKeyCreatedHttpBody = (created) => ({
|
|
300
|
+
secret: Redacted.value(created.secret),
|
|
301
|
+
key: apiKeyInfoHttpBody(created.key),
|
|
302
|
+
});
|
|
303
|
+
export const refreshTokenRefreshInput = (payload, now, expiresAt) => ({
|
|
304
|
+
token: Redacted.make(payload.refreshToken),
|
|
305
|
+
now,
|
|
306
|
+
expiresAt,
|
|
307
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
308
|
+
});
|
|
309
|
+
const refreshTokenInfoHttpBody = (refreshToken) => ({
|
|
310
|
+
tokenId: refreshToken.id,
|
|
311
|
+
familyId: refreshToken.familyId,
|
|
312
|
+
createdAt: refreshToken.createdAt,
|
|
313
|
+
expiresAt: refreshToken.expiresAt,
|
|
314
|
+
...(refreshToken.lastUsedAt === undefined
|
|
315
|
+
? {}
|
|
316
|
+
: { lastUsedAt: refreshToken.lastUsedAt }),
|
|
317
|
+
...(refreshToken.rotatedAt === undefined
|
|
318
|
+
? {}
|
|
319
|
+
: { rotatedAt: refreshToken.rotatedAt }),
|
|
320
|
+
...(refreshToken.replacedById === undefined
|
|
321
|
+
? {}
|
|
322
|
+
: { replacedById: refreshToken.replacedById }),
|
|
323
|
+
...(refreshToken.revokedAt === undefined
|
|
324
|
+
? {}
|
|
325
|
+
: { revokedAt: refreshToken.revokedAt }),
|
|
326
|
+
...(refreshToken.reuseDetectedAt === undefined
|
|
327
|
+
? {}
|
|
328
|
+
: { reuseDetectedAt: refreshToken.reuseDetectedAt }),
|
|
329
|
+
...(refreshToken.metadata === undefined ? {} : { metadata: refreshToken.metadata }),
|
|
330
|
+
});
|
|
331
|
+
const issuedJwtExpiresAt = (issued) => typeof issued.claims.exp === "number"
|
|
332
|
+
? (issued.claims.exp * 1000)
|
|
333
|
+
: undefined;
|
|
334
|
+
const refreshTokenRefreshedHttpBody = (input) => ({
|
|
335
|
+
accessToken: Redacted.value(input.accessToken.token),
|
|
336
|
+
...(issuedJwtExpiresAt(input.accessToken) === undefined
|
|
337
|
+
? {}
|
|
338
|
+
: { accessTokenExpiresAt: issuedJwtExpiresAt(input.accessToken) }),
|
|
339
|
+
refreshToken: input.refreshToken,
|
|
340
|
+
refreshTokenRecord: refreshTokenInfoHttpBody(input.refreshTokenRecord),
|
|
341
|
+
});
|
|
342
|
+
export const jwtIntrospectInput = (payload, options = {}) => ({
|
|
343
|
+
token: Redacted.make(payload.token),
|
|
344
|
+
...(options.issuer === undefined ? {} : { issuer: options.issuer }),
|
|
345
|
+
...(options.audience === undefined ? {} : { audience: options.audience }),
|
|
346
|
+
...(options.clockTolerance === undefined
|
|
347
|
+
? {}
|
|
348
|
+
: { clockTolerance: options.clockTolerance }),
|
|
349
|
+
});
|
|
350
|
+
export const jwtRevokeInput = (payload) => ({
|
|
351
|
+
jwtId: payload.jwtId,
|
|
352
|
+
...(payload.expiresAt === undefined ? {} : { expiresAt: payload.expiresAt }),
|
|
353
|
+
...(payload.reason === undefined ? {} : { reason: payload.reason }),
|
|
354
|
+
});
|
|
355
|
+
const jwtKeyInfoHttpBody = (result) => ({
|
|
356
|
+
keyId: result.key.id,
|
|
357
|
+
alg: result.key.alg,
|
|
358
|
+
status: result.key.status,
|
|
359
|
+
...(result.key.createdAt === undefined ? {} : { createdAt: result.key.createdAt }),
|
|
360
|
+
...(result.key.expiresAt === undefined ? {} : { expiresAt: result.key.expiresAt }),
|
|
361
|
+
...(result.key.metadata === undefined ? {} : { metadata: result.key.metadata }),
|
|
362
|
+
});
|
|
363
|
+
const jwtIntrospectionHttpBody = (result) => result.active
|
|
364
|
+
? {
|
|
365
|
+
active: true,
|
|
366
|
+
header: result.header,
|
|
367
|
+
claims: result.claims,
|
|
368
|
+
key: jwtKeyInfoHttpBody(result),
|
|
369
|
+
...(result.jwtId === undefined ? {} : { jwtId: result.jwtId }),
|
|
370
|
+
...(result.expiresAt === undefined ? {} : { expiresAt: result.expiresAt }),
|
|
371
|
+
revoked: false,
|
|
372
|
+
}
|
|
373
|
+
: {
|
|
374
|
+
active: false,
|
|
375
|
+
reason: result.reason,
|
|
376
|
+
...(result.jwtId === undefined ? {} : { jwtId: result.jwtId }),
|
|
377
|
+
...(result.expiresAt === undefined ? {} : { expiresAt: result.expiresAt }),
|
|
378
|
+
...(result.revokedAt === undefined ? {} : { revokedAt: result.revokedAt }),
|
|
379
|
+
...(result.revokedReason === undefined
|
|
380
|
+
? {}
|
|
381
|
+
: { revokedReason: result.revokedReason }),
|
|
382
|
+
};
|
|
383
|
+
export const totpMfaVerifyInput = (payload, options = {}) => ({
|
|
384
|
+
userId: payload.userId,
|
|
385
|
+
code: Redacted.make(payload.code),
|
|
386
|
+
...(options.window === undefined ? {} : { window: options.window }),
|
|
387
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
388
|
+
});
|
|
389
|
+
export const totpMfaVerifyForFlowInput = (userId, payload, options = {}) => ({
|
|
390
|
+
userId,
|
|
391
|
+
code: Redacted.make(payload.code),
|
|
392
|
+
...(options.window === undefined ? {} : { window: options.window }),
|
|
393
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
394
|
+
});
|
|
395
|
+
export const recoveryCodeMfaVerifyInput = (payload) => ({
|
|
396
|
+
userId: payload.userId,
|
|
397
|
+
code: Redacted.make(payload.code),
|
|
398
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
399
|
+
});
|
|
400
|
+
export const recoveryCodeMfaVerifyForFlowInput = (userId, payload) => ({
|
|
401
|
+
userId,
|
|
402
|
+
code: Redacted.make(payload.code),
|
|
403
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
404
|
+
});
|
|
405
|
+
export const loginApprovalApproveInput = (payload) => ({
|
|
406
|
+
flowId: payload.flowId,
|
|
407
|
+
approval: {
|
|
408
|
+
challengeId: payload.approvalChallengeId,
|
|
409
|
+
...(payload.secret === undefined
|
|
410
|
+
? {}
|
|
411
|
+
: { secret: Redacted.make(payload.secret) }),
|
|
412
|
+
},
|
|
413
|
+
rememberDevice: payload.rememberDevice ?? false,
|
|
414
|
+
});
|
|
415
|
+
export const loginApprovalStatusInput = (payload) => ({
|
|
416
|
+
flowId: payload.flowId,
|
|
417
|
+
approvalChallengeId: payload.approvalChallengeId,
|
|
418
|
+
});
|
|
419
|
+
export const loginApprovalFinalizeInput = (payload) => ({
|
|
420
|
+
flowId: payload.flowId,
|
|
421
|
+
approvalChallengeId: payload.approvalChallengeId,
|
|
422
|
+
rememberDevice: payload.rememberDevice ?? false,
|
|
423
|
+
});
|
|
424
|
+
export const loginNotificationReportInput = (payload) => ({
|
|
425
|
+
challengeId: payload.challengeId,
|
|
426
|
+
secret: Redacted.make(payload.secret),
|
|
427
|
+
});
|
|
43
428
|
const webRequestFromHeaders = (headers) => new Request("http://localhost", { headers });
|
|
429
|
+
const firstHeaderValue = (value) => {
|
|
430
|
+
const first = value?.split(",", 1)[0]?.trim();
|
|
431
|
+
return first === "" ? undefined : first;
|
|
432
|
+
};
|
|
433
|
+
const headerNumber = (value) => {
|
|
434
|
+
const parsed = Number(firstHeaderValue(value));
|
|
435
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
436
|
+
};
|
|
437
|
+
const authRequestMetadataOptions = Effect.gen(function* () {
|
|
438
|
+
const config = yield* Effect.serviceOption(AuthHttpApiConfig);
|
|
439
|
+
return Option.isSome(config) ? (config.value.requestMetadata ?? {}) : {};
|
|
440
|
+
});
|
|
441
|
+
const loginRequestContextFromRequest = (request, options) => {
|
|
442
|
+
if (request === undefined) {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
const metadata = makeAuthRequestMetadata(request, options);
|
|
446
|
+
const trustProxyHeaders = options.trustProxyHeaders ?? false;
|
|
447
|
+
const { headers } = request;
|
|
448
|
+
const country = trustProxyHeaders
|
|
449
|
+
? firstHeaderValue(headers["cf-ipcountry"])
|
|
450
|
+
: undefined;
|
|
451
|
+
const region = trustProxyHeaders
|
|
452
|
+
? firstHeaderValue(headers["cf-region"])
|
|
453
|
+
: undefined;
|
|
454
|
+
const city = trustProxyHeaders
|
|
455
|
+
? firstHeaderValue(headers["cf-ipcity"])
|
|
456
|
+
: undefined;
|
|
457
|
+
const latitude = trustProxyHeaders
|
|
458
|
+
? headerNumber(headers["cf-iplatitude"])
|
|
459
|
+
: undefined;
|
|
460
|
+
const longitude = trustProxyHeaders
|
|
461
|
+
? headerNumber(headers["cf-iplongitude"])
|
|
462
|
+
: undefined;
|
|
463
|
+
const context = {
|
|
464
|
+
...(metadata.ipAddress === undefined
|
|
465
|
+
? {}
|
|
466
|
+
: { ip: String(metadata.ipAddress) }),
|
|
467
|
+
...(metadata.userAgent === undefined
|
|
468
|
+
? {}
|
|
469
|
+
: { userAgent: metadata.userAgent }),
|
|
470
|
+
...(country === undefined ? {} : { country }),
|
|
471
|
+
...(region === undefined ? {} : { region }),
|
|
472
|
+
...(city === undefined ? {} : { city }),
|
|
473
|
+
...(latitude === undefined ? {} : { latitude }),
|
|
474
|
+
...(longitude === undefined ? {} : { longitude }),
|
|
475
|
+
};
|
|
476
|
+
return Object.keys(context).length === 0 ? undefined : context;
|
|
477
|
+
};
|
|
478
|
+
const trustedDeviceTokenFromRequest = (trustedDeviceCookie, request) => trustedDeviceCookie === undefined || request === undefined
|
|
479
|
+
? Effect.succeed(Option.none())
|
|
480
|
+
: trustedDeviceCookie.read(new Request("http://localhost", { headers: request.headers }));
|
|
481
|
+
const metadataString = (metadata, key) => {
|
|
482
|
+
const value = metadata?.[key];
|
|
483
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
484
|
+
};
|
|
44
485
|
const emailVerifiedClaims = (claims) => {
|
|
45
486
|
const requirements = claims?.requirements?.filter((requirement) => requirement !== "email_verification");
|
|
46
487
|
return {
|
|
@@ -49,6 +490,50 @@ const emailVerifiedClaims = (claims) => {
|
|
|
49
490
|
...(requirements === undefined ? {} : { requirements }),
|
|
50
491
|
};
|
|
51
492
|
};
|
|
493
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
494
|
+
const metadataNumber = (metadata, key) => {
|
|
495
|
+
const value = metadata?.[key];
|
|
496
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
497
|
+
};
|
|
498
|
+
const sessionRequestMetadata = (metadata) => {
|
|
499
|
+
const value = metadata?.[sessionRequestMetadataKey];
|
|
500
|
+
return isRecord(value) ? value : undefined;
|
|
501
|
+
};
|
|
502
|
+
const activeSessionBody = (session, currentSessionId) => {
|
|
503
|
+
const request = sessionRequestMetadata(session.metadata);
|
|
504
|
+
const ip = metadataString(request, "ip");
|
|
505
|
+
const userAgent = metadataString(request, "userAgent");
|
|
506
|
+
const country = metadataString(request, "country");
|
|
507
|
+
const region = metadataString(request, "region");
|
|
508
|
+
const city = metadataString(request, "city");
|
|
509
|
+
const latitude = metadataNumber(request, "latitude");
|
|
510
|
+
const longitude = metadataNumber(request, "longitude");
|
|
511
|
+
return {
|
|
512
|
+
sessionId: session.sessionId,
|
|
513
|
+
userId: session.userId,
|
|
514
|
+
createdAt: session.createdAt,
|
|
515
|
+
authTime: session.authTime,
|
|
516
|
+
...(session.lastSeenAt === undefined
|
|
517
|
+
? {}
|
|
518
|
+
: { lastSeenAt: session.lastSeenAt }),
|
|
519
|
+
expiresAt: session.expiresAt,
|
|
520
|
+
aal: session.aal,
|
|
521
|
+
amr: session.amr,
|
|
522
|
+
...(session.mfaVerifiedAt === undefined
|
|
523
|
+
? {}
|
|
524
|
+
: { mfaVerifiedAt: session.mfaVerifiedAt }),
|
|
525
|
+
current: session.sessionId === currentSessionId,
|
|
526
|
+
...(session.claims === undefined ? {} : { claims: session.claims }),
|
|
527
|
+
...(ip === undefined ? {} : { ip }),
|
|
528
|
+
...(userAgent === undefined ? {} : { userAgent }),
|
|
529
|
+
...(country === undefined ? {} : { country }),
|
|
530
|
+
...(region === undefined ? {} : { region }),
|
|
531
|
+
...(city === undefined ? {} : { city }),
|
|
532
|
+
...(latitude === undefined ? {} : { latitude }),
|
|
533
|
+
...(longitude === undefined ? {} : { longitude }),
|
|
534
|
+
...(session.metadata === undefined ? {} : { metadata: session.metadata }),
|
|
535
|
+
};
|
|
536
|
+
};
|
|
52
537
|
const readOptionalSession = (dependencies, request) => Effect.gen(function* () {
|
|
53
538
|
const tokenOption = yield* dependencies.sessionCookie
|
|
54
539
|
.read(webRequestFromHeaders(request.headers))
|
|
@@ -68,6 +553,12 @@ const readOptionalSession = (dependencies, request) => Effect.gen(function* () {
|
|
|
68
553
|
message: "Failed to validate session",
|
|
69
554
|
}))));
|
|
70
555
|
});
|
|
556
|
+
const readRequiredSession = (dependencies, request) => readOptionalSession(dependencies, request).pipe(Effect.flatMap((sessionOption) => Option.isSome(sessionOption)
|
|
557
|
+
? Effect.succeed(sessionOption.value)
|
|
558
|
+
: Effect.fail(new AuthUnauthenticatedError({
|
|
559
|
+
code: "unauthenticated",
|
|
560
|
+
message: "Unauthenticated",
|
|
561
|
+
}))));
|
|
71
562
|
const upgradeVerifiedEmailSession = (dependencies, request, verified) => Effect.gen(function* () {
|
|
72
563
|
const sessionOption = yield* readOptionalSession(dependencies, request);
|
|
73
564
|
if (Option.isNone(sessionOption)) {
|
|
@@ -115,28 +606,232 @@ const passwordInternalError = (message) => new AuthInternalError({
|
|
|
115
606
|
code: "internal_error",
|
|
116
607
|
message,
|
|
117
608
|
});
|
|
609
|
+
const loginApprovalBadRequest = () => new AuthBadRequestError({
|
|
610
|
+
code: "bad_request",
|
|
611
|
+
message: "Invalid login approval request",
|
|
612
|
+
});
|
|
613
|
+
const loginApprovalSameDeviceDenied = () => new AuthPolicyDeniedError({
|
|
614
|
+
code: "policy_denied",
|
|
615
|
+
message: "Login approval must be completed on the same device",
|
|
616
|
+
});
|
|
617
|
+
const loginApprovalNoSessionBindingDenied = () => new AuthPolicyDeniedError({
|
|
618
|
+
code: "policy_denied",
|
|
619
|
+
message: "Login approval does not create a user session",
|
|
620
|
+
});
|
|
621
|
+
const loginApprovalReviewNotConfigured = () => new AuthPolicyDeniedError({
|
|
622
|
+
code: "policy_denied",
|
|
623
|
+
message: "Login approval review is not configured",
|
|
624
|
+
});
|
|
625
|
+
const loginApprovalFinalizeNotConfigured = () => new AuthPolicyDeniedError({
|
|
626
|
+
code: "policy_denied",
|
|
627
|
+
message: "Login approval finalization is not configured",
|
|
628
|
+
});
|
|
629
|
+
const loginApprovalPolicyDenied = (reason) => new AuthPolicyDeniedError({
|
|
630
|
+
code: "policy_denied",
|
|
631
|
+
message: reason ?? "Login approval denied",
|
|
632
|
+
});
|
|
633
|
+
const loginNotificationNotConfigured = () => new AuthPolicyDeniedError({
|
|
634
|
+
code: "policy_denied",
|
|
635
|
+
message: "Login notification reporting is not configured",
|
|
636
|
+
});
|
|
637
|
+
const loginNotificationBadRequest = () => new AuthBadRequestError({
|
|
638
|
+
code: "bad_request",
|
|
639
|
+
message: "Invalid login notification report",
|
|
640
|
+
});
|
|
641
|
+
const passkeyInternalError = (message = "Failed to complete passkey request") => new AuthInternalError({
|
|
642
|
+
code: "internal_error",
|
|
643
|
+
message,
|
|
644
|
+
});
|
|
645
|
+
const passkeyBadRequest = (message = "Invalid passkey request") => new AuthBadRequestError({
|
|
646
|
+
code: "bad_request",
|
|
647
|
+
message,
|
|
648
|
+
});
|
|
649
|
+
const passkeyVerificationHttpError = (error) => error.message === "Failed to load passkey credential" ||
|
|
650
|
+
error.message === "Failed to store passkey credential" ||
|
|
651
|
+
error.message === "Failed to update passkey credential" ||
|
|
652
|
+
error.message === "Failed to generate passkey credential record id"
|
|
653
|
+
? passkeyInternalError()
|
|
654
|
+
: passkeyBadRequest();
|
|
655
|
+
const passkeyCredentialManagementHttpError = (error) => error.message === "Passkey credential not found"
|
|
656
|
+
? passkeyBadRequest("Invalid passkey credential")
|
|
657
|
+
: passkeyInternalError("Failed to update passkey credentials");
|
|
658
|
+
const totpInternalError = (message = "Failed to complete TOTP request") => new AuthInternalError({
|
|
659
|
+
code: "internal_error",
|
|
660
|
+
message,
|
|
661
|
+
});
|
|
662
|
+
const totpBadRequest = (message = "Invalid TOTP request") => new AuthBadRequestError({
|
|
663
|
+
code: "bad_request",
|
|
664
|
+
message,
|
|
665
|
+
});
|
|
666
|
+
const totpFactorManagementHttpError = (error) => {
|
|
667
|
+
switch (error.message) {
|
|
668
|
+
case "Failed to build TOTP enrollment URI":
|
|
669
|
+
case "TOTP factor not found":
|
|
670
|
+
case "TOTP factor already confirmed":
|
|
671
|
+
case "Invalid TOTP code": {
|
|
672
|
+
return totpBadRequest();
|
|
673
|
+
}
|
|
674
|
+
default: {
|
|
675
|
+
return totpInternalError();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const recoveryCodeInternalError = (message = "Failed to complete recovery code request") => new AuthInternalError({
|
|
680
|
+
code: "internal_error",
|
|
681
|
+
message,
|
|
682
|
+
});
|
|
683
|
+
const recoveryCodeBadRequest = (message = "Invalid recovery code request") => new AuthBadRequestError({
|
|
684
|
+
code: "bad_request",
|
|
685
|
+
message,
|
|
686
|
+
});
|
|
687
|
+
const recoveryCodeManagementHttpError = (error) => {
|
|
688
|
+
switch (error.message) {
|
|
689
|
+
case "Recovery code not found": {
|
|
690
|
+
return recoveryCodeBadRequest();
|
|
691
|
+
}
|
|
692
|
+
default: {
|
|
693
|
+
return recoveryCodeInternalError();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const apiKeyInternalError = (message = "Failed to complete API key request") => new AuthInternalError({
|
|
698
|
+
code: "internal_error",
|
|
699
|
+
message,
|
|
700
|
+
});
|
|
701
|
+
const apiKeyBadRequest = (message = "Invalid API key request") => new AuthBadRequestError({
|
|
702
|
+
code: "bad_request",
|
|
703
|
+
message,
|
|
704
|
+
});
|
|
705
|
+
const apiKeyManagementHttpError = (error) => {
|
|
706
|
+
switch (error.message) {
|
|
707
|
+
case "API key not found": {
|
|
708
|
+
return apiKeyBadRequest();
|
|
709
|
+
}
|
|
710
|
+
default: {
|
|
711
|
+
return apiKeyInternalError();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
const refreshTokenInternalError = (message = "Failed to complete refresh token request") => new AuthInternalError({
|
|
716
|
+
code: "internal_error",
|
|
717
|
+
message,
|
|
718
|
+
});
|
|
719
|
+
const refreshTokenInvalidCredentials = (message = "Invalid refresh token") => new AuthInvalidCredentialsError({
|
|
720
|
+
code: "invalid_credentials",
|
|
721
|
+
message,
|
|
722
|
+
});
|
|
723
|
+
const jwtInternalError = (message = "Failed to complete JWT request") => new AuthInternalError({
|
|
724
|
+
code: "internal_error",
|
|
725
|
+
message,
|
|
726
|
+
});
|
|
727
|
+
const jwtBadRequest = (message = "Invalid JWT request") => new AuthBadRequestError({
|
|
728
|
+
code: "bad_request",
|
|
729
|
+
message,
|
|
730
|
+
});
|
|
731
|
+
const jwtRevocationHttpError = (error) => error.message === "JWT ID must not be empty" ? jwtBadRequest() : jwtInternalError();
|
|
732
|
+
const mfaInternalError = (message = "Failed to complete MFA request") => new AuthInternalError({
|
|
733
|
+
code: "internal_error",
|
|
734
|
+
message,
|
|
735
|
+
});
|
|
736
|
+
const mfaBadRequest = (message = "Invalid MFA request") => new AuthBadRequestError({
|
|
737
|
+
code: "bad_request",
|
|
738
|
+
message,
|
|
739
|
+
});
|
|
740
|
+
const mfaInvalidCredentials = () => new AuthInvalidCredentialsError({
|
|
741
|
+
code: "invalid_credentials",
|
|
742
|
+
message: "Invalid MFA code",
|
|
743
|
+
});
|
|
744
|
+
const mfaPolicyDenied = (reason) => new AuthPolicyDeniedError({
|
|
745
|
+
code: "policy_denied",
|
|
746
|
+
message: reason ?? "MFA policy denied the request",
|
|
747
|
+
});
|
|
748
|
+
const authFlowMfaHttpError = (error) => error._tag === "AuthFlowStateError" ? mfaBadRequest() : mfaInternalError();
|
|
749
|
+
const trustedDeviceSetCookies = (dependencies) => Effect.gen(function* () {
|
|
750
|
+
if (!dependencies.rememberDevice ||
|
|
751
|
+
dependencies.trustedDevice === undefined ||
|
|
752
|
+
dependencies.trustedDeviceCookie === undefined) {
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
const token = yield* dependencies.trustedDevice
|
|
756
|
+
.trust({ userId: dependencies.userId })
|
|
757
|
+
.pipe(Effect.mapError(() => passwordInternalError("Failed to remember trusted device")));
|
|
758
|
+
const cookie = yield* dependencies.trustedDeviceCookie
|
|
759
|
+
.commit({ token })
|
|
760
|
+
.pipe(Effect.mapError(() => passwordInternalError("Failed to remember trusted device")));
|
|
761
|
+
return [cookie];
|
|
762
|
+
});
|
|
118
763
|
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, }) {
|
|
764
|
+
export const makePasswordSignInHandler = (dependencies) => Effect.fn("auth.http.password.sign_in")(function* ({ payload, request, }) {
|
|
120
765
|
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.sign_in");
|
|
121
|
-
const
|
|
766
|
+
const trustedDeviceToken = yield* trustedDeviceTokenFromRequest(dependencies.trustedDeviceCookie, request).pipe(Effect.mapError(() => passwordInternalError("Failed to read trusted device cookie")));
|
|
767
|
+
const requestMetadataOptions = yield* authRequestMetadataOptions;
|
|
768
|
+
const requestContext = loginRequestContextFromRequest(request, requestMetadataOptions);
|
|
769
|
+
const input = passwordSignInInput(payload, Option.isSome(trustedDeviceToken) ? trustedDeviceToken.value : undefined, requestContext);
|
|
122
770
|
const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
|
|
123
771
|
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"))));
|
|
772
|
+
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
773
|
yield* startRequiredEmailVerification(dependencies, result);
|
|
126
774
|
return yield* dependencies.authHttp.commitPasswordSignInResult(result);
|
|
127
775
|
});
|
|
128
|
-
export const makePasswordSignUpHandler = (dependencies) => Effect.fn("auth.http.password.sign_up")(function* ({ payload, }) {
|
|
776
|
+
export const makePasswordSignUpHandler = (dependencies) => Effect.fn("auth.http.password.sign_up")(function* ({ payload, request, }) {
|
|
129
777
|
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.sign_up");
|
|
130
|
-
const
|
|
778
|
+
const requestMetadataOptions = yield* authRequestMetadataOptions;
|
|
779
|
+
const input = passwordSignUpInput(payload, loginRequestContextFromRequest(request, requestMetadataOptions));
|
|
131
780
|
const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
|
|
132
781
|
yield* catchAuthApiRateLimit(authApiRateLimit.passwordSignUp(input), "Failed to sign up with password");
|
|
133
782
|
const result = yield* dependencies.registration.signUp(input).pipe(Effect.catchTag("EmailAlreadyRegisteredError", () => Effect.fail(new AuthEmailAlreadyRegisteredError({
|
|
134
783
|
code: "email_already_registered",
|
|
135
784
|
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"))));
|
|
785
|
+
}))), 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
786
|
yield* startRequiredEmailVerification(dependencies, result);
|
|
138
787
|
return yield* dependencies.authHttp.commitPasswordSignUpResult(result);
|
|
139
788
|
});
|
|
789
|
+
export const makePasswordResetStartHandler = (dependencies) => Effect.fn("auth.http.password.reset_start")(function* ({ payload, }) {
|
|
790
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.reset_start");
|
|
791
|
+
const input = passwordResetStartInput(payload);
|
|
792
|
+
const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
|
|
793
|
+
yield* catchAuthApiRateLimit(authApiRateLimit.passwordResetStart(input), "Failed to start password reset");
|
|
794
|
+
yield* dependencies.passwordReset.start(input).pipe(Effect.mapError(() => new AuthInternalError({
|
|
795
|
+
code: "internal_error",
|
|
796
|
+
message: "Failed to start password reset",
|
|
797
|
+
})));
|
|
798
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
799
|
+
});
|
|
800
|
+
export const makePasswordResetVerifyHandler = (dependencies) => Effect.fn("auth.http.password.reset_verify")(function* ({ payload, }) {
|
|
801
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.reset_verify");
|
|
802
|
+
yield* dependencies.passwordReset
|
|
803
|
+
.verify(passwordResetVerifyInput(payload))
|
|
804
|
+
.pipe(Effect.mapError(() => new AuthBadRequestError({
|
|
805
|
+
code: "bad_request",
|
|
806
|
+
message: "Invalid password reset request",
|
|
807
|
+
})));
|
|
808
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
809
|
+
});
|
|
810
|
+
const passwordManagementError = (error) => error.reason === "internal"
|
|
811
|
+
? new AuthInternalError({
|
|
812
|
+
code: "internal_error",
|
|
813
|
+
message: "Failed to update password",
|
|
814
|
+
})
|
|
815
|
+
: new AuthBadRequestError({
|
|
816
|
+
code: "bad_request",
|
|
817
|
+
message: "Invalid password update request",
|
|
818
|
+
});
|
|
819
|
+
export const makePasswordSetHandler = (dependencies) => Effect.fn("auth.http.password.set")(function* ({ payload, request, }) {
|
|
820
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.set");
|
|
821
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
822
|
+
yield* dependencies.passwordManagement
|
|
823
|
+
.set(passwordSetInput(current.currentSession.userId, payload))
|
|
824
|
+
.pipe(Effect.mapError(passwordManagementError));
|
|
825
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
826
|
+
});
|
|
827
|
+
export const makePasswordChangeHandler = (dependencies) => Effect.fn("auth.http.password.change")(function* ({ payload, request, }) {
|
|
828
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "password.change");
|
|
829
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
830
|
+
yield* dependencies.passwordManagement
|
|
831
|
+
.change(passwordChangeInput(current.currentSession.userId, payload))
|
|
832
|
+
.pipe(Effect.mapError(passwordManagementError));
|
|
833
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
834
|
+
});
|
|
140
835
|
export const makeCurrentSessionHandler = (dependencies) => Effect.fn("auth.http.session.current")(function* ({ request, }) {
|
|
141
836
|
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.current");
|
|
142
837
|
const tokenOption = yield* dependencies.sessionCookie
|
|
@@ -234,6 +929,66 @@ export const makeLogoutHandler = (dependencies) => Effect.fn("auth.http.session.
|
|
|
234
929
|
}
|
|
235
930
|
return yield* dependencies.authHttp.commitSessionCleared();
|
|
236
931
|
});
|
|
932
|
+
export const makeListSessionsHandler = (dependencies) => Effect.fn("auth.http.session.list")(function* ({ request, }) {
|
|
933
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.list");
|
|
934
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
935
|
+
const sessions = yield* dependencies.sessions
|
|
936
|
+
.listForUser({ userId: current.currentSession.userId })
|
|
937
|
+
.pipe(Effect.mapError(() => new AuthInternalError({
|
|
938
|
+
code: "internal_error",
|
|
939
|
+
message: "Failed to list sessions",
|
|
940
|
+
})));
|
|
941
|
+
return {
|
|
942
|
+
sessions: sessions.map((session) => activeSessionBody(session, current.currentSession.sessionId)),
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
export const makeRevokeSessionHandler = (dependencies) => Effect.fn("auth.http.session.revoke")(function* ({ payload, request, }) {
|
|
946
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.revoke");
|
|
947
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
948
|
+
yield* dependencies.sessions
|
|
949
|
+
.revokeForUser({
|
|
950
|
+
userId: current.currentSession.userId,
|
|
951
|
+
sessionId: payload.sessionId,
|
|
952
|
+
reason: "user_revoked_session",
|
|
953
|
+
})
|
|
954
|
+
.pipe(Effect.mapError(() => new AuthInternalError({
|
|
955
|
+
code: "internal_error",
|
|
956
|
+
message: "Failed to revoke session",
|
|
957
|
+
})));
|
|
958
|
+
return payload.sessionId === current.currentSession.sessionId
|
|
959
|
+
? yield* dependencies.authHttp.commitSessionCleared()
|
|
960
|
+
: HttpServerResponse.empty({ status: 204 });
|
|
961
|
+
});
|
|
962
|
+
export const makeRevokeOtherSessionsHandler = (dependencies) => Effect.fn("auth.http.session.revoke_others")(function* ({ request, }) {
|
|
963
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "session.revoke_others");
|
|
964
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
965
|
+
yield* dependencies.sessions
|
|
966
|
+
.revokeAllForUserExcept({
|
|
967
|
+
userId: current.currentSession.userId,
|
|
968
|
+
exceptSessionId: current.currentSession.sessionId,
|
|
969
|
+
reason: "user_revoked_other_sessions",
|
|
970
|
+
})
|
|
971
|
+
.pipe(Effect.mapError(() => new AuthInternalError({
|
|
972
|
+
code: "internal_error",
|
|
973
|
+
message: "Failed to revoke other sessions",
|
|
974
|
+
})));
|
|
975
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
976
|
+
});
|
|
977
|
+
export const makeLoginNotificationReportHandler = (dependencies) => Effect.fn("auth.http.security.login_notification.report")(function* ({ payload, }) {
|
|
978
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "security.login_notification.report");
|
|
979
|
+
if (dependencies.loginNotification === undefined) {
|
|
980
|
+
return yield* loginNotificationNotConfigured();
|
|
981
|
+
}
|
|
982
|
+
yield* dependencies.loginNotification
|
|
983
|
+
.report(loginNotificationReportInput(payload))
|
|
984
|
+
.pipe(Effect.mapError((error) => error.reason === "invalid_report"
|
|
985
|
+
? loginNotificationBadRequest()
|
|
986
|
+
: new AuthInternalError({
|
|
987
|
+
code: "internal_error",
|
|
988
|
+
message: "Failed to process login notification report",
|
|
989
|
+
})));
|
|
990
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
991
|
+
});
|
|
237
992
|
export const makeEmailVerificationStartHandler = (dependencies) => Effect.fn("auth.http.email_verification.start")(function* ({ payload, }) {
|
|
238
993
|
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "email_verification.start");
|
|
239
994
|
const input = emailVerificationStartInput(payload);
|
|
@@ -266,14 +1021,512 @@ export const makeEmailVerificationVerifyHandler = (dependencies) => Effect.fn("a
|
|
|
266
1021
|
yield* upgradeVerifiedEmailSession(dependencies, request, verified);
|
|
267
1022
|
return HttpServerResponse.empty({ status: 204 });
|
|
268
1023
|
});
|
|
1024
|
+
export const makeEmailOtpStartHandler = (dependencies) => Effect.fn("auth.http.email_otp.start")(function* ({ payload, }) {
|
|
1025
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "email_otp.start");
|
|
1026
|
+
const input = emailOtpStartInput(payload);
|
|
1027
|
+
const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
|
|
1028
|
+
yield* catchAuthApiRateLimit(authApiRateLimit.emailOtpStart(input), "Failed to start email OTP");
|
|
1029
|
+
return yield* dependencies.emailOtp.start(input).pipe(Effect.mapError(() => new AuthInternalError({
|
|
1030
|
+
code: "internal_error",
|
|
1031
|
+
message: "Failed to start email OTP",
|
|
1032
|
+
})));
|
|
1033
|
+
});
|
|
1034
|
+
export const makeEmailOtpVerifyHandler = (dependencies) => Effect.fn("auth.http.email_otp.verify")(function* ({ payload, request, }) {
|
|
1035
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "email_otp.verify");
|
|
1036
|
+
const trustedDeviceToken = yield* trustedDeviceTokenFromRequest(dependencies.trustedDeviceCookie, request).pipe(Effect.mapError(() => passwordInternalError("Failed to read trusted device cookie")));
|
|
1037
|
+
const requestMetadataOptions = yield* authRequestMetadataOptions;
|
|
1038
|
+
const requestContext = loginRequestContextFromRequest(request, requestMetadataOptions);
|
|
1039
|
+
const result = yield* dependencies.emailOtp
|
|
1040
|
+
.verify(emailOtpVerifyInput(payload, Option.isSome(trustedDeviceToken)
|
|
1041
|
+
? trustedDeviceToken.value
|
|
1042
|
+
: undefined, requestContext))
|
|
1043
|
+
.pipe(Effect.mapError(() => new AuthInternalError({
|
|
1044
|
+
code: "internal_error",
|
|
1045
|
+
message: "Failed to verify email OTP",
|
|
1046
|
+
})));
|
|
1047
|
+
return yield* dependencies.authHttp.commitEmailOtpVerifyResult(result);
|
|
1048
|
+
});
|
|
1049
|
+
export const makeMagicLinkStartHandler = (dependencies) => Effect.fn("auth.http.magic_link.start")(function* ({ payload, }) {
|
|
1050
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "magic_link.start");
|
|
1051
|
+
const input = magicLinkStartInput(payload);
|
|
1052
|
+
const authApiRateLimit = dependencies.authApiRateLimit ?? noopAuthApiRateLimit;
|
|
1053
|
+
yield* catchAuthApiRateLimit(authApiRateLimit.magicLinkStart(input), "Failed to start magic link");
|
|
1054
|
+
const started = yield* dependencies.magicLink.start(input).pipe(Effect.mapError(() => new AuthInternalError({
|
|
1055
|
+
code: "internal_error",
|
|
1056
|
+
message: "Failed to start magic link",
|
|
1057
|
+
})));
|
|
1058
|
+
return {
|
|
1059
|
+
email: started.email,
|
|
1060
|
+
expiresAt: started.expiresAt,
|
|
1061
|
+
};
|
|
1062
|
+
});
|
|
1063
|
+
export const makeMagicLinkVerifyHandler = (dependencies) => Effect.fn("auth.http.magic_link.verify")(function* ({ payload, request, }) {
|
|
1064
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "magic_link.verify");
|
|
1065
|
+
const trustedDeviceToken = yield* trustedDeviceTokenFromRequest(dependencies.trustedDeviceCookie, request).pipe(Effect.mapError(() => passwordInternalError("Failed to read trusted device cookie")));
|
|
1066
|
+
const requestMetadataOptions = yield* authRequestMetadataOptions;
|
|
1067
|
+
const requestContext = loginRequestContextFromRequest(request, requestMetadataOptions);
|
|
1068
|
+
const result = yield* dependencies.magicLink
|
|
1069
|
+
.verify(magicLinkVerifyInput(payload, Option.isSome(trustedDeviceToken)
|
|
1070
|
+
? trustedDeviceToken.value
|
|
1071
|
+
: undefined, requestContext))
|
|
1072
|
+
.pipe(Effect.mapError(() => new AuthInternalError({
|
|
1073
|
+
code: "internal_error",
|
|
1074
|
+
message: "Failed to verify magic link",
|
|
1075
|
+
})));
|
|
1076
|
+
return yield* dependencies.authHttp.commitMagicLinkVerifyResult(result);
|
|
1077
|
+
});
|
|
1078
|
+
export const makePasskeyRegistrationStartHandler = (dependencies) => Effect.fn("auth.http.passkey.registration_start")(function* ({ payload, }) {
|
|
1079
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.registration_start");
|
|
1080
|
+
return yield* dependencies.passkeyOptions
|
|
1081
|
+
.startRegistration(passkeyRegistrationStartInput(payload, dependencies))
|
|
1082
|
+
.pipe(Effect.mapError(() => passkeyInternalError("Failed to start passkey registration")));
|
|
1083
|
+
});
|
|
1084
|
+
export const makePasskeyRegistrationFinishHandler = (dependencies) => Effect.fn("auth.http.passkey.registration_finish")(function* ({ payload, }) {
|
|
1085
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.registration_finish");
|
|
1086
|
+
const finished = yield* dependencies.passkeyVerification
|
|
1087
|
+
.finishRegistration(passkeyRegistrationFinishInput(payload, dependencies))
|
|
1088
|
+
.pipe(Effect.mapError(passkeyVerificationHttpError));
|
|
1089
|
+
return { credentialId: finished.credential.credentialId };
|
|
1090
|
+
});
|
|
1091
|
+
export const makePasskeyAuthenticationStartHandler = (dependencies) => Effect.fn("auth.http.passkey.authentication_start")(function* ({ payload, }) {
|
|
1092
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.authentication_start");
|
|
1093
|
+
return yield* dependencies.passkeyOptions
|
|
1094
|
+
.startAuthentication(passkeyAuthenticationStartInput(payload, dependencies))
|
|
1095
|
+
.pipe(Effect.mapError(() => passkeyInternalError("Failed to start passkey authentication")));
|
|
1096
|
+
});
|
|
1097
|
+
export const makePasskeyAuthenticationFinishHandler = (dependencies) => Effect.fn("auth.http.passkey.authentication_finish")(function* ({ payload, request, }) {
|
|
1098
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.authentication_finish");
|
|
1099
|
+
const finished = yield* dependencies.passkeyVerification
|
|
1100
|
+
.finishAuthentication(passkeyAuthenticationFinishInput(payload, dependencies))
|
|
1101
|
+
.pipe(Effect.mapError(passkeyVerificationHttpError));
|
|
1102
|
+
const session = yield* dependencies.sessions
|
|
1103
|
+
.create(dependencies.makeSessionInput({
|
|
1104
|
+
finished,
|
|
1105
|
+
payload,
|
|
1106
|
+
...(request === undefined ? {} : { request }),
|
|
1107
|
+
}))
|
|
1108
|
+
.pipe(Effect.mapError(() => passkeyInternalError("Failed to create passkey session")));
|
|
1109
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(session);
|
|
1110
|
+
});
|
|
1111
|
+
export const makePasskeyCredentialListHandler = (dependencies) => Effect.fn("auth.http.passkey.credential_list")(function* ({ request, }) {
|
|
1112
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.credential_list");
|
|
1113
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1114
|
+
const credentials = yield* dependencies.passkeyCredentialManagement
|
|
1115
|
+
.listForUser({ userId: current.currentSession.userId })
|
|
1116
|
+
.pipe(Effect.mapError(() => passkeyInternalError("Failed to list passkey credentials")));
|
|
1117
|
+
return {
|
|
1118
|
+
credentials: credentials.map(passkeyCredentialInfoHttpBody),
|
|
1119
|
+
};
|
|
1120
|
+
});
|
|
1121
|
+
export const makePasskeyCredentialRevokeHandler = (dependencies) => Effect.fn("auth.http.passkey.credential_revoke")(function* ({ payload, request, }) {
|
|
1122
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "passkey.credential_revoke");
|
|
1123
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1124
|
+
yield* dependencies.passkeyCredentialManagement
|
|
1125
|
+
.revokeForUser(passkeyCredentialRevokeInput(current.currentSession.userId, payload))
|
|
1126
|
+
.pipe(Effect.mapError(passkeyCredentialManagementHttpError));
|
|
1127
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
1128
|
+
});
|
|
1129
|
+
export const makeTotpEnrollmentStartHandler = (dependencies) => Effect.fn("auth.http.totp.enrollment_start")(function* ({ payload, request, }) {
|
|
1130
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.enrollment_start");
|
|
1131
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1132
|
+
const started = yield* dependencies.totpFactorManagement
|
|
1133
|
+
.startEnrollment(totpEnrollmentStartInput(current.currentSession.userId, payload, dependencies))
|
|
1134
|
+
.pipe(Effect.mapError(totpFactorManagementHttpError));
|
|
1135
|
+
return totpEnrollmentStartedHttpBody(started);
|
|
1136
|
+
});
|
|
1137
|
+
export const makeTotpEnrollmentConfirmHandler = (dependencies) => Effect.fn("auth.http.totp.enrollment_confirm")(function* ({ payload, request, }) {
|
|
1138
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.enrollment_confirm");
|
|
1139
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1140
|
+
const confirmed = yield* dependencies.totpFactorManagement
|
|
1141
|
+
.confirmEnrollment(totpEnrollmentConfirmInput(current.currentSession.userId, payload, dependencies))
|
|
1142
|
+
.pipe(Effect.mapError(totpFactorManagementHttpError));
|
|
1143
|
+
return totpFactorInfoHttpBody(confirmed);
|
|
1144
|
+
});
|
|
1145
|
+
export const makeTotpVerifyHandler = (dependencies) => Effect.fn("auth.http.totp.verify")(function* ({ payload, request, }) {
|
|
1146
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.verify");
|
|
1147
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1148
|
+
const result = yield* dependencies.totpFactorManagement
|
|
1149
|
+
.verifyForUser(totpVerifyInput(current.currentSession.userId, payload, dependencies))
|
|
1150
|
+
.pipe(Effect.mapError(() => totpInternalError("Failed to verify TOTP factor")));
|
|
1151
|
+
return totpVerifyResultHttpBody(result);
|
|
1152
|
+
});
|
|
1153
|
+
export const makeTotpFactorListHandler = (dependencies) => Effect.fn("auth.http.totp.factor_list")(function* ({ request, }) {
|
|
1154
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.factor_list");
|
|
1155
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1156
|
+
const factors = yield* dependencies.totpFactorManagement
|
|
1157
|
+
.listForUser({ userId: current.currentSession.userId })
|
|
1158
|
+
.pipe(Effect.mapError(() => totpInternalError("Failed to list TOTP factors")));
|
|
1159
|
+
return { factors: factors.map(totpFactorInfoHttpBody) };
|
|
1160
|
+
});
|
|
1161
|
+
export const makeTotpFactorRevokeHandler = (dependencies) => Effect.fn("auth.http.totp.factor_revoke")(function* ({ payload, request, }) {
|
|
1162
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "totp.factor_revoke");
|
|
1163
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1164
|
+
yield* dependencies.totpFactorManagement
|
|
1165
|
+
.revokeForUser(totpFactorRevokeInput(current.currentSession.userId, payload))
|
|
1166
|
+
.pipe(Effect.mapError(totpFactorManagementHttpError));
|
|
1167
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
1168
|
+
});
|
|
1169
|
+
export const makeRecoveryCodeGenerateHandler = (dependencies) => Effect.fn("auth.http.recovery_code.generate")(function* ({ payload, request, }) {
|
|
1170
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.generate");
|
|
1171
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1172
|
+
const generated = yield* dependencies.recoveryCodeManagement
|
|
1173
|
+
.generateForUser(recoveryCodeGenerateInput(current.currentSession.userId, payload, dependencies))
|
|
1174
|
+
.pipe(Effect.mapError(recoveryCodeManagementHttpError));
|
|
1175
|
+
return recoveryCodesGeneratedHttpBody(generated);
|
|
1176
|
+
});
|
|
1177
|
+
export const makeRecoveryCodeVerifyHandler = (dependencies) => Effect.fn("auth.http.recovery_code.verify")(function* ({ payload, request, }) {
|
|
1178
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.verify");
|
|
1179
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1180
|
+
const result = yield* dependencies.recoveryCodeManagement
|
|
1181
|
+
.verifyForUser(recoveryCodeVerifyInput(current.currentSession.userId, payload))
|
|
1182
|
+
.pipe(Effect.mapError(() => recoveryCodeInternalError("Failed to verify recovery code")));
|
|
1183
|
+
return recoveryCodeVerifyResultHttpBody(result);
|
|
1184
|
+
});
|
|
1185
|
+
export const makeRecoveryCodeListHandler = (dependencies) => Effect.fn("auth.http.recovery_code.list")(function* ({ request, }) {
|
|
1186
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.list");
|
|
1187
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1188
|
+
const codes = yield* dependencies.recoveryCodeManagement
|
|
1189
|
+
.listForUser({ userId: current.currentSession.userId })
|
|
1190
|
+
.pipe(Effect.mapError(() => recoveryCodeInternalError("Failed to list recovery codes")));
|
|
1191
|
+
return { codes: codes.map(recoveryCodeInfoHttpBody) };
|
|
1192
|
+
});
|
|
1193
|
+
export const makeRecoveryCodeRevokeHandler = (dependencies) => Effect.fn("auth.http.recovery_code.revoke")(function* ({ payload, request, }) {
|
|
1194
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "recovery_code.revoke");
|
|
1195
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1196
|
+
yield* dependencies.recoveryCodeManagement
|
|
1197
|
+
.revokeForUser(recoveryCodeRevokeInput(current.currentSession.userId, payload))
|
|
1198
|
+
.pipe(Effect.mapError(recoveryCodeManagementHttpError));
|
|
1199
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
1200
|
+
});
|
|
1201
|
+
export const makeApiKeyCreateHandler = (dependencies) => Effect.fn("auth.http.api_key.create")(function* ({ payload, request, }) {
|
|
1202
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "api_key.create");
|
|
1203
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1204
|
+
const created = yield* dependencies.apiKeyManagement
|
|
1205
|
+
.createForUser(apiKeyCreateInput(current.currentSession.userId, payload))
|
|
1206
|
+
.pipe(Effect.mapError(apiKeyManagementHttpError));
|
|
1207
|
+
return apiKeyCreatedHttpBody(created);
|
|
1208
|
+
});
|
|
1209
|
+
export const makeApiKeyListHandler = (dependencies) => Effect.fn("auth.http.api_key.list")(function* ({ request, }) {
|
|
1210
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "api_key.list");
|
|
1211
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1212
|
+
const keys = yield* dependencies.apiKeyManagement
|
|
1213
|
+
.listForUser({ userId: current.currentSession.userId })
|
|
1214
|
+
.pipe(Effect.mapError(() => apiKeyInternalError("Failed to list API keys")));
|
|
1215
|
+
return { keys: keys.map(apiKeyInfoHttpBody) };
|
|
1216
|
+
});
|
|
1217
|
+
export const makeApiKeyRevokeHandler = (dependencies) => Effect.fn("auth.http.api_key.revoke")(function* ({ payload, request, }) {
|
|
1218
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "api_key.revoke");
|
|
1219
|
+
const current = yield* readRequiredSession(dependencies, request);
|
|
1220
|
+
yield* dependencies.apiKeyManagement
|
|
1221
|
+
.revokeForUser(apiKeyRevokeInput(current.currentSession.userId, payload))
|
|
1222
|
+
.pipe(Effect.mapError(apiKeyManagementHttpError));
|
|
1223
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
1224
|
+
});
|
|
1225
|
+
export const makeRefreshTokenRefreshHandler = (dependencies) => Effect.fn("auth.http.refresh_token.refresh")(function* ({ payload, }) {
|
|
1226
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "refresh_token.refresh");
|
|
1227
|
+
const now = yield* currentUnixMillis;
|
|
1228
|
+
const expiresAt = dependencies.refreshTokenExpiresAt({ now });
|
|
1229
|
+
const rotated = yield* dependencies.refreshTokenManagement
|
|
1230
|
+
.rotate(refreshTokenRefreshInput(payload, now, expiresAt))
|
|
1231
|
+
.pipe(Effect.mapError(() => refreshTokenInternalError("Failed to rotate refresh token")));
|
|
1232
|
+
switch (rotated._tag) {
|
|
1233
|
+
case "Invalid": {
|
|
1234
|
+
return yield* refreshTokenInvalidCredentials();
|
|
1235
|
+
}
|
|
1236
|
+
case "ReuseDetected": {
|
|
1237
|
+
return yield* refreshTokenInvalidCredentials("Refresh token reuse detected");
|
|
1238
|
+
}
|
|
1239
|
+
case "Rotated": {
|
|
1240
|
+
const accessToken = yield* dependencies.jwtIssuer
|
|
1241
|
+
.issue(dependencies.accessToken({
|
|
1242
|
+
now,
|
|
1243
|
+
refreshToken: rotated.refreshToken,
|
|
1244
|
+
previousToken: rotated.previousToken,
|
|
1245
|
+
}))
|
|
1246
|
+
.pipe(Effect.mapError(() => refreshTokenInternalError("Failed to issue access token")));
|
|
1247
|
+
return refreshTokenRefreshedHttpBody({
|
|
1248
|
+
accessToken,
|
|
1249
|
+
refreshToken: Redacted.value(rotated.token),
|
|
1250
|
+
refreshTokenRecord: rotated.refreshToken,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
export const makeJwtIntrospectHandler = (dependencies) => Effect.fn("auth.http.jwt.introspect")(function* ({ payload, }) {
|
|
1256
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "jwt.introspect");
|
|
1257
|
+
const result = yield* dependencies.jwtRevocation
|
|
1258
|
+
.introspect(jwtIntrospectInput(payload, dependencies))
|
|
1259
|
+
.pipe(Effect.mapError(() => jwtInternalError("Failed to introspect JWT access token")));
|
|
1260
|
+
return jwtIntrospectionHttpBody(result);
|
|
1261
|
+
});
|
|
1262
|
+
export const makeJwtRevokeHandler = (dependencies) => Effect.fn("auth.http.jwt.revoke")(function* ({ payload }) {
|
|
1263
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "jwt.revoke");
|
|
1264
|
+
yield* dependencies.jwtRevocation
|
|
1265
|
+
.revokeJwtId(jwtRevokeInput(payload))
|
|
1266
|
+
.pipe(Effect.mapError(jwtRevocationHttpError));
|
|
1267
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
1268
|
+
});
|
|
1269
|
+
export const makeMfaOptionsHandler = (dependencies) => Effect.fn("auth.http.mfa.options")(function* ({ payload, }) {
|
|
1270
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.options");
|
|
1271
|
+
const pending = yield* dependencies.authFlowState
|
|
1272
|
+
.inspect(payload.flowId)
|
|
1273
|
+
.pipe(Effect.mapError(() => mfaBadRequest()));
|
|
1274
|
+
const factors = [];
|
|
1275
|
+
if (dependencies.totpFactorManagement !== undefined) {
|
|
1276
|
+
const totpFactors = yield* dependencies.totpFactorManagement
|
|
1277
|
+
.listForUser({ userId: pending.userId })
|
|
1278
|
+
.pipe(Effect.mapError(() => mfaInternalError("Failed to list MFA options")));
|
|
1279
|
+
if (totpFactors.length > 0) {
|
|
1280
|
+
factors.push({ type: "totp" });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (dependencies.recoveryCodeManagement !== undefined) {
|
|
1284
|
+
const recoveryCodes = yield* dependencies.recoveryCodeManagement
|
|
1285
|
+
.listForUser({ userId: pending.userId })
|
|
1286
|
+
.pipe(Effect.mapError(() => mfaInternalError("Failed to list MFA options")));
|
|
1287
|
+
if (recoveryCodes.length > 0) {
|
|
1288
|
+
factors.push({ type: "backup-code" });
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (dependencies.passkeyCredentialManagement !== undefined) {
|
|
1292
|
+
const credentials = yield* dependencies.passkeyCredentialManagement
|
|
1293
|
+
.listForUser({ userId: pending.userId })
|
|
1294
|
+
.pipe(Effect.mapError(() => mfaInternalError("Failed to list MFA options")));
|
|
1295
|
+
if (credentials.length > 0) {
|
|
1296
|
+
factors.push({ type: "passkey" });
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return { factors };
|
|
1300
|
+
});
|
|
1301
|
+
export const makeTotpMfaVerifyHandler = (dependencies) => Effect.fn("auth.http.mfa.totp_verify")(function* ({ payload, }) {
|
|
1302
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.totp_verify");
|
|
1303
|
+
const verified = yield* dependencies.totpFactorManagement
|
|
1304
|
+
.verifyForUser(totpMfaVerifyInput(payload, dependencies))
|
|
1305
|
+
.pipe(Effect.mapError(() => mfaInternalError("Failed to verify TOTP MFA factor")));
|
|
1306
|
+
if (!verified.valid) {
|
|
1307
|
+
return yield* mfaInvalidCredentials();
|
|
1308
|
+
}
|
|
1309
|
+
const result = yield* dependencies.authFlow
|
|
1310
|
+
.completeMfa({
|
|
1311
|
+
flowId: payload.flowId,
|
|
1312
|
+
userId: payload.userId,
|
|
1313
|
+
method: "totp",
|
|
1314
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
1315
|
+
})
|
|
1316
|
+
.pipe(Effect.mapError(authFlowMfaHttpError));
|
|
1317
|
+
if (result._tag === "PolicyDenied") {
|
|
1318
|
+
return yield* mfaPolicyDenied(result.reason);
|
|
1319
|
+
}
|
|
1320
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
|
|
1321
|
+
});
|
|
1322
|
+
export const makeTotpMfaVerifyForFlowHandler = (dependencies) => Effect.fn("auth.http.mfa.totp_verify_for_flow")(function* ({ payload, }) {
|
|
1323
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.totp_verify_for_flow");
|
|
1324
|
+
const pending = yield* dependencies.authFlowState
|
|
1325
|
+
.inspect(payload.flowId)
|
|
1326
|
+
.pipe(Effect.mapError(() => mfaBadRequest()));
|
|
1327
|
+
const verified = yield* dependencies.totpFactorManagement
|
|
1328
|
+
.verifyForUser(totpMfaVerifyForFlowInput(pending.userId, payload, dependencies))
|
|
1329
|
+
.pipe(Effect.mapError(() => mfaInternalError("Failed to verify TOTP MFA factor")));
|
|
1330
|
+
if (!verified.valid) {
|
|
1331
|
+
return yield* mfaInvalidCredentials();
|
|
1332
|
+
}
|
|
1333
|
+
const result = yield* dependencies.authFlow
|
|
1334
|
+
.completeMfa({
|
|
1335
|
+
flowId: payload.flowId,
|
|
1336
|
+
userId: pending.userId,
|
|
1337
|
+
method: "totp",
|
|
1338
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
1339
|
+
})
|
|
1340
|
+
.pipe(Effect.mapError(authFlowMfaHttpError));
|
|
1341
|
+
if (result._tag === "PolicyDenied") {
|
|
1342
|
+
return yield* mfaPolicyDenied(result.reason);
|
|
1343
|
+
}
|
|
1344
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
|
|
1345
|
+
});
|
|
1346
|
+
export const makeRecoveryCodeMfaVerifyHandler = (dependencies) => Effect.fn("auth.http.mfa.recovery_code_verify")(function* ({ payload, }) {
|
|
1347
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.recovery_code_verify");
|
|
1348
|
+
const verified = yield* dependencies.recoveryCodeManagement
|
|
1349
|
+
.verifyForUser(recoveryCodeMfaVerifyInput(payload))
|
|
1350
|
+
.pipe(Effect.mapError(() => mfaInternalError("Failed to verify recovery code MFA factor")));
|
|
1351
|
+
if (!verified.valid) {
|
|
1352
|
+
return yield* mfaInvalidCredentials();
|
|
1353
|
+
}
|
|
1354
|
+
const result = yield* dependencies.authFlow
|
|
1355
|
+
.completeMfa({
|
|
1356
|
+
flowId: payload.flowId,
|
|
1357
|
+
userId: payload.userId,
|
|
1358
|
+
method: "backup-code",
|
|
1359
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
1360
|
+
})
|
|
1361
|
+
.pipe(Effect.mapError(authFlowMfaHttpError));
|
|
1362
|
+
if (result._tag === "PolicyDenied") {
|
|
1363
|
+
return yield* mfaPolicyDenied(result.reason);
|
|
1364
|
+
}
|
|
1365
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
|
|
1366
|
+
});
|
|
1367
|
+
export const makeRecoveryCodeMfaVerifyForFlowHandler = (dependencies) => Effect.fn("auth.http.mfa.recovery_code_verify_for_flow")(function* ({ payload, }) {
|
|
1368
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.recovery_code_verify_for_flow");
|
|
1369
|
+
const pending = yield* dependencies.authFlowState
|
|
1370
|
+
.inspect(payload.flowId)
|
|
1371
|
+
.pipe(Effect.mapError(() => mfaBadRequest()));
|
|
1372
|
+
const verified = yield* dependencies.recoveryCodeManagement
|
|
1373
|
+
.verifyForUser(recoveryCodeMfaVerifyForFlowInput(pending.userId, payload))
|
|
1374
|
+
.pipe(Effect.mapError(() => mfaInternalError("Failed to verify recovery code MFA factor")));
|
|
1375
|
+
if (!verified.valid) {
|
|
1376
|
+
return yield* mfaInvalidCredentials();
|
|
1377
|
+
}
|
|
1378
|
+
const result = yield* dependencies.authFlow
|
|
1379
|
+
.completeMfa({
|
|
1380
|
+
flowId: payload.flowId,
|
|
1381
|
+
userId: pending.userId,
|
|
1382
|
+
method: "backup-code",
|
|
1383
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
1384
|
+
})
|
|
1385
|
+
.pipe(Effect.mapError(authFlowMfaHttpError));
|
|
1386
|
+
if (result._tag === "PolicyDenied") {
|
|
1387
|
+
return yield* mfaPolicyDenied(result.reason);
|
|
1388
|
+
}
|
|
1389
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
|
|
1390
|
+
});
|
|
1391
|
+
export const makePasskeyMfaStartHandler = (dependencies) => Effect.fn("auth.http.mfa.passkey_start")(function* ({ payload, }) {
|
|
1392
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.passkey_start");
|
|
1393
|
+
const pending = yield* dependencies.authFlowState
|
|
1394
|
+
.inspect(payload.flowId)
|
|
1395
|
+
.pipe(Effect.mapError(() => mfaBadRequest()));
|
|
1396
|
+
return yield* dependencies.passkeyOptions
|
|
1397
|
+
.startAuthentication(passkeyMfaStartInput(pending.userId, payload, dependencies))
|
|
1398
|
+
.pipe(Effect.mapError(() => passkeyInternalError("Failed to start passkey MFA authentication")));
|
|
1399
|
+
});
|
|
1400
|
+
export const makePasskeyMfaVerifyHandler = (dependencies) => Effect.fn("auth.http.mfa.passkey_verify")(function* ({ payload, }) {
|
|
1401
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "mfa.passkey_verify");
|
|
1402
|
+
const finished = yield* dependencies.passkeyVerification
|
|
1403
|
+
.finishAuthentication(passkeyMfaVerifyInput(payload, dependencies))
|
|
1404
|
+
.pipe(Effect.mapError(passkeyVerificationHttpError));
|
|
1405
|
+
const result = yield* dependencies.authFlow
|
|
1406
|
+
.completeMfa({
|
|
1407
|
+
flowId: payload.flowId,
|
|
1408
|
+
userId: finished.userId,
|
|
1409
|
+
method: "passkey",
|
|
1410
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }),
|
|
1411
|
+
})
|
|
1412
|
+
.pipe(Effect.mapError(authFlowMfaHttpError));
|
|
1413
|
+
if (result._tag === "PolicyDenied") {
|
|
1414
|
+
return yield* mfaPolicyDenied(result.reason);
|
|
1415
|
+
}
|
|
1416
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(result.session);
|
|
1417
|
+
});
|
|
1418
|
+
export const makeLoginApprovalApproveHandler = (dependencies) => Effect.fn("auth.http.login_approval.approve")(function* ({ payload, request, }) {
|
|
1419
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "login_approval.approve");
|
|
1420
|
+
const input = loginApprovalApproveInput(payload);
|
|
1421
|
+
const approval = yield* dependencies.loginApproval
|
|
1422
|
+
.verify(input.approval)
|
|
1423
|
+
.pipe(Effect.mapError(() => loginApprovalBadRequest()));
|
|
1424
|
+
const approvedFlowId = metadataString(approval.metadata, "flowId");
|
|
1425
|
+
if (approvedFlowId !== input.flowId) {
|
|
1426
|
+
return yield* loginApprovalBadRequest();
|
|
1427
|
+
}
|
|
1428
|
+
if (approval.sessionBinding === "none") {
|
|
1429
|
+
return yield* loginApprovalNoSessionBindingDenied();
|
|
1430
|
+
}
|
|
1431
|
+
const sameDeviceSecret = approval.sameDeviceRequired
|
|
1432
|
+
? yield* dependencies.loginApprovalPendingCookie
|
|
1433
|
+
.read(new Request("http://localhost", { headers: request.headers }))
|
|
1434
|
+
.pipe(Effect.flatMap((secret) => Option.isSome(secret)
|
|
1435
|
+
? Effect.succeed(secret.value)
|
|
1436
|
+
: Effect.fail(loginApprovalSameDeviceDenied())))
|
|
1437
|
+
: undefined;
|
|
1438
|
+
const consumed = yield* dependencies.authFlowState
|
|
1439
|
+
.consume(input.flowId, sameDeviceSecret)
|
|
1440
|
+
.pipe(Effect.mapError(() => approval.sameDeviceRequired
|
|
1441
|
+
? loginApprovalSameDeviceDenied()
|
|
1442
|
+
: loginApprovalBadRequest()));
|
|
1443
|
+
if (consumed.userId !== approval.userId) {
|
|
1444
|
+
return yield* loginApprovalBadRequest();
|
|
1445
|
+
}
|
|
1446
|
+
if (approval.markEmailVerified === true) {
|
|
1447
|
+
const updated = yield* dependencies.users
|
|
1448
|
+
.markEmailVerified({
|
|
1449
|
+
userId: approval.userId,
|
|
1450
|
+
updatedAt: yield* currentUnixMillis,
|
|
1451
|
+
})
|
|
1452
|
+
.pipe(Effect.mapError(() => passwordInternalError("Failed to mark email verified")));
|
|
1453
|
+
if (Option.isNone(updated)) {
|
|
1454
|
+
return yield* loginApprovalBadRequest();
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const result = yield* dependencies.authFlow
|
|
1458
|
+
.complete(consumed)
|
|
1459
|
+
.pipe(Effect.mapError(() => passwordInternalError("Failed to complete login approval")));
|
|
1460
|
+
const setCookies = yield* trustedDeviceSetCookies({
|
|
1461
|
+
trustedDevice: dependencies.trustedDevice,
|
|
1462
|
+
trustedDeviceCookie: dependencies.trustedDeviceCookie,
|
|
1463
|
+
userId: consumed.userId,
|
|
1464
|
+
rememberDevice: input.rememberDevice,
|
|
1465
|
+
});
|
|
1466
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(result.session, { setCookies });
|
|
1467
|
+
});
|
|
1468
|
+
export const makeLoginApprovalStatusHandler = (dependencies) => Effect.fn("auth.http.login_approval.status")(function* ({ payload, }) {
|
|
1469
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "login_approval.status");
|
|
1470
|
+
if (dependencies.loginApprovalReview === undefined) {
|
|
1471
|
+
return yield* loginApprovalReviewNotConfigured();
|
|
1472
|
+
}
|
|
1473
|
+
const input = loginApprovalStatusInput(payload);
|
|
1474
|
+
const recordOption = yield* dependencies.loginApprovalReview
|
|
1475
|
+
.status({ approvalChallengeId: input.approvalChallengeId })
|
|
1476
|
+
.pipe(Effect.mapError(() => passwordInternalError("Failed to load login approval review")));
|
|
1477
|
+
if (Option.isNone(recordOption)) {
|
|
1478
|
+
return yield* loginApprovalBadRequest();
|
|
1479
|
+
}
|
|
1480
|
+
const record = recordOption.value;
|
|
1481
|
+
if (record.flowId !== input.flowId) {
|
|
1482
|
+
return yield* loginApprovalBadRequest();
|
|
1483
|
+
}
|
|
1484
|
+
const now = yield* currentUnixMillis;
|
|
1485
|
+
const status = Number(record.expiresAt) <= Number(now)
|
|
1486
|
+
? "expired"
|
|
1487
|
+
: record.status;
|
|
1488
|
+
return {
|
|
1489
|
+
status,
|
|
1490
|
+
expiresAt: record.expiresAt,
|
|
1491
|
+
};
|
|
1492
|
+
});
|
|
1493
|
+
export const makeLoginApprovalFinalizeHandler = (dependencies) => Effect.fn("auth.http.login_approval.finalize")(function* ({ payload, request, }) {
|
|
1494
|
+
yield* Effect.annotateCurrentSpan("auth.http.endpoint", "login_approval.finalize");
|
|
1495
|
+
if (dependencies.loginApprovalFinalize === undefined) {
|
|
1496
|
+
return yield* loginApprovalFinalizeNotConfigured();
|
|
1497
|
+
}
|
|
1498
|
+
const input = loginApprovalFinalizeInput(payload);
|
|
1499
|
+
const sameDeviceSecret = yield* dependencies.loginApprovalPendingCookie.read(new Request("http://localhost", { headers: request.headers }));
|
|
1500
|
+
const result = yield* dependencies.loginApprovalFinalize
|
|
1501
|
+
.complete({
|
|
1502
|
+
flowId: input.flowId,
|
|
1503
|
+
approvalChallengeId: input.approvalChallengeId,
|
|
1504
|
+
...(Option.isSome(sameDeviceSecret)
|
|
1505
|
+
? { sameDeviceSecret: sameDeviceSecret.value }
|
|
1506
|
+
: {}),
|
|
1507
|
+
})
|
|
1508
|
+
.pipe(Effect.mapError((error) => error._tag === "AuthFlowStateError"
|
|
1509
|
+
? loginApprovalSameDeviceDenied()
|
|
1510
|
+
: passwordInternalError("Failed to finalize login approval")));
|
|
1511
|
+
if (result._tag === "PolicyDenied") {
|
|
1512
|
+
return yield* loginApprovalPolicyDenied(result.reason);
|
|
1513
|
+
}
|
|
1514
|
+
const setCookies = yield* trustedDeviceSetCookies({
|
|
1515
|
+
trustedDevice: dependencies.trustedDevice,
|
|
1516
|
+
trustedDeviceCookie: dependencies.trustedDeviceCookie,
|
|
1517
|
+
userId: result.session.userId,
|
|
1518
|
+
rememberDevice: input.rememberDevice,
|
|
1519
|
+
});
|
|
1520
|
+
return yield* dependencies.authHttp.commitAuthenticatedSession(result.session, { setCookies });
|
|
1521
|
+
});
|
|
269
1522
|
export class PasswordApi extends HttpApiGroup.make("password")
|
|
270
|
-
.add(passwordSignInEndpoint.middleware(AuthRateLimitMiddleware), passwordSignUpEndpoint.middleware(AuthRateLimitMiddleware))
|
|
1523
|
+
.add(passwordSignInEndpoint.middleware(AuthRateLimitMiddleware), passwordSignUpEndpoint.middleware(AuthRateLimitMiddleware), passwordResetStartEndpoint.middleware(AuthRateLimitMiddleware), passwordResetVerifyEndpoint.middleware(AuthRateLimitMiddleware), passwordSetEndpoint.middleware(AuthOriginCheckMiddleware), passwordChangeEndpoint.middleware(AuthOriginCheckMiddleware))
|
|
271
1524
|
.prefix("/auth/password")
|
|
272
1525
|
.middleware(AuthSchemaErrorMiddleware)
|
|
273
1526
|
.middleware(AuthOriginCheckMiddleware) {
|
|
274
1527
|
}
|
|
275
1528
|
export class SessionApi extends HttpApiGroup.make("session")
|
|
276
|
-
.add(currentSessionEndpoint, refreshSessionEndpoint.middleware(AuthOriginCheckMiddleware), logoutEndpoint.middleware(AuthOriginCheckMiddleware))
|
|
1529
|
+
.add(currentSessionEndpoint, refreshSessionEndpoint.middleware(AuthOriginCheckMiddleware), logoutEndpoint.middleware(AuthOriginCheckMiddleware), listSessionsEndpoint, revokeSessionEndpoint.middleware(AuthOriginCheckMiddleware), revokeOtherSessionsEndpoint.middleware(AuthOriginCheckMiddleware))
|
|
277
1530
|
.prefix("/auth") {
|
|
278
1531
|
}
|
|
279
1532
|
export class EmailVerificationApi extends HttpApiGroup.make("emailVerification")
|
|
@@ -282,29 +1535,104 @@ export class EmailVerificationApi extends HttpApiGroup.make("emailVerification")
|
|
|
282
1535
|
.middleware(AuthSchemaErrorMiddleware)
|
|
283
1536
|
.middleware(AuthOriginCheckMiddleware) {
|
|
284
1537
|
}
|
|
1538
|
+
export class EmailOtpApi extends HttpApiGroup.make("emailOtp")
|
|
1539
|
+
.add(emailOtpStartEndpoint.middleware(AuthRateLimitMiddleware), emailOtpVerifyEndpoint.middleware(AuthRateLimitMiddleware))
|
|
1540
|
+
.prefix("/auth/email-otp")
|
|
1541
|
+
.middleware(AuthSchemaErrorMiddleware)
|
|
1542
|
+
.middleware(AuthOriginCheckMiddleware) {
|
|
1543
|
+
}
|
|
1544
|
+
export class MagicLinkApi extends HttpApiGroup.make("magicLink")
|
|
1545
|
+
.add(magicLinkStartEndpoint.middleware(AuthRateLimitMiddleware), magicLinkVerifyEndpoint.middleware(AuthRateLimitMiddleware))
|
|
1546
|
+
.prefix("/auth/magic-link")
|
|
1547
|
+
.middleware(AuthSchemaErrorMiddleware)
|
|
1548
|
+
.middleware(AuthOriginCheckMiddleware) {
|
|
1549
|
+
}
|
|
1550
|
+
export class LoginApprovalApi extends HttpApiGroup.make("loginApproval")
|
|
1551
|
+
.add(loginApprovalApproveEndpoint, loginApprovalStatusEndpoint, loginApprovalFinalizeEndpoint)
|
|
1552
|
+
.prefix("/auth/login-approval")
|
|
1553
|
+
.middleware(AuthSchemaErrorMiddleware)
|
|
1554
|
+
.middleware(AuthOriginCheckMiddleware) {
|
|
1555
|
+
}
|
|
1556
|
+
export class MfaApi extends HttpApiGroup.make("mfa")
|
|
1557
|
+
.add(mfaOptionsEndpoint, totpMfaVerifyEndpoint, totpMfaVerifyForFlowEndpoint, recoveryCodeMfaVerifyEndpoint, recoveryCodeMfaVerifyForFlowEndpoint, passkeyMfaStartEndpoint, passkeyMfaVerifyEndpoint)
|
|
1558
|
+
.prefix("/auth/mfa")
|
|
1559
|
+
.middleware(AuthSchemaErrorMiddleware)
|
|
1560
|
+
.middleware(AuthOriginCheckMiddleware) {
|
|
1561
|
+
}
|
|
1562
|
+
export class ApiKeyApi extends HttpApiGroup.make("apiKey")
|
|
1563
|
+
.add(apiKeyCreateEndpoint, apiKeyListEndpoint, apiKeyRevokeEndpoint)
|
|
1564
|
+
.prefix("/auth/api-keys")
|
|
1565
|
+
.middleware(AuthSchemaErrorMiddleware)
|
|
1566
|
+
.middleware(AuthOriginCheckMiddleware) {
|
|
1567
|
+
}
|
|
1568
|
+
export class RefreshTokenApi extends HttpApiGroup.make("refreshToken")
|
|
1569
|
+
.add(refreshTokenRefreshEndpoint)
|
|
1570
|
+
.prefix("/auth/token")
|
|
1571
|
+
.middleware(AuthSchemaErrorMiddleware) {
|
|
1572
|
+
}
|
|
1573
|
+
export class JwtApi extends HttpApiGroup.make("jwt")
|
|
1574
|
+
.add(jwtIntrospectEndpoint, jwtRevokeEndpoint)
|
|
1575
|
+
.prefix("/auth/jwt")
|
|
1576
|
+
.middleware(AuthSchemaErrorMiddleware) {
|
|
1577
|
+
}
|
|
1578
|
+
export class SecurityApi extends HttpApiGroup.make("security")
|
|
1579
|
+
.add(loginNotificationReportEndpoint)
|
|
1580
|
+
.prefix("/auth/security")
|
|
1581
|
+
.middleware(AuthSchemaErrorMiddleware)
|
|
1582
|
+
.middleware(AuthOriginCheckMiddleware) {
|
|
1583
|
+
}
|
|
285
1584
|
export class AuthApi extends HttpApi.make("AuthApi")
|
|
286
1585
|
.add(PasswordApi)
|
|
287
1586
|
.add(SessionApi)
|
|
288
|
-
.add(EmailVerificationApi)
|
|
1587
|
+
.add(EmailVerificationApi)
|
|
1588
|
+
.add(EmailOtpApi)
|
|
1589
|
+
.add(MagicLinkApi)
|
|
1590
|
+
.add(LoginApprovalApi)
|
|
1591
|
+
.add(SecurityApi) {
|
|
289
1592
|
}
|
|
290
1593
|
export const PasswordApiLive = HttpApiBuilder.group(AuthApi, "password", Effect.fn("auth.http.password.group")(function* (handlers) {
|
|
291
1594
|
const password = yield* PasswordLogin;
|
|
292
1595
|
const registration = yield* PasswordRegistration;
|
|
1596
|
+
const passwordReset = yield* PasswordReset;
|
|
1597
|
+
const passwordManagement = yield* PasswordManagement;
|
|
293
1598
|
const authHttp = yield* AuthHttp;
|
|
294
1599
|
const emailVerificationFlow = yield* EmailVerificationFlow;
|
|
1600
|
+
const sessions = yield* Sessions;
|
|
1601
|
+
const sessionCookie = yield* SessionCookie;
|
|
295
1602
|
const authApiRateLimit = yield* AuthApiRateLimit;
|
|
1603
|
+
const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
|
|
296
1604
|
return handlers
|
|
297
1605
|
.handle("signIn", makePasswordSignInHandler({
|
|
298
1606
|
password,
|
|
299
1607
|
authHttp,
|
|
300
1608
|
emailVerificationFlow,
|
|
301
1609
|
authApiRateLimit,
|
|
1610
|
+
...(Option.isSome(trustedDeviceCookie)
|
|
1611
|
+
? { trustedDeviceCookie: trustedDeviceCookie.value }
|
|
1612
|
+
: {}),
|
|
302
1613
|
}))
|
|
303
1614
|
.handle("signUp", makePasswordSignUpHandler({
|
|
304
1615
|
registration,
|
|
305
1616
|
authHttp,
|
|
306
1617
|
emailVerificationFlow,
|
|
307
1618
|
authApiRateLimit,
|
|
1619
|
+
}))
|
|
1620
|
+
.handle("resetStart", makePasswordResetStartHandler({
|
|
1621
|
+
passwordReset,
|
|
1622
|
+
authApiRateLimit,
|
|
1623
|
+
}))
|
|
1624
|
+
.handle("resetVerify", makePasswordResetVerifyHandler({
|
|
1625
|
+
passwordReset,
|
|
1626
|
+
}))
|
|
1627
|
+
.handle("set", makePasswordSetHandler({
|
|
1628
|
+
passwordManagement,
|
|
1629
|
+
sessions,
|
|
1630
|
+
sessionCookie,
|
|
1631
|
+
}))
|
|
1632
|
+
.handle("change", makePasswordChangeHandler({
|
|
1633
|
+
passwordManagement,
|
|
1634
|
+
sessions,
|
|
1635
|
+
sessionCookie,
|
|
308
1636
|
}));
|
|
309
1637
|
}));
|
|
310
1638
|
export const SessionApiLive = HttpApiBuilder.group(AuthApi, "session", Effect.fn("auth.http.session.group")(function* (handlers) {
|
|
@@ -314,7 +1642,10 @@ export const SessionApiLive = HttpApiBuilder.group(AuthApi, "session", Effect.fn
|
|
|
314
1642
|
return handlers
|
|
315
1643
|
.handle("current", makeCurrentSessionHandler({ sessions, sessionCookie, authHttp }))
|
|
316
1644
|
.handle("refresh", makeRefreshSessionHandler({ sessions, sessionCookie, authHttp }))
|
|
317
|
-
.handle("logout", makeLogoutHandler({ sessions, sessionCookie, authHttp }))
|
|
1645
|
+
.handle("logout", makeLogoutHandler({ sessions, sessionCookie, authHttp }))
|
|
1646
|
+
.handle("list", makeListSessionsHandler({ sessions, sessionCookie }))
|
|
1647
|
+
.handle("revoke", makeRevokeSessionHandler({ sessions, sessionCookie, authHttp }))
|
|
1648
|
+
.handle("revokeOthers", makeRevokeOtherSessionsHandler({ sessions, sessionCookie }));
|
|
318
1649
|
}));
|
|
319
1650
|
export const EmailVerificationApiLive = HttpApiBuilder.group(AuthApi, "emailVerification", Effect.fn("auth.http.email_verification.group")(function* (handlers) {
|
|
320
1651
|
const emailVerificationFlow = yield* EmailVerificationFlow;
|
|
@@ -333,7 +1664,89 @@ export const EmailVerificationApiLive = HttpApiBuilder.group(AuthApi, "emailVeri
|
|
|
333
1664
|
sessionCookie,
|
|
334
1665
|
}));
|
|
335
1666
|
}));
|
|
336
|
-
const
|
|
1667
|
+
export const EmailOtpApiLive = HttpApiBuilder.group(AuthApi, "emailOtp", Effect.fn("auth.http.email_otp.group")(function* (handlers) {
|
|
1668
|
+
const emailOtp = yield* EmailOtpLogin;
|
|
1669
|
+
const authHttp = yield* AuthHttp;
|
|
1670
|
+
const authApiRateLimit = yield* AuthApiRateLimit;
|
|
1671
|
+
const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
|
|
1672
|
+
return handlers
|
|
1673
|
+
.handle("start", makeEmailOtpStartHandler({ emailOtp, authApiRateLimit }))
|
|
1674
|
+
.handle("verify", makeEmailOtpVerifyHandler({
|
|
1675
|
+
emailOtp,
|
|
1676
|
+
authHttp,
|
|
1677
|
+
...(Option.isSome(trustedDeviceCookie)
|
|
1678
|
+
? { trustedDeviceCookie: trustedDeviceCookie.value }
|
|
1679
|
+
: {}),
|
|
1680
|
+
}));
|
|
1681
|
+
}));
|
|
1682
|
+
export const MagicLinkApiLive = HttpApiBuilder.group(AuthApi, "magicLink", Effect.fn("auth.http.magic_link.group")(function* (handlers) {
|
|
1683
|
+
const magicLink = yield* MagicLinkLogin;
|
|
1684
|
+
const authHttp = yield* AuthHttp;
|
|
1685
|
+
const authApiRateLimit = yield* AuthApiRateLimit;
|
|
1686
|
+
const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
|
|
1687
|
+
return handlers
|
|
1688
|
+
.handle("start", makeMagicLinkStartHandler({ magicLink, authApiRateLimit }))
|
|
1689
|
+
.handle("verify", makeMagicLinkVerifyHandler({
|
|
1690
|
+
magicLink,
|
|
1691
|
+
authHttp,
|
|
1692
|
+
...(Option.isSome(trustedDeviceCookie)
|
|
1693
|
+
? { trustedDeviceCookie: trustedDeviceCookie.value }
|
|
1694
|
+
: {}),
|
|
1695
|
+
}));
|
|
1696
|
+
}));
|
|
1697
|
+
export const LoginApprovalApiLive = HttpApiBuilder.group(AuthApi, "loginApproval", Effect.fn("auth.http.login_approval.group")(function* (handlers) {
|
|
1698
|
+
const loginApproval = yield* LoginApproval;
|
|
1699
|
+
const authFlowState = yield* AuthFlowState;
|
|
1700
|
+
const authFlow = yield* AuthFlow;
|
|
1701
|
+
const users = yield* UserStore;
|
|
1702
|
+
const authHttp = yield* AuthHttp;
|
|
1703
|
+
const loginApprovalReview = yield* Effect.serviceOption(LoginApprovalReview);
|
|
1704
|
+
const loginApprovalFinalize = yield* Effect.serviceOption(LoginApprovalFinalize);
|
|
1705
|
+
const pendingCookieOption = yield* Effect.serviceOption(LoginApprovalPendingCookie);
|
|
1706
|
+
const trustedDevice = yield* Effect.serviceOption(TrustedDevice);
|
|
1707
|
+
const trustedDeviceCookie = yield* Effect.serviceOption(TrustedDeviceCookie);
|
|
1708
|
+
const loginApprovalPendingCookie = Option.isSome(pendingCookieOption)
|
|
1709
|
+
? pendingCookieOption.value
|
|
1710
|
+
: makeLoginApprovalPendingCookie();
|
|
1711
|
+
return handlers
|
|
1712
|
+
.handle("approve", makeLoginApprovalApproveHandler({
|
|
1713
|
+
loginApproval,
|
|
1714
|
+
authFlowState,
|
|
1715
|
+
authFlow,
|
|
1716
|
+
users,
|
|
1717
|
+
authHttp,
|
|
1718
|
+
loginApprovalPendingCookie,
|
|
1719
|
+
...(Option.isSome(trustedDevice)
|
|
1720
|
+
? { trustedDevice: trustedDevice.value }
|
|
1721
|
+
: {}),
|
|
1722
|
+
...(Option.isSome(trustedDeviceCookie)
|
|
1723
|
+
? { trustedDeviceCookie: trustedDeviceCookie.value }
|
|
1724
|
+
: {}),
|
|
1725
|
+
}))
|
|
1726
|
+
.handle("status", makeLoginApprovalStatusHandler(Option.isSome(loginApprovalReview)
|
|
1727
|
+
? { loginApprovalReview: loginApprovalReview.value }
|
|
1728
|
+
: {}))
|
|
1729
|
+
.handle("finalize", makeLoginApprovalFinalizeHandler({
|
|
1730
|
+
authHttp,
|
|
1731
|
+
loginApprovalPendingCookie,
|
|
1732
|
+
...(Option.isSome(loginApprovalFinalize)
|
|
1733
|
+
? { loginApprovalFinalize: loginApprovalFinalize.value }
|
|
1734
|
+
: {}),
|
|
1735
|
+
...(Option.isSome(trustedDevice)
|
|
1736
|
+
? { trustedDevice: trustedDevice.value }
|
|
1737
|
+
: {}),
|
|
1738
|
+
...(Option.isSome(trustedDeviceCookie)
|
|
1739
|
+
? { trustedDeviceCookie: trustedDeviceCookie.value }
|
|
1740
|
+
: {}),
|
|
1741
|
+
}));
|
|
1742
|
+
}));
|
|
1743
|
+
export const SecurityApiLive = HttpApiBuilder.group(AuthApi, "security", Effect.fn("auth.http.security.group")(function* (handlers) {
|
|
1744
|
+
const loginNotification = yield* Effect.serviceOption(LoginNotification);
|
|
1745
|
+
return handlers.handle("reportLogin", makeLoginNotificationReportHandler(Option.isSome(loginNotification)
|
|
1746
|
+
? { loginNotification: loginNotification.value }
|
|
1747
|
+
: {}));
|
|
1748
|
+
}));
|
|
1749
|
+
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
1750
|
export const AuthHttpApiBareLive = AuthHttpApiBaseLive.pipe(Layer.provide(AuthApiRateLimitNoopLive));
|
|
338
1751
|
export const AuthHttpApiLive = AuthHttpApiBaseLive.pipe(Layer.provide(AuthApiRateLimitLive));
|
|
339
1752
|
export { AuthResult };
|