@effect-auth/core 0.1.0-alpha.3 → 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.
Files changed (149) hide show
  1. package/README.md +148 -13
  2. package/dist/ApiKey.d.ts +207 -0
  3. package/dist/ApiKey.d.ts.map +1 -0
  4. package/dist/ApiKey.js +275 -0
  5. package/dist/ApiKey.js.map +1 -0
  6. package/dist/AuditLog.d.ts +204 -1
  7. package/dist/AuditLog.d.ts.map +1 -1
  8. package/dist/AuditLog.js +170 -0
  9. package/dist/AuditLog.js.map +1 -1
  10. package/dist/AuthConfig.d.ts +0 -9
  11. package/dist/AuthConfig.d.ts.map +1 -1
  12. package/dist/AuthConfig.js +1 -14
  13. package/dist/AuthConfig.js.map +1 -1
  14. package/dist/AuthFlow.d.ts +220 -51
  15. package/dist/AuthFlow.d.ts.map +1 -1
  16. package/dist/AuthFlow.js +550 -22
  17. package/dist/AuthFlow.js.map +1 -1
  18. package/dist/AuthKernel.d.ts +1 -1
  19. package/dist/Challenge.d.ts +5 -1
  20. package/dist/Challenge.d.ts.map +1 -1
  21. package/dist/Challenge.js +58 -48
  22. package/dist/Challenge.js.map +1 -1
  23. package/dist/Client.d.ts +242 -1
  24. package/dist/Client.d.ts.map +1 -1
  25. package/dist/Client.js +489 -1
  26. package/dist/Client.js.map +1 -1
  27. package/dist/D1Kysely.d.ts +1 -1
  28. package/dist/D1Kysely.d.ts.map +1 -1
  29. package/dist/EffectQbSqliteStorage.d.ts +115 -3
  30. package/dist/EffectQbSqliteStorage.d.ts.map +1 -1
  31. package/dist/EffectQbSqliteStorage.js +314 -4
  32. package/dist/EffectQbSqliteStorage.js.map +1 -1
  33. package/dist/EmailOtp.d.ts +5 -2
  34. package/dist/EmailOtp.d.ts.map +1 -1
  35. package/dist/EmailOtp.js +11 -4
  36. package/dist/EmailOtp.js.map +1 -1
  37. package/dist/EmailVerification.d.ts +2 -2
  38. package/dist/EmailVerification.d.ts.map +1 -1
  39. package/dist/EmailVerification.js +2 -3
  40. package/dist/EmailVerification.js.map +1 -1
  41. package/dist/HttpApi/Api.d.ts +1161 -19
  42. package/dist/HttpApi/Api.d.ts.map +1 -1
  43. package/dist/HttpApi/Api.js +1433 -20
  44. package/dist/HttpApi/Api.js.map +1 -1
  45. package/dist/HttpApi/Endpoints.d.ts +611 -0
  46. package/dist/HttpApi/Endpoints.d.ts.map +1 -1
  47. package/dist/HttpApi/Endpoints.js +242 -1
  48. package/dist/HttpApi/Endpoints.js.map +1 -1
  49. package/dist/HttpApi/Schemas.d.ts +704 -0
  50. package/dist/HttpApi/Schemas.d.ts.map +1 -1
  51. package/dist/HttpApi/Schemas.js +435 -1
  52. package/dist/HttpApi/Schemas.js.map +1 -1
  53. package/dist/HttpApi/Security.d.ts +13 -1
  54. package/dist/HttpApi/Security.d.ts.map +1 -1
  55. package/dist/HttpApi/Security.js +107 -16
  56. package/dist/HttpApi/Security.js.map +1 -1
  57. package/dist/HttpApi/Transport.d.ts +36 -2
  58. package/dist/HttpApi/Transport.d.ts.map +1 -1
  59. package/dist/HttpApi/Transport.js +151 -9
  60. package/dist/HttpApi/Transport.js.map +1 -1
  61. package/dist/Jwt.d.ts +271 -0
  62. package/dist/Jwt.d.ts.map +1 -0
  63. package/dist/Jwt.js +512 -0
  64. package/dist/Jwt.js.map +1 -0
  65. package/dist/KyselyStorage.d.ts +134 -1
  66. package/dist/KyselyStorage.d.ts.map +1 -1
  67. package/dist/KyselyStorage.js +918 -4
  68. package/dist/KyselyStorage.js.map +1 -1
  69. package/dist/LoginApproval.d.ts +314 -0
  70. package/dist/LoginApproval.d.ts.map +1 -0
  71. package/dist/LoginApproval.js +551 -0
  72. package/dist/LoginApproval.js.map +1 -0
  73. package/dist/LoginNotification.d.ts +183 -0
  74. package/dist/LoginNotification.d.ts.map +1 -0
  75. package/dist/LoginNotification.js +329 -0
  76. package/dist/LoginNotification.js.map +1 -0
  77. package/dist/LoginRisk.d.ts +73 -0
  78. package/dist/LoginRisk.d.ts.map +1 -0
  79. package/dist/LoginRisk.js +57 -0
  80. package/dist/LoginRisk.js.map +1 -0
  81. package/dist/MagicLink.d.ts +85 -0
  82. package/dist/MagicLink.d.ts.map +1 -0
  83. package/dist/MagicLink.js +206 -0
  84. package/dist/MagicLink.js.map +1 -0
  85. package/dist/Mailer.d.ts +33 -2
  86. package/dist/Mailer.d.ts.map +1 -1
  87. package/dist/Mailer.js +107 -0
  88. package/dist/Mailer.js.map +1 -1
  89. package/dist/Passkey.d.ts +334 -0
  90. package/dist/Passkey.d.ts.map +1 -0
  91. package/dist/Passkey.js +422 -0
  92. package/dist/Passkey.js.map +1 -0
  93. package/dist/Password.d.ts +138 -8
  94. package/dist/Password.d.ts.map +1 -1
  95. package/dist/Password.js +397 -8
  96. package/dist/Password.js.map +1 -1
  97. package/dist/RecoveryCode.d.ts +169 -0
  98. package/dist/RecoveryCode.d.ts.map +1 -0
  99. package/dist/RecoveryCode.js +299 -0
  100. package/dist/RecoveryCode.js.map +1 -0
  101. package/dist/RefreshToken.d.ts +222 -0
  102. package/dist/RefreshToken.d.ts.map +1 -0
  103. package/dist/RefreshToken.js +342 -0
  104. package/dist/RefreshToken.js.map +1 -0
  105. package/dist/Sessions.d.ts +52 -2
  106. package/dist/Sessions.d.ts.map +1 -1
  107. package/dist/Sessions.js +123 -1
  108. package/dist/Sessions.js.map +1 -1
  109. package/dist/StepUp.d.ts +44 -0
  110. package/dist/StepUp.d.ts.map +1 -0
  111. package/dist/StepUp.js +82 -0
  112. package/dist/StepUp.js.map +1 -0
  113. package/dist/Storage.d.ts +14 -1
  114. package/dist/Storage.d.ts.map +1 -1
  115. package/dist/Storage.js.map +1 -1
  116. package/dist/StorageMigrations.d.ts +9 -1
  117. package/dist/StorageMigrations.d.ts.map +1 -1
  118. package/dist/StorageMigrations.js +159 -0
  119. package/dist/StorageMigrations.js.map +1 -1
  120. package/dist/Testing.d.ts +1 -1
  121. package/dist/Totp.d.ts +220 -0
  122. package/dist/Totp.d.ts.map +1 -0
  123. package/dist/Totp.js +477 -0
  124. package/dist/Totp.js.map +1 -0
  125. package/dist/TrustedDevice.d.ts +115 -0
  126. package/dist/TrustedDevice.d.ts.map +1 -0
  127. package/dist/TrustedDevice.js +222 -0
  128. package/dist/TrustedDevice.js.map +1 -0
  129. package/dist/alchemy/cloudflare/Email.d.ts +25 -0
  130. package/dist/alchemy/cloudflare/Email.d.ts.map +1 -0
  131. package/dist/alchemy/cloudflare/Email.js +117 -0
  132. package/dist/alchemy/cloudflare/Email.js.map +1 -0
  133. package/dist/cloudflare/Email.d.ts +25 -0
  134. package/dist/cloudflare/Email.d.ts.map +1 -0
  135. package/dist/cloudflare/Email.js +153 -0
  136. package/dist/cloudflare/Email.js.map +1 -0
  137. package/dist/index.d.ts +12 -0
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +12 -0
  140. package/dist/index.js.map +1 -1
  141. package/migrations/0003_auth_login_approval_review.sql +22 -0
  142. package/migrations/0004_auth_trusted_device.sql +12 -0
  143. package/migrations/0005_auth_passkey.sql +17 -0
  144. package/migrations/0006_auth_totp_factor.sql +17 -0
  145. package/migrations/0007_auth_recovery_code.sql +13 -0
  146. package/migrations/0008_auth_api_key.sql +17 -0
  147. package/migrations/0009_auth_refresh_token.sql +21 -0
  148. package/migrations/0010_auth_jwt_revocation.sql +8 -0
  149. package/package.json +57 -1
