@revealui/auth 0.2.1 → 0.3.2

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 (75) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/react/index.d.ts +4 -0
  3. package/dist/react/index.d.ts.map +1 -1
  4. package/dist/react/index.js +2 -0
  5. package/dist/react/useMFA.d.ts +83 -0
  6. package/dist/react/useMFA.d.ts.map +1 -0
  7. package/dist/react/useMFA.js +182 -0
  8. package/dist/react/usePasskey.d.ts +88 -0
  9. package/dist/react/usePasskey.d.ts.map +1 -0
  10. package/dist/react/usePasskey.js +203 -0
  11. package/dist/react/useSession.d.ts.map +1 -1
  12. package/dist/react/useSession.js +16 -5
  13. package/dist/react/useSignIn.d.ts +9 -3
  14. package/dist/react/useSignIn.d.ts.map +1 -1
  15. package/dist/react/useSignIn.js +32 -10
  16. package/dist/react/useSignOut.d.ts.map +1 -1
  17. package/dist/react/useSignUp.d.ts.map +1 -1
  18. package/dist/react/useSignUp.js +25 -9
  19. package/dist/server/auth.d.ts.map +1 -1
  20. package/dist/server/auth.js +85 -8
  21. package/dist/server/brute-force.d.ts +10 -1
  22. package/dist/server/brute-force.d.ts.map +1 -1
  23. package/dist/server/brute-force.js +17 -3
  24. package/dist/server/errors.d.ts.map +1 -1
  25. package/dist/server/index.d.ts +16 -6
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +11 -5
  28. package/dist/server/magic-link.d.ts +52 -0
  29. package/dist/server/magic-link.d.ts.map +1 -0
  30. package/dist/server/magic-link.js +111 -0
  31. package/dist/server/mfa.d.ts +87 -0
  32. package/dist/server/mfa.d.ts.map +1 -0
  33. package/dist/server/mfa.js +263 -0
  34. package/dist/server/oauth.d.ts +52 -4
  35. package/dist/server/oauth.d.ts.map +1 -1
  36. package/dist/server/oauth.js +165 -18
  37. package/dist/server/passkey.d.ts +132 -0
  38. package/dist/server/passkey.d.ts.map +1 -0
  39. package/dist/server/passkey.js +257 -0
  40. package/dist/server/password-reset.d.ts +15 -0
  41. package/dist/server/password-reset.d.ts.map +1 -1
  42. package/dist/server/password-reset.js +44 -1
  43. package/dist/server/password-validation.d.ts.map +1 -1
  44. package/dist/server/providers/github.d.ts.map +1 -1
  45. package/dist/server/providers/github.js +18 -5
  46. package/dist/server/providers/google.d.ts.map +1 -1
  47. package/dist/server/providers/google.js +18 -3
  48. package/dist/server/providers/vercel.d.ts.map +1 -1
  49. package/dist/server/providers/vercel.js +18 -3
  50. package/dist/server/rate-limit.d.ts +10 -1
  51. package/dist/server/rate-limit.d.ts.map +1 -1
  52. package/dist/server/rate-limit.js +61 -43
  53. package/dist/server/session.d.ts +65 -1
  54. package/dist/server/session.d.ts.map +1 -1
  55. package/dist/server/session.js +175 -7
  56. package/dist/server/signed-cookie.d.ts +32 -0
  57. package/dist/server/signed-cookie.d.ts.map +1 -0
  58. package/dist/server/signed-cookie.js +67 -0
  59. package/dist/server/storage/database.d.ts +1 -1
  60. package/dist/server/storage/database.d.ts.map +1 -1
  61. package/dist/server/storage/database.js +15 -7
  62. package/dist/server/storage/in-memory.d.ts.map +1 -1
  63. package/dist/server/storage/in-memory.js +7 -7
  64. package/dist/server/storage/index.d.ts +11 -3
  65. package/dist/server/storage/index.d.ts.map +1 -1
  66. package/dist/server/storage/index.js +18 -4
  67. package/dist/server/storage/interface.d.ts +1 -1
  68. package/dist/server/storage/interface.d.ts.map +1 -1
  69. package/dist/server/storage/interface.js +1 -1
  70. package/dist/types.d.ts +20 -8
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js +2 -2
  73. package/dist/utils/database.d.ts.map +1 -1
  74. package/dist/utils/database.js +9 -2
  75. package/package.json +31 -13
