@effect-auth/core 0.1.0-alpha.1 → 0.1.0-alpha.10

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 (227) hide show
  1. package/README.md +728 -72
  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 +275 -1
  7. package/dist/AuditLog.d.ts.map +1 -1
  8. package/dist/AuditLog.js +213 -1
  9. package/dist/AuditLog.js.map +1 -1
  10. package/dist/AuthConfig.d.ts +42 -0
  11. package/dist/AuthConfig.d.ts.map +1 -0
  12. package/dist/AuthConfig.js +43 -0
  13. package/dist/AuthConfig.js.map +1 -0
  14. package/dist/AuthFlow.d.ts +462 -36
  15. package/dist/AuthFlow.d.ts.map +1 -1
  16. package/dist/AuthFlow.js +657 -5
  17. package/dist/AuthFlow.js.map +1 -1
  18. package/dist/AuthKernel.d.ts +3 -0
  19. package/dist/AuthKernel.d.ts.map +1 -0
  20. package/dist/AuthKernel.js +9 -0
  21. package/dist/AuthKernel.js.map +1 -0
  22. package/dist/Challenge.d.ts +24 -4
  23. package/dist/Challenge.d.ts.map +1 -1
  24. package/dist/Challenge.js +167 -1
  25. package/dist/Challenge.js.map +1 -1
  26. package/dist/Client.d.ts +378 -0
  27. package/dist/Client.d.ts.map +1 -0
  28. package/dist/Client.js +768 -0
  29. package/dist/Client.js.map +1 -0
  30. package/dist/Crypto.d.ts +6 -1
  31. package/dist/Crypto.d.ts.map +1 -1
  32. package/dist/Crypto.js +62 -1
  33. package/dist/Crypto.js.map +1 -1
  34. package/dist/EffectQbSqliteStorage.d.ts +600 -0
  35. package/dist/EffectQbSqliteStorage.d.ts.map +1 -0
  36. package/dist/EffectQbSqliteStorage.js +1637 -0
  37. package/dist/EffectQbSqliteStorage.js.map +1 -0
  38. package/dist/EmailOtp.d.ts +120 -0
  39. package/dist/EmailOtp.d.ts.map +1 -0
  40. package/dist/EmailOtp.js +286 -0
  41. package/dist/EmailOtp.js.map +1 -0
  42. package/dist/EmailVerification.d.ts +121 -0
  43. package/dist/EmailVerification.d.ts.map +1 -0
  44. package/dist/EmailVerification.js +200 -0
  45. package/dist/EmailVerification.js.map +1 -0
  46. package/dist/HttpApi/Api.d.ts +1956 -0
  47. package/dist/HttpApi/Api.d.ts.map +1 -0
  48. package/dist/HttpApi/Api.js +2325 -0
  49. package/dist/HttpApi/Api.js.map +1 -0
  50. package/dist/HttpApi/Endpoints.d.ts +945 -0
  51. package/dist/HttpApi/Endpoints.d.ts.map +1 -0
  52. package/dist/HttpApi/Endpoints.js +373 -0
  53. package/dist/HttpApi/Endpoints.js.map +1 -0
  54. package/dist/HttpApi/Errors.d.ts +82 -0
  55. package/dist/HttpApi/Errors.d.ts.map +1 -0
  56. package/dist/HttpApi/Errors.js +184 -0
  57. package/dist/HttpApi/Errors.js.map +1 -0
  58. package/dist/HttpApi/Schemas.d.ts +1113 -0
  59. package/dist/HttpApi/Schemas.d.ts.map +1 -0
  60. package/dist/HttpApi/Schemas.js +734 -0
  61. package/dist/HttpApi/Schemas.js.map +1 -0
  62. package/dist/HttpApi/Security.d.ts +159 -0
  63. package/dist/HttpApi/Security.d.ts.map +1 -0
  64. package/dist/HttpApi/Security.js +530 -0
  65. package/dist/HttpApi/Security.js.map +1 -0
  66. package/dist/HttpApi/Transport.d.ts +65 -0
  67. package/dist/HttpApi/Transport.d.ts.map +1 -0
  68. package/dist/HttpApi/Transport.js +311 -0
  69. package/dist/HttpApi/Transport.js.map +1 -0
  70. package/dist/HttpApi/index.d.ts +8 -0
  71. package/dist/HttpApi/index.d.ts.map +1 -0
  72. package/dist/HttpApi/index.js +7 -0
  73. package/dist/HttpApi/index.js.map +1 -0
  74. package/dist/HttpApi.d.ts +2 -0
  75. package/dist/HttpApi.d.ts.map +1 -0
  76. package/dist/HttpApi.js +2 -0
  77. package/dist/HttpApi.js.map +1 -0
  78. package/dist/Identifiers.d.ts.map +1 -1
  79. package/dist/Identifiers.js.map +1 -1
  80. package/dist/Internal.d.ts +7 -0
  81. package/dist/Internal.d.ts.map +1 -0
  82. package/dist/Internal.js +12 -0
  83. package/dist/Internal.js.map +1 -0
  84. package/dist/Jwt.d.ts +271 -0
  85. package/dist/Jwt.d.ts.map +1 -0
  86. package/dist/Jwt.js +512 -0
  87. package/dist/Jwt.js.map +1 -0
  88. package/dist/LoginApproval.d.ts +314 -0
  89. package/dist/LoginApproval.d.ts.map +1 -0
  90. package/dist/LoginApproval.js +551 -0
  91. package/dist/LoginApproval.js.map +1 -0
  92. package/dist/LoginNotification.d.ts +183 -0
  93. package/dist/LoginNotification.d.ts.map +1 -0
  94. package/dist/LoginNotification.js +329 -0
  95. package/dist/LoginNotification.js.map +1 -0
  96. package/dist/LoginRisk.d.ts +73 -0
  97. package/dist/LoginRisk.d.ts.map +1 -0
  98. package/dist/LoginRisk.js +57 -0
  99. package/dist/LoginRisk.js.map +1 -0
  100. package/dist/MagicLink.d.ts +85 -0
  101. package/dist/MagicLink.d.ts.map +1 -0
  102. package/dist/MagicLink.js +206 -0
  103. package/dist/MagicLink.js.map +1 -0
  104. package/dist/Mailer.d.ts +147 -0
  105. package/dist/Mailer.d.ts.map +1 -0
  106. package/dist/Mailer.js +208 -0
  107. package/dist/Mailer.js.map +1 -0
  108. package/dist/OAuth.d.ts +934 -0
  109. package/dist/OAuth.d.ts.map +1 -0
  110. package/dist/OAuth.js +1661 -0
  111. package/dist/OAuth.js.map +1 -0
  112. package/dist/Passkey.d.ts +334 -0
  113. package/dist/Passkey.d.ts.map +1 -0
  114. package/dist/Passkey.js +422 -0
  115. package/dist/Passkey.js.map +1 -0
  116. package/dist/PasskeySimpleWebAuthn.d.ts +11 -0
  117. package/dist/PasskeySimpleWebAuthn.d.ts.map +1 -0
  118. package/dist/PasskeySimpleWebAuthn.js +181 -0
  119. package/dist/PasskeySimpleWebAuthn.js.map +1 -0
  120. package/dist/Password.d.ts +237 -0
  121. package/dist/Password.d.ts.map +1 -0
  122. package/dist/Password.js +641 -0
  123. package/dist/Password.js.map +1 -0
  124. package/dist/Policy.d.ts +7 -4
  125. package/dist/Policy.d.ts.map +1 -1
  126. package/dist/Policy.js +28 -5
  127. package/dist/Policy.js.map +1 -1
  128. package/dist/Privacy.d.ts +15 -0
  129. package/dist/Privacy.d.ts.map +1 -1
  130. package/dist/Privacy.js +58 -1
  131. package/dist/Privacy.js.map +1 -1
  132. package/dist/RateLimiter.d.ts +104 -0
  133. package/dist/RateLimiter.d.ts.map +1 -0
  134. package/dist/RateLimiter.js +118 -0
  135. package/dist/RateLimiter.js.map +1 -0
  136. package/dist/RecoveryCode.d.ts +169 -0
  137. package/dist/RecoveryCode.d.ts.map +1 -0
  138. package/dist/RecoveryCode.js +299 -0
  139. package/dist/RecoveryCode.js.map +1 -0
  140. package/dist/RefreshToken.d.ts +222 -0
  141. package/dist/RefreshToken.d.ts.map +1 -0
  142. package/dist/RefreshToken.js +342 -0
  143. package/dist/RefreshToken.js.map +1 -0
  144. package/dist/SecurityTimeline.d.ts +72 -0
  145. package/dist/SecurityTimeline.d.ts.map +1 -0
  146. package/dist/SecurityTimeline.js +386 -0
  147. package/dist/SecurityTimeline.js.map +1 -0
  148. package/dist/Sessions.d.ts +123 -11
  149. package/dist/Sessions.d.ts.map +1 -1
  150. package/dist/Sessions.js +534 -1
  151. package/dist/Sessions.js.map +1 -1
  152. package/dist/StepUp.d.ts +44 -0
  153. package/dist/StepUp.d.ts.map +1 -0
  154. package/dist/StepUp.js +82 -0
  155. package/dist/StepUp.js.map +1 -0
  156. package/dist/Storage.d.ts +73 -5
  157. package/dist/Storage.d.ts.map +1 -1
  158. package/dist/Storage.js +11 -1
  159. package/dist/Storage.js.map +1 -1
  160. package/dist/StorageMigrations.d.ts +13 -1
  161. package/dist/StorageMigrations.d.ts.map +1 -1
  162. package/dist/StorageMigrations.js +256 -2
  163. package/dist/StorageMigrations.js.map +1 -1
  164. package/dist/Testing.d.ts +9 -1
  165. package/dist/Testing.d.ts.map +1 -1
  166. package/dist/Testing.js +30 -2
  167. package/dist/Testing.js.map +1 -1
  168. package/dist/Totp.d.ts +220 -0
  169. package/dist/Totp.d.ts.map +1 -0
  170. package/dist/Totp.js +477 -0
  171. package/dist/Totp.js.map +1 -0
  172. package/dist/TrustedDevice.d.ts +127 -0
  173. package/dist/TrustedDevice.d.ts.map +1 -0
  174. package/dist/TrustedDevice.js +252 -0
  175. package/dist/TrustedDevice.js.map +1 -0
  176. package/dist/WaitUntil.d.ts.map +1 -1
  177. package/dist/WaitUntil.js +1 -1
  178. package/dist/WaitUntil.js.map +1 -1
  179. package/dist/Webhook.d.ts +72 -0
  180. package/dist/Webhook.d.ts.map +1 -0
  181. package/dist/Webhook.js +82 -0
  182. package/dist/Webhook.js.map +1 -0
  183. package/dist/alchemy/cloudflare/Email.d.ts +25 -0
  184. package/dist/alchemy/cloudflare/Email.d.ts.map +1 -0
  185. package/dist/alchemy/cloudflare/Email.js +117 -0
  186. package/dist/alchemy/cloudflare/Email.js.map +1 -0
  187. package/dist/alchemy/cloudflare/RateLimitDurableObject.d.ts +34 -0
  188. package/dist/alchemy/cloudflare/RateLimitDurableObject.d.ts.map +1 -0
  189. package/dist/alchemy/cloudflare/RateLimitDurableObject.js +30 -0
  190. package/dist/alchemy/cloudflare/RateLimitDurableObject.js.map +1 -0
  191. package/dist/cloudflare/Email.d.ts +25 -0
  192. package/dist/cloudflare/Email.d.ts.map +1 -0
  193. package/dist/cloudflare/Email.js +153 -0
  194. package/dist/cloudflare/Email.js.map +1 -0
  195. package/dist/cloudflare/RateLimitDurableObject.d.ts +50 -0
  196. package/dist/cloudflare/RateLimitDurableObject.d.ts.map +1 -0
  197. package/dist/cloudflare/RateLimitDurableObject.js +51 -0
  198. package/dist/cloudflare/RateLimitDurableObject.js.map +1 -0
  199. package/dist/index.d.ts +21 -0
  200. package/dist/index.d.ts.map +1 -1
  201. package/dist/index.js +21 -0
  202. package/dist/index.js.map +1 -1
  203. package/dist/internal/RateLimitDurableObject.d.ts +32 -0
  204. package/dist/internal/RateLimitDurableObject.d.ts.map +1 -0
  205. package/dist/internal/RateLimitDurableObject.js +80 -0
  206. package/dist/internal/RateLimitDurableObject.js.map +1 -0
  207. package/migrations/0002_auth_password.sql +25 -0
  208. package/migrations/0003_auth_login_approval_review.sql +22 -0
  209. package/migrations/0004_auth_trusted_device.sql +12 -0
  210. package/migrations/0005_auth_passkey.sql +17 -0
  211. package/migrations/0006_auth_totp_factor.sql +17 -0
  212. package/migrations/0007_auth_recovery_code.sql +13 -0
  213. package/migrations/0008_auth_api_key.sql +17 -0
  214. package/migrations/0009_auth_refresh_token.sql +21 -0
  215. package/migrations/0010_auth_jwt_revocation.sql +8 -0
  216. package/migrations/0011_auth_oauth_account.sql +16 -0
  217. package/migrations/0012_auth_oauth_provider_token_vault.sql +20 -0
  218. package/migrations/0013_auth_security_timeline.sql +16 -0
  219. package/package.json +146 -26
  220. package/dist/D1Kysely.d.ts +0 -40
  221. package/dist/D1Kysely.d.ts.map +0 -1
  222. package/dist/D1Kysely.js +0 -75
  223. package/dist/D1Kysely.js.map +0 -1
  224. package/dist/KyselyStorage.d.ts +0 -41
  225. package/dist/KyselyStorage.d.ts.map +0 -1
  226. package/dist/KyselyStorage.js +0 -176
  227. package/dist/KyselyStorage.js.map +0 -1