package/dist/AuthFlow.js CHANGED
@@ -1,7 +1,16 @@
1
- import { Context, Data, Effect, Layer } from "effect";
1
+ // oxlint-disable complexity
2
+ import { Context, Data, Duration, Effect, Layer, Option, Redacted, } from "effect";
2
3
  import { AuthEmailVerificationSessionPolicyConfig } from "./AuthConfig.js";
3
- import { AuthFlowId } from "./Identifiers.js";
4
- import { Sessions } from "./Sessions.js";
4
+ import { Challenge } from "./Challenge.js";
5
+ import { Crypto } from "./Crypto.js";
6
+ import { AuthFlowId, ChallengeId, Email, UserId } from "./Identifiers.js";
7
+ import { currentUnixMillis } from "./Internal.js";
8
+ import { LoginApprovalFlow, LoginApprovalPolicy, LoginApprovalReview, makeLoginApprovalPolicy, unknownLoginDeviceContext, } from "./LoginApproval.js";
9
+ import { LoginNotification } from "./LoginNotification.js";
10
+ import { LoginRiskEngine, defaultLoginRiskContext, makeLoginRiskEngineNoop, } from "./LoginRisk.js";
11
+ import { Sessions, sessionRequestMetadataKey } from "./Sessions.js";
12
+ import { TrustedDevice } from "./TrustedDevice.js";
13
+ export const defaultLoginApprovalSameDeviceSecretBytes = 32;
5
14
  export class EmailVerificationSessionPolicy extends Context.Service()("auth/EmailVerificationSessionPolicy") {
6
15
  }