@@ -6,18 +6,33 @@
6
6
  'use client';
7
7
  import { useCallback, useRef, useState } from 'react';
8
8
  import { z } from 'zod/v4';
9
+ // Zod validates the required fields; .passthrough() preserves the rest.
10
+ // The API returns JSON-serialized User objects (Dates as ISO strings).
11
+ // z.infer output has an index signature incompatible with the concrete User
12
+ // interface, so we extract the validated user via the helper below.
13
+ const SignInUserSchema = z
14
+ .object({
15
+ id: z.string(),
16
+ email: z.string(),
17
+ name: z.string().nullable().optional(),
18
+ })
19
+ .passthrough();
20
+ /**
21
+ * Narrow Zod-validated API response data to User.
22
+ *
23
+ * The cast is safe because: (1) Zod verified the required fields (id, email),
24
+ * (2) .passthrough() preserves all other properties from the API response,
25
+ * and (3) the API serializes a full User row (Dates become ISO strings in JSON).
26
+ */
27
+ function toUser(validated) {
28
+ return validated;
29
+ }
9
30
  // Validation schemas for sign-in response
10
31
  const SignInErrorResponseSchema = z.object({
11
32
  error: z.string().optional(),
12
33
  });
13
34
  const SignInSuccessResponseSchema = z.object({
14
- user: z
15
- .object({
16
- id: z.string(),
17
- email: z.string(),
18
- name: z.string().nullable().optional(),
19
- })
20
- .passthrough(), // Allow all other User properties
35
+ user: SignInUserSchema,
21
36
  });
22
37
  /**
23
38
  * Hook to sign in a user
@@ -68,12 +83,19 @@ export function useSignIn() {
68
83
  error: errorData.error || 'Failed to sign in',
69
84
  };
70
85
  }
86
+ // Check for MFA challenge before parsing as full success
87
+ const jsonObj = json;
88
+ if (jsonObj.requiresMfa === true && typeof jsonObj.mfaUserId === 'string') {
89
+ return {
90
+ success: false,
91
+ requiresMfa: true,
92
+ mfaUserId: jsonObj.mfaUserId,
93
+ };
94
+ }
71
95
  const successData = SignInSuccessResponseSchema.parse(json);
72
96
  return {
73
97
  success: true,
74
- // Type assertion through unknown is safe because Zod validation ensures the shape is correct
75
- // The API returns serialized data, so we cast to expected type
76
- user: successData.user,
98
+ user: toUser(successData.user),
77
99
  };
78
100
  }
79
101
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"useSignOut.d.ts","sourceRoot":"","sources":["../../src/react/useSignOut.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CAkC7C"}
1
+ {"version":3,"file":"useSignOut.d.ts","sourceRoot":"","sources":["../../src/react/useSignOut.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CAkC7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"useSignUp.d.ts","sourceRoot":"","sources":["../../src/react/useSignUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAiBvC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1F,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAoD3C"}
1
+ {"version":3,"file":"useSignUp.d.ts","sourceRoot":"","sources":["../../src/react/useSignUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAmCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3F,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAoD3C"}
@@ -6,18 +6,34 @@
6
6
  'use client';
7
7
  import { useState } from 'react';
8
8
  import { z } from 'zod/v4';
9
+ // Zod validates the required fields; .passthrough() preserves the rest.
10
+ // The API returns JSON-serialized User objects (Dates as ISO strings).
11
+ // z.infer output has an index signature incompatible with the concrete User
12
+ // interface, so we extract the validated user via the helper below.
13
+ const SignUpUserSchema = z
14
+ .object({
15
+ id: z.string(),
16
+ email: z.string(),
17
+ name: z.string().nullable().optional(),
18
+ })
19
+ .passthrough();
20
+ /**
21
+ * Narrow Zod-validated API response data to User.
22
+ *
23
+ * The cast is safe because: (1) Zod verified the required fields (id, email),
24
+ * (2) .passthrough() preserves all other properties from the API response,
25
+ * and (3) the API serializes a full User row (Dates become ISO strings in JSON).
26
+ */
27
+ function toUser(validated) {
28
+ return validated;
29
+ }
9
30
  // Validation schemas for sign-up response
10
31
  const SignUpErrorResponseSchema = z.object({
32
+ message: z.string().optional(),
11
33
  error: z.string().optional(),
12
34
  });