package/dist/AuthFlow.js CHANGED
@@ -1,13 +1,665 @@
1
- import { Context, Data, Effect } from "effect";
2
- export class AuthFlowError extends Data.TaggedError("AuthFlowError") {
1
+ // oxlint-disable complexity
2
+ import { Context, Data, Duration, Effect, Layer, Option, Redacted, } from "effect";
3
+ import { AuthEmailVerificationSessionPolicyConfig } from "./AuthConfig.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;
14
+ export class EmailVerificationSessionPolicy extends Context.Service()("auth/EmailVerificationSessionPolicy") {
3
15
  }
16
+ (function (EmailVerificationSessionPolicy) {
17
+ EmailVerificationSessionPolicy.make = (service) => EmailVerificationSessionPolicy.of(service);
18
+ })(EmailVerificationSessionPolicy || (EmailVerificationSessionPolicy = {}));
19
+ const addEmailVerificationRequirement = (requirements) => requirements?.includes("email_verification") === true
20
+ ? requirements
21
+ : [...(requirements ?? []), "email_verification"];
22
+ const removeEmailVerificationRequirement = (requirements) => {
23
+ const filtered = requirements?.filter((requirement) => requirement !== "email_verification");
24
+ return filtered?.length === 0 ? undefined : filtered;
25
+ };
26
+ const withEmailVerified = (claims, emailVerified) => {
27
+ const { requirements: _requirements, ...rest } = claims ?? {};
28
+ const requirements = emailVerified
29
+ ? removeEmailVerificationRequirement(claims?.requirements)
30
+ : claims?.requirements;
31
+ return {
32
+ ...rest,
33
+ emailVerified,
34
+ ...(requirements === undefined ? {} : { requirements }),
35
+ };
36
+ };
37
+ const withEmailVerificationRequirement = (claims) => ({
38
+ ...claims,
39
+ emailVerified: false,
40
+ requirements: addEmailVerificationRequirement(claims?.requirements),
41
+ });
42
+ export const makeEmailVerificationSessionPolicy = (options = {}) => {
43
+ const mode = options.mode ?? "allow-unverified";
44
+ return EmailVerificationSessionPolicy.make({
45
+ decide: Effect.fn("auth.email_verification_session_policy.decide")(function* (input) {
46
+ const emailVerified = input.claims?.emailVerified;
47
+ if (emailVerified !== false) {
48
+ return {
49
+ _tag: "CreateSession",
50
+ claims: emailVerified === true
51
+ ? withEmailVerified(input.claims, true)
52
+ : input.claims,
53
+ };
54
+ }
55
+ switch (mode) {
56
+ case "allow-unverified": {
57
+ return {
58
+ _tag: "CreateSession",
59
+ claims: withEmailVerified(input.claims, false),
60
+ };
61
+ }
62
+ case "limited-session": {
63
+ return {
64
+ _tag: "CreateSession",
65
+ claims: withEmailVerificationRequirement(input.claims),
66
+ };
67
+ }
68
+ }
69
+ }),
70
+ });
71
+ };
72
+ export const EmailVerificationSessionPolicyLive = (options) => Layer.succeed(EmailVerificationSessionPolicy)(makeEmailVerificationSessionPolicy(options));
73
+ export const AuthResult = Data.taggedEnum();
74
+ export class AuthFlowStateError extends Data.TaggedError("AuthFlowStateError") {
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
+ }));
4
253
  export class AuthFlow extends Context.Service()("auth/AuthFlow") {
5
254
  }