7
16
  (function (EmailVerificationSessionPolicy) {
@@ -10,13 +19,17 @@ export class EmailVerificationSessionPolicy extends Context.Service()("auth/Emai
10
19
  const addEmailVerificationRequirement = (requirements) => requirements?.includes("email_verification") === true
11
20
  ? requirements
12
21
  : [...(requirements ?? []), "email_verification"];
13
- const removeEmailVerificationRequirement = (requirements) => requirements?.filter((requirement) => requirement !== "email_verification");
22
+ const removeEmailVerificationRequirement = (requirements) => {
23
+ const filtered = requirements?.filter((requirement) => requirement !== "email_verification");
24
+ return filtered?.length === 0 ? undefined : filtered;
25
+ };
14
26
  const withEmailVerified = (claims, emailVerified) => {
27
+ const { requirements: _requirements, ...rest } = claims ?? {};
15
28
  const requirements = emailVerified
16
29
  ? removeEmailVerificationRequirement(claims?.requirements)
17
30
  : claims?.requirements;
18
31
  return {
19
- ...claims,
32
+ ...rest,
20
33
  emailVerified,
21
34
  ...(requirements === undefined ? {} : { requirements }),
22
35
  };
@@ -52,9 +65,6 @@ export const makeEmailVerificationSessionPolicy = (options = {}) => {
52
65
  claims: withEmailVerificationRequirement(input.claims),
53
66
  };
54
67
  }
55
- case "require-before-session": {
56
- return { _tag: "RequiresEmailVerification" };
57
- }
58
68
  }
59
69
  }),
60
70
  });