13
35
  const SignUpSuccessResponseSchema = z.object({
14
- user: z
15
- .object({
16
- id: z.string(),
17
- email: z.string(),
18
- name: z.string().nullable().optional(),
19
- })
20
- .passthrough(), // Allow all other User properties
36
+ user: SignUpUserSchema,
21
37
  });
22
38
  /**
23
39
  * Hook to sign up a new user
@@ -59,7 +75,7 @@ export function useSignUp() {
59
75
  const errorData = SignUpErrorResponseSchema.parse(json);
60
76
  return {
61
77
  success: false,
62
- error: errorData.error || 'Failed to sign up',
78
+ error: errorData.message || errorData.error || 'Failed to sign up',
63
79
  };
64
80
  }
65
81
  const successData = SignUpSuccessResponseSchema.parse(json);
@@ -67,7 +83,7 @@ export function useSignUp() {
67
83
  success: true,
68
84
  // Type assertion through unknown is safe because Zod validation ensures the shape is correct
69
85
  // The API returns serialized data, so we cast to expected type
70
- user: successData.user,
86
+ user: toUser(successData.user),
71
87
  };
72
88
  }
73
89
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAA;AAMnE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GACA,OAAO,CAAC,YAAY,CAAC,CAwHvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,IAAI,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CAkJvB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AASpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CAqKvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAsLvB"}
@@ -6,13 +6,15 @@
6
6
  import { createHash, randomBytes } from 'node:crypto';
7
7
  import { logger } from '@revealui/core/observability/logger';
8
8
  import { getClient } from '@revealui/db/client';
9
- import { users } from '@revealui/db/schema';
9
+ import { oauthAccounts, users } from '@revealui/db/schema';
10
10
  import bcrypt from 'bcryptjs';
11
- import { eq } from 'drizzle-orm';
11
+ import { and, eq, isNull } from 'drizzle-orm';
12
12
  import { clearFailedAttempts, isAccountLocked, recordFailedAttempt } from './brute-force.js';
13
13
  import { validatePasswordStrength } from './password-validation.js';
14
14
  import { checkRateLimit } from './rate-limit.js';
15
15
  import { createSession } from './session.js';
16
+ /** Grace period after signup during which unverified users can still sign in (24 hours) */
17
+ const EMAIL_VERIFICATION_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000;
16
18
  /**
17
19
  * Sign in with email and password
18
20
  *
@@ -29,6 +31,7 @@ export async function signIn(email, password, options) {
29
31
  if (!rateLimit.allowed) {
30
32
  return {
31
33
  success: false,
34
+ reason: 'rate_limited',
32
35
  error: 'Too many login attempts. Please try again later.',
33
36
  };
34
37
  }
@@ -40,6 +43,7 @@ export async function signIn(email, password, options) {
40
43
  : 30;
41
44
  return {
42
45
  success: false,
46
+ reason: 'account_locked',
43
47
  error: `Account locked due to too many failed attempts. Please try again in ${lockMinutes} minutes.`,
44
48
  };
45
49
  }
@@ -47,23 +51,35 @@ export async function signIn(email, password, options) {
47
51
  try {
48
52
  db = getClient();
49
53
  }
50
- catch {
51
- logger.error('Error getting database client');
54
+ catch (clientError) {
55
+ logger.error('Error getting database client', clientError instanceof Error ? clientError : undefined, {
56
+ message: clientError instanceof Error ? clientError.message : String(clientError),
57
+ });
52
58
  return {
53
59
  success: false,
60
+ reason: 'database_error',
54
61
  error: 'Database connection failed',
55
62
  };
56
63
  }
57
64
  // Find user by email
58
65
  let user;
59
66
  try {
60
- const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
67
+ const result = await db
68
+ .select()
69
+ .from(users)
70
+ .where(and(eq(users.email, email), isNull(users.deletedAt)))
71
+ .limit(1);
61
72
  user = result[0];
62
73
  }
63
- catch {
64
- logger.error('Error querying user');
74
+ catch (dbError) {
75
+ logger.error('Error querying user', dbError instanceof Error ? dbError : undefined, {
76
+ message: dbError instanceof Error ? dbError.message : String(dbError),
77
+ name: dbError instanceof Error ? dbError.name : 'unknown',
78
+ stack: dbError instanceof Error ? dbError.stack : undefined,
79
+ });
65
80
  return {
66
81
  success: false,
82
+ reason: 'database_error',
67
83
  error: 'Database error',
68
84
  };
69
85
  }
@@ -73,6 +89,7 @@ export async function signIn(email, password, options) {
73
89
  await recordFailedAttempt(email);
74
90
  return {
75
91
  success: false,
92
+ reason: 'invalid_credentials',
76
93
  error: invalidCredentialsMessage,
77
94
  };
78
95
  }
@@ -81,6 +98,7 @@ export async function signIn(email, password, options) {
81
98
  await recordFailedAttempt(email);
82
99
  return {
83
100
  success: false,
101
+ reason: 'invalid_credentials',
84
102
  error: invalidCredentialsMessage,
85
103
  };
86
104
  }
@@ -94,6 +112,7 @@ export async function signIn(email, password, options) {
94
112
  await recordFailedAttempt(email);
95
113
  return {
96
114
  success: false,
115
+ reason: 'invalid_credentials',
97
116
  error: invalidCredentialsMessage,
98
117
  };
99
118
  }
@@ -101,11 +120,31 @@ export async function signIn(email, password, options) {
101
120
  await recordFailedAttempt(email);
102
121
  return {
103
122
  success: false,
123
+ reason: 'invalid_credentials',
104
124
  error: invalidCredentialsMessage,
105
125
  };
106
126
  }
107
127
  // Successful login - clear failed attempts
108
128
  await clearFailedAttempts(email);
129
+ // Check email verification (with grace period for new accounts)
130
+ if (!user.emailVerified) {
131
+ const accountAge = Date.now() - user.createdAt.getTime();
132
+ if (accountAge > EMAIL_VERIFICATION_GRACE_PERIOD_MS) {
133
+ return {
134
+ success: false,
135
+ reason: 'email_not_verified',
136
+ error: 'Please verify your email address before signing in.',
137
+ };
138
+ }
139
+ }
140
+ // Check if MFA is enabled — if so, return early and require TOTP verification
141
+ if (user.mfaEnabled) {
142
+ return {
143
+ success: true,
144
+ requiresMfa: true,
145
+ mfaUserId: user.id,
146
+ };
147
+ }
109
148
  // Create session
110
149
  let token;
111
150
  try {
@@ -119,6 +158,7 @@ export async function signIn(email, password, options) {
119
158
  logger.error('Error creating session');
120
159
  return {
121
160
  success: false,
161
+ reason: 'session_error',
122
162
  error: 'Failed to create session',
123
163
  };
124
164
  }
@@ -132,6 +172,7 @@ export async function signIn(email, password, options) {
132
172
  logger.error('Unexpected error in signIn');
133
173
  return {
134
174
  success: false,
175
+ reason: 'unexpected_error',
135
176
  error: 'Unexpected error',
136
177
  };
137
178
  }
@@ -208,7 +249,9 @@ export async function signUp(email, password, name, options) {
208
249
  error: 'Database connection failed',
209
250
  };
210
251
  }
211
- // Check if user already exists
252
+ // Check if user already exists (by email in users table or OAuth accounts).
253
+ // Both checks prevent account collision: a password signup must not collide
254
+ // with an existing OAuth identity for the same email address.
212
255
  let existing;
213
256
  try {
214
257
  const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
@@ -227,6 +270,29 @@ export async function signUp(email, password, name, options) {
227
270
  error: 'Unable to create account',
228
271
  };
229
272
  }
273
+ // Block signup if an OAuth account already uses this email.
274
+ // Without this check, an attacker could create a password account
275
+ // for an email that was registered via OAuth, enabling account takeover.
276
+ try {
277
+ const [existingOAuth] = await db
278
+ .select({ id: oauthAccounts.id })
279
+ .from(oauthAccounts)
280
+ .where(eq(oauthAccounts.providerEmail, email))
281
+ .limit(1);
282
+ if (existingOAuth) {
283
+ return {
284
+ success: false,
285
+ error: 'Unable to create account',
286
+ };
287
+ }
288
+ }
289
+ catch {
290
+ logger.error('Error checking OAuth accounts');
291
+ return {
292
+ success: false,
293
+ error: 'Database error',
294
+ };
295
+ }
230
296
  // Hash password
231
297
  let hashedPassword;
232
298
  try {
@@ -290,6 +356,17 @@ export async function signUp(email, password, name, options) {
290
356
  }
291
357
  catch {
292
358
  logger.error('Error creating session');
359
+ // Clean up orphaned user so the email isn't permanently locked out.
360
+ // Without this, a retry would fail with "Unable to create account"
361
+ // because the user row already exists but has no valid session.
362
+ try {
363
+ await db.delete(users).where(eq(users.id, user.id));
364
+ }
365
+ catch {
366
+ logger.error('Failed to clean up orphaned user after session creation failure', undefined, {
367
+ userId: user.id,
368
+ });
369
+ }
293
370
  return {
294
371
  success: false,
295
372
  error: 'Failed to create session',
@@ -2,13 +2,22 @@
2
2
  * Brute Force Protection
3
3
  *
4
4
  * Tracks failed login attempts and locks accounts after threshold.
5
- * Uses storage abstraction (Redis, database, or in-memory).
5
+ * Uses storage abstraction (database or in-memory).
6
6
  */