6
255
  (function (AuthFlow) {
7
256
  AuthFlow.make = (service) => AuthFlow.of(service);
8
257
  })(AuthFlow || (AuthFlow = {}));
9
- export const denied = (reason) => ({
10
- type: "denied",
11
- reason
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
+ };
12
422
  });
423
+ export const makeAuthFlow = (dependencies) => {
424
+ const emailVerificationSessionPolicy = dependencies.emailVerificationSessionPolicy ??
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));
459
+ const complete = Effect.fn("auth.auth_flow.complete")(function* (input) {
460
+ yield* Effect.annotateCurrentSpan({
461
+ "auth.method": input.method,
462
+ "auth.aal": input.aal,
463
+ "auth.amr_count": input.amr.length,
464
+ });
465
+ const session = yield* dependencies.sessions.create({
466
+ userId: input.userId,
467
+ method: input.method,
468
+ amr: input.amr,
469
+ aal: input.aal,
470
+ claims: input.claims,
471
+ ...(input.mfaVerifiedAt === undefined
472
+ ? {}
473
+ : { mfaVerifiedAt: input.mfaVerifiedAt }),
474
+ metadata: input.metadata,
475
+ });
476
+ yield* notifyLogin(input, session);
477
+ yield* Effect.annotateCurrentSpan("auth.result", "Authenticated");
478
+ return AuthResult.Authenticated({ session });
479
+ });
480
+ const completePrimaryFactor = Effect.fn("auth.auth_flow.complete_primary_factor")(function* (input) {
481
+ const emailVerificationDecision = yield* emailVerificationSessionPolicy.decide({
482
+ claims: input.claims,
483
+ });
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 }),
590
+ });
591
+ }
592
+ return yield* complete({
593
+ userId: input.userId,
594
+ ...(input.email === undefined ? {} : { email: input.email }),
595
+ ...(input.intent === undefined ? {} : { intent: input.intent }),
596
+ method: input.method,
597
+ amr: input.amr,
598
+ aal: input.aal,
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,
637
+ });
638
+ });
639
+ return AuthFlow.make({
640
+ startPrimaryFactor: completePrimaryFactor,
641
+ completePrimaryFactor,
642
+ completeMfa,
643
+ complete,
644
+ });
645
+ };
646
+ export const AuthFlowLive = Layer.effect(AuthFlow)(Effect.gen(function* () {
647
+ const sessions = yield* Sessions;
648
+ const emailVerificationSessionPolicyOptions = yield* AuthEmailVerificationSessionPolicyConfig;
649
+ const loginApprovalDependencies = yield* optionalLoginApprovalDependencies;
650
+ return makeAuthFlow({
651
+ sessions,
652
+ emailVerificationSessionPolicy: makeEmailVerificationSessionPolicy(emailVerificationSessionPolicyOptions),
653
+ ...loginApprovalDependencies,
654
+ });
655
+ }));
656
+ export const AuthFlowLiveWithEmailVerificationPolicy = (options) => Layer.effect(AuthFlow)(Effect.gen(function* () {
657
+ const sessions = yield* Sessions;
658
+ const loginApprovalDependencies = yield* optionalLoginApprovalDependencies;
659
+ return makeAuthFlow({
660
+ sessions,
661
+ emailVerificationSessionPolicy: makeEmailVerificationSessionPolicy(options),
662
+ ...loginApprovalDependencies,
663
+ });
664
+ }));
13
665
  //# sourceMappingURL=AuthFlow.js.map