@@ -63,14 +73,389 @@ export const EmailVerificationSessionPolicyLive = (options) => Layer.succeed(Ema
63
73
  export const AuthResult = Data.taggedEnum();
64
74
  export class AuthFlowStateError extends Data.TaggedError("AuthFlowStateError") {
65
75
  }
76
+ export class AuthFlowState extends Context.Service()("auth/AuthFlowState") {
77
+ }
78
+ (function (AuthFlowState) {
79
+ AuthFlowState.make = (service) => AuthFlowState.of(service);
80
+ })(AuthFlowState || (AuthFlowState = {}));
81
+ export const defaultAuthFlowStateTtl = Duration.minutes(10);
82
+ const flowStateMetadata = (input) => ({
83
+ ...input.metadata,
84
+ method: input.method,
85
+ amr: input.amr,
86
+ aal: input.aal,
87
+ ...(input.email === undefined ? {} : { email: input.email }),
88
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
89
+ ...(input.device === undefined ? {} : { deviceStatus: input.device.status }),
90
+ ...(input.claims === undefined ? {} : { claims: input.claims }),
91
+ });
92
+ const metadataString = (metadata, key) => {
93
+ const value = metadata?.[key];
94
+ return typeof value === "string" && value.length > 0 ? value : undefined;
95
+ };
96
+ const metadataStringArray = (metadata, key) => {
97
+ const value = metadata?.[key];
98
+ return Array.isArray(value) && value.every((item) => typeof item === "string")
99
+ ? value
100
+ : undefined;
101
+ };
102
+ const metadataClaims = (metadata) => {
103
+ const value = metadata?.claims;
104
+ if (value === undefined) {
105
+ return undefined;
106
+ }
107
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
108
+ return undefined;
109
+ }
110
+ const shape = value;
111
+ const { requirements } = shape;
112
+ if (shape.emailVerified !== undefined &&
113
+ typeof shape.emailVerified !== "boolean") {
114
+ return undefined;
115
+ }
116
+ if (requirements !== undefined &&
117
+ (!Array.isArray(requirements) ||
118
+ !requirements.every((item) => typeof item === "string"))) {
119
+ return undefined;
120
+ }
121
+ return {
122
+ ...(shape.emailVerified === undefined
123
+ ? {}
124
+ : { emailVerified: shape.emailVerified }),
125
+ ...(requirements === undefined ? {} : { requirements }),
126
+ };
127
+ };
128
+ const metadataAal = (metadata) => {
129
+ const value = metadataString(metadata, "aal");
130
+ return value === "aal1" || value === "aal2" || value === "aal3"
131
+ ? value
132
+ : undefined;
133
+ };
134
+ const metadataIntent = (metadata) => {
135
+ const value = metadataString(metadata, "intent");
136
+ return value === "sign-in" || value === "sign-up" ? value : undefined;
137
+ };
138
+ const metadataDevice = (metadata) => {
139
+ const value = metadataString(metadata, "deviceStatus");
140
+ return value === "known" || value === "new" || value === "unknown"
141
+ ? { status: value }
142
+ : undefined;
143
+ };
144
+ const appMetadata = (metadata) => {
145
+ if (metadata === undefined) {
146
+ return undefined;
147
+ }
148
+ const { method: _method, amr: _amr, aal: _aal, email: _email, intent: _intent, deviceStatus: _deviceStatus, claims: _claims, ...rest } = metadata;
149
+ return Object.keys(rest).length === 0 ? undefined : rest;
150
+ };
151
+ const authFlowStateFromChallenge = (flowId, verified) => Effect.gen(function* () {
152
+ const method = metadataString(verified.metadata, "method");
153
+ const amr = metadataStringArray(verified.metadata, "amr");
154
+ const aal = metadataAal(verified.metadata);
155
+ const email = metadataString(verified.metadata, "email");
156
+ const intent = metadataIntent(verified.metadata);
157
+ const device = metadataDevice(verified.metadata);
158
+ const claims = metadataClaims(verified.metadata);
159
+ if (method === undefined) {
160
+ return yield* new AuthFlowStateError({
161
+ message: "Auth flow state is missing method metadata",
162
+ });
163
+ }
164
+ if (amr === undefined) {
165
+ return yield* new AuthFlowStateError({
166
+ message: "Auth flow state is missing AMR metadata",
167
+ });
168
+ }
169
+ if (aal === undefined) {
170
+ return yield* new AuthFlowStateError({
171
+ message: "Auth flow state is missing AAL metadata",
172
+ });
173
+ }
174
+ if (verified.metadata?.claims !== undefined && claims === undefined) {
175
+ return yield* new AuthFlowStateError({
176
+ message: "Auth flow state has invalid claims metadata",
177
+ });
178
+ }
179
+ return {
180
+ flowId,
181
+ userId: UserId(verified.subject),
182
+ ...(email === undefined ? {} : { email: Email(email) }),
183
+ ...(intent === undefined ? {} : { intent }),
184
+ method,
185
+ amr,
186
+ aal,
187
+ ...(device === undefined ? {} : { device }),
188
+ ...(claims === undefined ? {} : { claims }),
189
+ metadata: appMetadata(verified.metadata),
190
+ };
191
+ });
192
+ export const makeAuthFlowState = (dependencies) => {
193
+ const ttl = dependencies.ttl ?? defaultAuthFlowStateTtl;
194
+ return AuthFlowState.make({
195
+ start: Effect.fn("auth.auth_flow_state.start")(function* (input) {
196
+ const issued = yield* dependencies.challenge
197
+ .issue({
198
+ type: "auth-flow-state",
199
+ subject: input.userId,
200
+ ttl: input.ttl ?? ttl,
201
+ secret: input.secret,
202
+ metadata: flowStateMetadata(input),
203
+ })
204
+ .pipe(Effect.mapError((cause) => new AuthFlowStateError({
205
+ message: "Failed to start auth flow state",
206
+ cause,
207
+ })));
208
+ return {
209
+ flowId: AuthFlowId(issued.id),
210
+ userId: input.userId,
211
+ expiresAt: issued.expiresAt,
212
+ };
213
+ }),
214
+ inspect: Effect.fn("auth.auth_flow_state.inspect")(function* (flowId, secret) {
215
+ const inspected = yield* dependencies.challenge
216
+ .inspect({
217
+ challengeId: ChallengeId(flowId),
218
+ type: "auth-flow-state",
219
+ secret,
220
+ })
221
+ .pipe(Effect.mapError((cause) => new AuthFlowStateError({
222
+ message: "Invalid auth flow state",
223
+ cause,
224
+ })));
225
+ const state = yield* authFlowStateFromChallenge(flowId, inspected);
226
+ return {
227
+ ...state,
228
+ expiresAt: inspected.expiresAt,
229
+ };
230
+ }),
231
+ consume: Effect.fn("auth.auth_flow_state.consume")(function* (flowId, secret) {
232
+ const verified = yield* dependencies.challenge
233
+ .verify({
234
+ challengeId: ChallengeId(flowId),
235
+ type: "auth-flow-state",
236
+ secret,
237
+ })
238
+ .pipe(Effect.mapError((cause) => new AuthFlowStateError({
239
+ message: "Invalid auth flow state",
240
+ cause,
241
+ })));
242
+ return yield* authFlowStateFromChallenge(flowId, verified);
243
+ }),
244
+ });
245
+ };
246
+ export const AuthFlowStateLive = (options) => Layer.effect(AuthFlowState)(Effect.gen(function* () {
247
+ const challenge = yield* Challenge;
248
+ return makeAuthFlowState({
249
+ ...options,
250
+ challenge,
251
+ });
252
+ }));
66
253
  export class AuthFlow extends Context.Service()("auth/AuthFlow") {
67
254
  }