7
7
  export interface BruteForceConfig {
8
8
  maxAttempts: number;
9
9
  lockDurationMs: number;
10
10
  windowMs: number;
11
11
  }
12
+ /**
13
+ * Override default brute force configuration globally.
14
+ * Per-call config parameters still take precedence.
15
+ */
16
+ export declare function configureBruteForce(overrides: Partial<BruteForceConfig>): void;
17
+ /**
18
+ * Reset brute force configuration to defaults (for testing).
19
+ */
20
+ export declare function resetBruteForceConfig(): void;
12
21
  /**
13
22
  * Records a failed login attempt
14
23
  *
@@ -1 +1 @@
1
- {"version":3,"file":"brute-force.d.ts","sourceRoot":"","sources":["../../src/server/brute-force.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAqCD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,gBAAiC,GACxC,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAItE;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,gBAAiC,GACxC,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,CA6C7E;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ1E"}
1
+ {"version":3,"file":"brute-force.d.ts","sourceRoot":"","sources":["../../src/server/brute-force.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAUD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAE9E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AA+BD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,gBAA+B,GACtC,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAItE;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,gBAA+B,GACtC,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,CA6C7E;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ1E"}
@@ -2,7 +2,7 @@
2
2
  * Brute Force Protection
3
3
  *
4
4
  * Tracks failed login attempts and locks accounts after threshold.
5
- * Uses storage abstraction (Redis, database, or in-memory).
5
+ * Uses storage abstraction (database or in-memory).
6
6
  */