68
255
  (function (AuthFlow) {
69
256
  AuthFlow.make = (service) => AuthFlow.of(service);
70
257
  })(AuthFlow || (AuthFlow = {}));
258
+ export class LoginApprovalFinalize extends Context.Service()("auth/LoginApprovalFinalize") {
259
+ }
260
+ (function (LoginApprovalFinalize) {
261
+ LoginApprovalFinalize.make = (service) => LoginApprovalFinalize.of(service);
262
+ })(LoginApprovalFinalize || (LoginApprovalFinalize = {}));
263
+ const loginApprovalFinalizeDenied = (reason) => AuthResult.PolicyDenied({ reason });
264
+ export const makeLoginApprovalFinalize = (dependencies) => LoginApprovalFinalize.make({
265
+ complete: Effect.fn("auth.login_approval_finalize.complete")(function* (input) {
266
+ const recordOption = yield* dependencies.loginApprovalReview.status({
267
+ approvalChallengeId: input.approvalChallengeId,
268
+ });
269
+ if (Option.isNone(recordOption)) {
270
+ return loginApprovalFinalizeDenied("Login approval review was not found");
271
+ }
272
+ const record = recordOption.value;
273
+ if (record.flowId !== input.flowId) {
274
+ return loginApprovalFinalizeDenied("Invalid login approval review");
275
+ }
276
+ const now = yield* currentUnixMillis;
277
+ if (Number(record.expiresAt) <= Number(now)) {
278
+ return loginApprovalFinalizeDenied("Login approval expired");
279
+ }
280
+ if (record.status === "pending") {
281
+ return loginApprovalFinalizeDenied("Login approval is pending");
282
+ }
283
+ if (record.status === "denied") {
284
+ return loginApprovalFinalizeDenied(record.deniedReason ?? "Login approval denied");
285
+ }
286
+ const consumed = yield* dependencies.authFlowState.consume(input.flowId, input.sameDeviceSecret);
287
+ if (consumed.userId !== record.userId) {
288
+ return loginApprovalFinalizeDenied("Invalid login approval review");
289
+ }
290
+ return yield* dependencies.authFlow.complete(consumed);
291
+ }),
292
+ });
293
+ export const LoginApprovalFinalizeLive = Layer.effect(LoginApprovalFinalize)(Effect.gen(function* () {
294
+ const loginApprovalReview = yield* LoginApprovalReview;
295
+ const authFlowState = yield* AuthFlowState;
296
+ const authFlow = yield* AuthFlow;
297
+ return makeLoginApprovalFinalize({
298
+ loginApprovalReview,
299
+ authFlowState,
300
+ authFlow,
301
+ });
302
+ }));
303
+ const loginApprovalFlowIdMetadata = (flowId, metadata) => ({
304
+ ...metadata,
305
+ flowId,
306
+ });
307
+ const sessionMetadata = (metadata, request) => request === undefined
308
+ ? metadata
309
+ : {
310
+ ...metadata,
311
+ [sessionRequestMetadataKey]: request,
312
+ };
313
+ const mergeMetadata = (left, right) => {
314
+ if (left === undefined) {
315
+ return right;
316
+ }
317
+ if (right === undefined) {
318
+ return left;
319
+ }
320
+ return { ...left, ...right };
321
+ };
322
+ const aalRank = {
323
+ aal1: 1,
324
+ aal2: 2,
325
+ aal3: 3,
326
+ };
327
+ const strongerAal = (left, right) => (aalRank[left] >= aalRank[right] ? left : right);
328
+ const authMethodReference = (method) => {
329
+ switch (method) {
330
+ case "password": {
331
+ return "pwd";
332
+ }
333
+ case "email-otp": {
334
+ return "otp";
335
+ }
336
+ case "magic-link": {
337
+ return "magic_link";
338
+ }
339
+ case "backup-code": {
340
+ return "backup_code";
341
+ }
342
+ default: {
343
+ return method;
344
+ }
345
+ }
346
+ };
347
+ const appendAuthMethods = (current, methods) => {
348
+ const next = [...current];
349
+ for (const method of methods) {
350
+ if (!next.includes(method)) {
351
+ next.push(method);
352
+ }
353
+ }
354
+ return next;
355
+ };
356
+ const loginNotificationSession = (session) => ({
357
+ sessionId: session.sessionId,
358
+ userId: session.userId,
359
+ authTime: session.authTime,
360
+ expiresAt: session.expiresAt,
361
+ aal: session.aal,
362
+ amr: session.amr,
363
+ ...(session.claims === undefined ? {} : { claims: session.claims }),
364
+ });
365
+ const loginRequestContextFromMetadata = (metadata) => {
366
+ const value = metadata?.[sessionRequestMetadataKey];
367
+ return typeof value === "object" && value !== null && !Array.isArray(value)
368
+ ? value
369
+ : undefined;
370
+ };
371
+ const loginApprovalNotConfigured = () => AuthResult.PolicyDenied({
372
+ reason: "Login approval is not configured",
373
+ });
374
+ const loginApprovalEmailMissing = () => AuthResult.PolicyDenied({
375
+ reason: "Login approval requires an email address",
376
+ });
377
+ const loginApprovalSameDeviceNotConfigured = () => AuthResult.PolicyDenied({
378
+ reason: "Login approval same-device binding is not configured",
379
+ });
380
+ const loginApprovalReviewNotConfigured = () => AuthResult.PolicyDenied({
381
+ reason: "Login approval review is not configured",
382
+ });
383
+ const mfaFlowStateNotConfigured = () => AuthResult.PolicyDenied({
384
+ reason: "MFA flow state is not configured",
385
+ });
386
+ const mfaFlowUserMismatch = () => AuthResult.PolicyDenied({
387
+ reason: "MFA flow user mismatch",
388
+ });
389
+ const optionalLoginApprovalDependencies = Effect.gen(function* () {
390
+ const loginApprovalPolicy = yield* Effect.serviceOption(LoginApprovalPolicy);
391
+ const loginApprovalFlow = yield* Effect.serviceOption(LoginApprovalFlow);
392
+ const authFlowState = yield* Effect.serviceOption(AuthFlowState);
393
+ const crypto = yield* Effect.serviceOption(Crypto);
394
+ const trustedDevice = yield* Effect.serviceOption(TrustedDevice);
395
+ const loginRiskEngine = yield* Effect.serviceOption(LoginRiskEngine);
396
+ const loginApprovalReview = yield* Effect.serviceOption(LoginApprovalReview);
397
+ const loginNotification = yield* Effect.serviceOption(LoginNotification);
398
+ return {
399
+ ...(Option.isSome(loginApprovalPolicy)
400
+ ? { loginApprovalPolicy: loginApprovalPolicy.value }
401
+ : {}),
402
+ ...(Option.isSome(loginApprovalFlow)
403
+ ? { loginApprovalFlow: loginApprovalFlow.value }
404
+ : {}),
405
+ ...(Option.isSome(authFlowState)
406
+ ? { authFlowState: authFlowState.value }
407
+ : {}),
408
+ ...(Option.isSome(crypto) ? { crypto: crypto.value } : {}),
409
+ ...(Option.isSome(trustedDevice)
410
+ ? { trustedDevice: trustedDevice.value }
411
+ : {}),
412
+ ...(Option.isSome(loginRiskEngine)
413
+ ? { loginRiskEngine: loginRiskEngine.value }
414
+ : {}),
415
+ ...(Option.isSome(loginApprovalReview)
416
+ ? { loginApprovalReview: loginApprovalReview.value }
417
+ : {}),
418
+ ...(Option.isSome(loginNotification)
419
+ ? { loginNotification: loginNotification.value }
420
+ : {}),
421
+ };
422
+ });
71
423
  export const makeAuthFlow = (dependencies) => {
72
424
  const emailVerificationSessionPolicy = dependencies.emailVerificationSessionPolicy ??
73
425
  makeEmailVerificationSessionPolicy();
426
+ const loginApprovalPolicy = dependencies.loginApprovalPolicy ?? makeLoginApprovalPolicy();
427
+ const loginRiskEngine = dependencies.loginRiskEngine ?? makeLoginRiskEngineNoop();
428
+ const loginApprovalSameDeviceSecretBytes = dependencies.loginApprovalSameDeviceSecretBytes ??
429
+ defaultLoginApprovalSameDeviceSecretBytes;
430
+ const resolveDevice = (input) => {
431
+ if (input.device !== undefined) {
432
+ return Effect.succeed(input.device);
433
+ }
434
+ if (dependencies.trustedDevice === undefined) {
435
+ return Effect.succeed(unknownLoginDeviceContext);
436
+ }
437
+ return dependencies.trustedDevice.status({
438
+ userId: input.userId,
439
+ token: input.trustedDeviceToken,
440
+ });
441
+ };
442
+ const notifyLogin = (input, session) => dependencies.loginNotification === undefined
443
+ ? Effect.void
444
+ : dependencies.loginNotification
445
+ .notify({
446
+ userId: input.userId,
447
+ ...(input.email === undefined ? {} : { email: input.email }),
448
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
449
+ method: input.method,
450
+ amr: input.amr,
451
+ aal: input.aal,
452
+ session: loginNotificationSession(session),
453
+ device: input.device ?? unknownLoginDeviceContext,
454
+ risk: input.risk ?? defaultLoginRiskContext,
455
+ request: input.request ?? loginRequestContextFromMetadata(input.metadata),
456
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
457
+ })
458
+ .pipe(Effect.catchCause(() => Effect.void));
74
459
  const complete = Effect.fn("auth.auth_flow.complete")(function* (input) {
75
460
  yield* Effect.annotateCurrentSpan({
76
461
  "auth.method": input.method,
@@ -83,55 +468,198 @@ export const makeAuthFlow = (dependencies) => {
83
468
  amr: input.amr,
84
469
  aal: input.aal,
85
470
  claims: input.claims,
471
+ ...(input.mfaVerifiedAt === undefined
472
+ ? {}
473
+ : { mfaVerifiedAt: input.mfaVerifiedAt }),
474
+ metadata: input.metadata,
86
475
  });
476
+ yield* notifyLogin(input, session);
87
477
  yield* Effect.annotateCurrentSpan("auth.result", "Authenticated");
88
478
  return AuthResult.Authenticated({ session });
89
479
  });
90
480
  const completePrimaryFactor = Effect.fn("auth.auth_flow.complete_primary_factor")(function* (input) {
91
- const decision = yield* emailVerificationSessionPolicy.decide({
481
+ const emailVerificationDecision = yield* emailVerificationSessionPolicy.decide({
92
482
  claims: input.claims,
93
483
  });
94
- if (decision._tag === "RequiresEmailVerification") {
95
- yield* Effect.annotateCurrentSpan("auth.result", "RequiresEmailVerification");
96
- return AuthResult.RequiresEmailVerification({
97
- flowId: AuthFlowId("email-verification"),
98
- ...(input.email === undefined
99
- ? {}
100
- : { userId: input.userId, email: input.email }),
484
+ const metadata = sessionMetadata(input.metadata, input.request);
485
+ const device = yield* resolveDevice(input);
486
+ const risk = yield* loginRiskEngine.assess({
487
+ userId: input.userId,
488
+ ...(input.email === undefined ? {} : { email: input.email }),
489
+ emailVerified: emailVerificationDecision.claims?.emailVerified,
490
+ method: input.method,
491
+ amr: input.amr,
492
+ aal: input.aal,
493
+ device,
494
+ ...(input.request === undefined ? {} : { request: input.request }),
495
+ ...(metadata === undefined ? {} : { metadata }),
496
+ });
497
+ const loginApprovalDecision = yield* loginApprovalPolicy.decide({
498
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
499
+ userId: input.userId,
500
+ ...(input.email === undefined ? {} : { email: input.email }),
501
+ emailVerified: emailVerificationDecision.claims?.emailVerified,
502
+ method: input.method,
503
+ amr: input.amr,
504
+ aal: input.aal,
505
+ device,
506
+ risk,
507
+ ...(metadata === undefined ? {} : { metadata }),
508
+ });
509
+ if (loginApprovalDecision._tag === "RequiresApproval") {
510
+ if (loginApprovalDecision.channel === "email" &&
511
+ input.email === undefined) {
512
+ yield* Effect.annotateCurrentSpan("auth.result", "PolicyDenied");
513
+ return loginApprovalEmailMissing();
514
+ }
515
+ if (dependencies.authFlowState === undefined ||
516
+ dependencies.loginApprovalFlow === undefined) {
517
+ yield* Effect.annotateCurrentSpan("auth.result", "PolicyDenied");
518
+ return loginApprovalNotConfigured();
519
+ }
520
+ const { loginApprovalReview } = dependencies;
521
+ if (loginApprovalDecision.sessionBinding === "none" &&
522
+ loginApprovalReview === undefined) {
523
+ yield* Effect.annotateCurrentSpan("auth.result", "PolicyDenied");
524
+ return loginApprovalReviewNotConfigured();
525
+ }
526
+ let sameDeviceSecret;
527
+ if (loginApprovalDecision.sameDeviceRequired) {
528
+ if (dependencies.crypto === undefined) {
529
+ yield* Effect.annotateCurrentSpan("auth.result", "PolicyDenied");
530
+ return loginApprovalSameDeviceNotConfigured();
531
+ }
532
+ sameDeviceSecret = Redacted.make(yield* dependencies.crypto
533
+ .randomToken(loginApprovalSameDeviceSecretBytes)
534
+ .pipe(Effect.mapError((cause) => new AuthFlowStateError({
535
+ message: "Failed to create login approval same-device binding",
536
+ cause,
537
+ }))));
538
+ }
539
+ const claims = loginApprovalDecision.markEmailVerified === true
540
+ ? withEmailVerified(emailVerificationDecision.claims, true)
541
+ : emailVerificationDecision.claims;
542
+ const flowState = yield* dependencies.authFlowState.start({
543
+ userId: input.userId,
544
+ method: input.method,
545
+ amr: input.amr,
546
+ aal: input.aal,
547
+ ...(input.email === undefined ? {} : { email: input.email }),
548
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
549
+ device,
550
+ claims,
551
+ secret: sameDeviceSecret,
552
+ ...(metadata === undefined ? {} : { metadata }),
553
+ });
554
+ const approval = yield* dependencies.loginApprovalFlow.start({
555
+ userId: input.userId,
556
+ ...(input.email === undefined ? {} : { email: input.email }),
557
+ channel: loginApprovalDecision.channel,
558
+ reason: loginApprovalDecision.reason,
559
+ sessionBinding: loginApprovalDecision.sessionBinding,
560
+ sameDeviceRequired: loginApprovalDecision.sameDeviceRequired,
561
+ ...(loginApprovalDecision.markEmailVerified === true
562
+ ? { markEmailVerified: true }
563
+ : {}),
564
+ risk,
565
+ metadata: loginApprovalFlowIdMetadata(flowState.flowId, metadata),
566
+ });
567
+ if (approval.sessionBinding === "none" &&
568
+ loginApprovalReview !== undefined) {
569
+ yield* loginApprovalReview.start({
570
+ approval,
571
+ flowId: flowState.flowId,
572
+ metadata,
573
+ });
574
+ }
575
+ yield* Effect.annotateCurrentSpan({
576
+ "auth.result": "RequiresLoginApproval",
577
+ "auth.login_approval.reason": approval.reason,
578
+ "auth.login_approval.session_binding": approval.sessionBinding,
579
+ "auth.login_approval.same_device_required": approval.sameDeviceRequired,
580
+ });
581
+ return AuthResult.RequiresLoginApproval({
582
+ flowId: flowState.flowId,
583
+ approvalChallengeId: approval.challengeId,
584
+ channel: approval.channel,
585
+ sessionBinding: approval.sessionBinding,
586
+ sameDeviceRequired: approval.sameDeviceRequired,
587
+ reason: approval.reason,
588
+ expiresAt: flowState.expiresAt,
589
+ ...(sameDeviceSecret === undefined ? {} : { sameDeviceSecret }),
101
590
  });
102
591
  }
103
592
  return yield* complete({
104
593
  userId: input.userId,
594
+ ...(input.email === undefined ? {} : { email: input.email }),
595
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
105
596
  method: input.method,
106
597
  amr: input.amr,
107
598
  aal: input.aal,
108
- claims: decision.claims,
599
+ device,
600
+ ...(input.request === undefined ? {} : { request: input.request }),
601
+ risk,
602
+ claims: emailVerificationDecision.claims,
603
+ metadata,
604
+ });
605
+ });
606
+ const completeMfa = Effect.fn("auth.auth_flow.complete_mfa")(function* (input) {
607
+ if (dependencies.authFlowState === undefined) {
608
+ yield* Effect.annotateCurrentSpan("auth.result", "PolicyDenied");
609
+ return mfaFlowStateNotConfigured();
610
+ }
611
+ const consumed = yield* dependencies.authFlowState.consume(input.flowId, input.secret);
612
+ if (input.userId !== undefined && consumed.userId !== input.userId) {
613
+ yield* Effect.annotateCurrentSpan("auth.result", "PolicyDenied");
614
+ return mfaFlowUserMismatch();
615
+ }
616
+ const mfaVerifiedAt = input.now ?? (yield* currentUnixMillis);
617
+ const amr = appendAuthMethods(consumed.amr, input.amr ?? [authMethodReference(input.method)]);
618
+ const aal = strongerAal(consumed.aal, input.aal ?? "aal2");
619
+ const metadata = mergeMetadata(consumed.metadata, input.metadata);
620
+ yield* Effect.annotateCurrentSpan({
621
+ "auth.result": "Authenticated",
622
+ "auth.mfa.method": input.method,
623
+ "auth.mfa.aal": aal,
624
+ "auth.mfa.amr_count": amr.length,
625
+ });
626
+ return yield* complete({
627
+ userId: consumed.userId,
628
+ ...(consumed.email === undefined ? {} : { email: consumed.email }),
629
+ ...(consumed.intent === undefined ? {} : { intent: consumed.intent }),
630
+ method: consumed.method,
631
+ amr,
632
+ aal,
633
+ ...(consumed.device === undefined ? {} : { device: consumed.device }),
634
+ claims: consumed.claims,
635
+ mfaVerifiedAt,
636
+ metadata,
109
637
  });
110
638
  });
111
639
  return AuthFlow.make({
112
640
  startPrimaryFactor: completePrimaryFactor,
113
641
  completePrimaryFactor,
114
- completeMfa: Effect.fn("auth.auth_flow.complete_mfa")(function* () {
115
- return yield* new AuthFlowStateError({
116
- message: "MFA flow state is not implemented yet",
117
- });
118
- }),
642
+ completeMfa,
119
643
  complete,
120
644
  });
121
645
  };
122
646
  export const AuthFlowLive = Layer.effect(AuthFlow)(Effect.gen(function* () {
123
647
  const sessions = yield* Sessions;
124
648
  const emailVerificationSessionPolicyOptions = yield* AuthEmailVerificationSessionPolicyConfig;
649
+ const loginApprovalDependencies = yield* optionalLoginApprovalDependencies;
125
650
  return makeAuthFlow({
126
651
  sessions,
127
652
  emailVerificationSessionPolicy: makeEmailVerificationSessionPolicy(emailVerificationSessionPolicyOptions),
653
+ ...loginApprovalDependencies,
128
654
  });
129
655
  }));
130
656
  export const AuthFlowLiveWithEmailVerificationPolicy = (options) => Layer.effect(AuthFlow)(Effect.gen(function* () {
131
657
  const sessions = yield* Sessions;
658
+ const loginApprovalDependencies = yield* optionalLoginApprovalDependencies;
132
659
  return makeAuthFlow({
133
660
  sessions,
134
661
  emailVerificationSessionPolicy: makeEmailVerificationSessionPolicy(options),
662
+ ...loginApprovalDependencies,
135
663
  });
136
664
  }));
137
665
  //# sourceMappingURL=AuthFlow.js.map