7
7
  import { getStorage } from './storage/index.js';
8
8
  const DEFAULT_CONFIG = {
@@ -10,6 +10,20 @@ const DEFAULT_CONFIG = {
10
10
  lockDurationMs: 30 * 60 * 1000, // 30 minutes
11
11
  windowMs: 15 * 60 * 1000, // 15 minutes
12
12
  };
13
+ let globalConfig = { ...DEFAULT_CONFIG };
14
+ /**
15
+ * Override default brute force configuration globally.
16
+ * Per-call config parameters still take precedence.
17
+ */
18
+ export function configureBruteForce(overrides) {
19
+ globalConfig = { ...DEFAULT_CONFIG, ...overrides };
20
+ }
21
+ /**
22
+ * Reset brute force configuration to defaults (for testing).
23
+ */
24
+ export function resetBruteForceConfig() {
25
+ globalConfig = { ...DEFAULT_CONFIG };
26
+ }
13
27
  /**
14
28
  * Serialize failed attempt entry
15
29
  */
@@ -42,7 +56,7 @@ function getStorageKey(email) {
42
56
  * @param email - User email
43
57
  * @param config - Brute force configuration
44
58
  */
45
- export async function recordFailedAttempt(email, config = DEFAULT_CONFIG) {
59
+ export async function recordFailedAttempt(email, config = globalConfig) {
46
60
  const storage = getStorage();
47
61
  const storageKey = getStorageKey(email);
48
62
  const updater = (entryData) => {
@@ -94,7 +108,7 @@ export async function clearFailedAttempts(email) {
94
108
  * @param config - Brute force configuration
95
109
  * @returns Lock status
96
110
  */
97
- export async function isAccountLocked(email, config = DEFAULT_CONFIG) {
111
+ export async function isAccountLocked(email, config = globalConfig) {
98
112
  const storage = getStorage();
99
113
  const storageKey = getStorageKey(email);
100
114
  const now = Date.now();
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,qBAAa,SAAU,SAAQ,KAAK;IAGzB,IAAI,EAAE,MAAM;IACZ,UAAU,EAAE,MAAM;gBAFzB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAAY;CAKlC;AAED,qBAAa,YAAa,SAAQ,SAAS;gBAC7B,OAAO,GAAE,MAAwB,EAAE,UAAU,GAAE,MAAY;CAIxE;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,GAAE,MAAgC,EAAE,UAAU,GAAE,MAAY;CAIhF;AAED,qBAAa,aAAc,SAAQ,SAAS;IACnC,aAAa,CAAC,EAAE,KAAK,CAAA;gBAEhB,OAAO,GAAE,MAAyB,EAAE,aAAa,CAAC,EAAE,OAAO;CAOxE;AAED,qBAAa,UAAW,SAAQ,SAAS;gBAC3B,OAAO,GAAE,MAAsB,EAAE,UAAU,GAAE,MAAY;CAItE;AAED,qBAAa,yBAA0B,SAAQ,SAAS;IAC/C,KAAK,EAAE,MAAM,CAAA;gBAER,KAAK,EAAE,MAAM;CAS1B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,qBAAa,SAAU,SAAQ,KAAK;IAGzB,IAAI,EAAE,MAAM;IACZ,UAAU,EAAE,MAAM;gBAFzB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAAY;CAKlC;AAED,qBAAa,YAAa,SAAQ,SAAS;gBAC7B,OAAO,GAAE,MAAwB,EAAE,UAAU,GAAE,MAAY;CAIxE;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,GAAE,MAAgC,EAAE,UAAU,GAAE,MAAY;CAIhF;AAED,qBAAa,aAAc,SAAQ,SAAS;IACnC,aAAa,CAAC,EAAE,KAAK,CAAC;gBAEjB,OAAO,GAAE,MAAyB,EAAE,aAAa,CAAC,EAAE,OAAO;CAOxE;AAED,qBAAa,UAAW,SAAQ,SAAS;gBAC3B,OAAO,GAAE,MAAsB,EAAE,UAAU,GAAE,MAAY;CAItE;AAED,qBAAa,yBAA0B,SAAQ,SAAS;IAC/C,KAAK,EAAE,MAAM,CAAC;gBAET,KAAK,EAAE,MAAM;CAS1B"}
@@ -6,12 +6,22 @@
6
6
  */
7
7
  export type { SignInResult, SignUpResult } from '../types.js';
8
8
  export { isSignupAllowed, signIn, signUp } from './auth.js';
9
- export { clearFailedAttempts, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, } from './brute-force.js';
9
+ export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
10
10
  export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.js';
11
- export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, type ProviderUser, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
12
- export type { PasswordResetResult, PasswordResetToken } from './password-reset.js';
13
- export { generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
11
+ export type { MagicLinkConfig } from './magic-link.js';
12
+ export { configureMagicLink, createMagicLink, resetMagicLinkConfig, verifyMagicLink, } from './magic-link.js';
13
+ export type { MFAConfig, MFADisableProof, MFASetupResult } from './mfa.js';
14
+ export { configureMFA, disableMFA, initiateMFASetup, isMFAEnabled, regenerateBackupCodes, resetMFAConfig, verifyBackupCode, verifyMFACode, verifyMFASetup, } from './mfa.js';
15
+ export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getLinkedProviders, linkOAuthAccount, type ProviderUser, type UpsertOAuthOptions, unlinkOAuthAccount, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
16
+ export type { PasskeyConfig } from './passkey.js';
17
+ export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenticationChallenge, generateRegistrationChallenge, listPasskeys, renamePasskey, resetPasskeyConfig, storePasskey, verifyAuthentication, verifyRegistration, } from './passkey.js';
18
+ export type { ChangePasswordResult, PasswordResetResult, PasswordResetToken, } from './password-reset.js';
19
+ export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
14
20
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
15
- export { checkRateLimit, getRateLimitStatus, resetRateLimit, } from './rate-limit.js';
16
- export { createSession, deleteAllUserSessions, deleteSession, getSession, } from './session.js';
21
+ export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
22
+ export type { RequestContext, SessionBindingConfig, SessionData } from './session.js';
23
+ export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
24
+ export { signCookiePayload, verifyCookiePayload } from './signed-cookie.js';
25
+ export type { Storage } from './storage/index.js';
26
+ export { createStorage, DatabaseStorage, getStorage, InMemoryStorage, resetStorage, } from './storage/index.js';
17
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAC3D,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,YAAY,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAClF,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,aAAa,EACb,UAAU,GACX,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,+BAA+B,EAC/B,6BAA6B,EAC7B,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAC"}
@@ -5,10 +5,16 @@
5
5
  * Inspired by Better Auth and Neon Auth patterns.
6
6
  */
7
7
  export { isSignupAllowed, signIn, signUp } from './auth.js';
8
- export { clearFailedAttempts, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, } from './brute-force.js';
8
+ export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
9
9
  export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.js';
10
- export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
11
- export { generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
10
+ export { configureMagicLink, createMagicLink, resetMagicLinkConfig, verifyMagicLink, } from './magic-link.js';
11
+ export { configureMFA, disableMFA, initiateMFASetup, isMFAEnabled, regenerateBackupCodes, resetMFAConfig, verifyBackupCode, verifyMFACode, verifyMFASetup, } from './mfa.js';
12
+ export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getLinkedProviders, linkOAuthAccount, unlinkOAuthAccount, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
13
+ export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenticationChallenge, generateRegistrationChallenge, listPasskeys, renamePasskey, resetPasskeyConfig, storePasskey, verifyAuthentication, verifyRegistration, } from './passkey.js';
14
+ export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
12
15
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
13
- export { checkRateLimit, getRateLimitStatus, resetRateLimit, } from './rate-limit.js';
14
- export { createSession, deleteAllUserSessions, deleteSession, getSession, } from './session.js';
16
+ export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
17
+ export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
18
+ // Signed Cookie
19
+ export { signCookiePayload, verifyCookiePayload } from './signed-cookie.js';
20
+ export { createStorage, DatabaseStorage, getStorage, InMemoryStorage, resetStorage, } from './storage/index.js';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Magic Link Token Module
3
+ *
4
+ * Generates and verifies single-use, time-limited tokens for passwordless
5
+ * email authentication and account recovery flows.
6
+ *
7
+ * Tokens are stored in the database as HMAC-SHA256 hashes with per-token salts,
8
+ * following the same security pattern as password-reset.ts.
9
+ */
10
+ export interface MagicLinkConfig {
11
+ /** Token expiry in ms (default: 15 minutes) */
12
+ tokenExpiryMs: number;
13
+ /** Temp session duration in ms (default: 30 minutes) */
14
+ tempSessionDurationMs: number;
15
+ /** Max requests per hour per email (default: 3) */
16
+ maxRequestsPerHour: number;
17
+ }
18
+ export declare function configureMagicLink(overrides: Partial<MagicLinkConfig>): void;
19
+ export declare function resetMagicLinkConfig(): void;
20
+ /**
21
+ * Creates a magic link token for a user.
22
+ *
23
+ * - Generates a 32-byte random token
24
+ * - Hashes it with HMAC-SHA256 + per-token salt
25
+ * - Cleans up expired magic links for the same user (opportunistic)
26
+ * - Inserts the hashed token into the database
27
+ *
28
+ * @param userId - User ID to create the magic link for
29
+ * @returns The plaintext token (to embed in the email link) and its expiry
30
+ */
31
+ export declare function createMagicLink(userId: string): Promise<{
32
+ token: string;
33
+ expiresAt: Date;
34
+ }>;
35
+ /**
36
+ * Verifies a magic link token.
37
+ *
38
+ * Selects all unexpired, unused magic links and checks each one against the
39
+ * provided token using HMAC-SHA256 + timingSafeEqual. This is a table scan
40
+ * by design (same approach as password-reset.ts validation). The table stays
41
+ * small due to opportunistic cleanup in createMagicLink.
42
+ *
43
+ * On match: marks the token as used and returns the userId.
44
+ * On no match: returns null.
45
+ *
46
+ * @param token - Plaintext token from the magic link URL
47
+ * @returns Object with userId if valid, null otherwise
48
+ */
49
+ export declare function verifyMagicLink(token: string): Promise<{
50
+ userId: string;
51
+ } | null>;
52
+ //# sourceMappingURL=magic-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-link.d.ts","sourceRoot":"","sources":["../../src/server/magic-link.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mDAAmD;IACnD,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAUD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAE5E;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AA0BD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAyBjG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAuBvF"}