clearauth 0.3.0

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 (88) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/LICENSE +21 -0
  3. package/README.md +417 -0
  4. package/dist/auth/handler.d.ts +38 -0
  5. package/dist/auth/handler.js +483 -0
  6. package/dist/auth/handler.js.map +1 -0
  7. package/dist/auth/login.d.ts +69 -0
  8. package/dist/auth/login.js +103 -0
  9. package/dist/auth/login.js.map +1 -0
  10. package/dist/auth/register.d.ts +72 -0
  11. package/dist/auth/register.js +122 -0
  12. package/dist/auth/register.js.map +1 -0
  13. package/dist/auth/reset-password.d.ts +106 -0
  14. package/dist/auth/reset-password.js +213 -0
  15. package/dist/auth/reset-password.js.map +1 -0
  16. package/dist/auth/utils.d.ts +58 -0
  17. package/dist/auth/utils.js +121 -0
  18. package/dist/auth/utils.js.map +1 -0
  19. package/dist/auth/verify-email.d.ts +70 -0
  20. package/dist/auth/verify-email.js +137 -0
  21. package/dist/auth/verify-email.js.map +1 -0
  22. package/dist/createMechAuth.d.ts +178 -0
  23. package/dist/createMechAuth.js +215 -0
  24. package/dist/createMechAuth.js.map +1 -0
  25. package/dist/database/schema.d.ts +135 -0
  26. package/dist/database/schema.js +37 -0
  27. package/dist/database/schema.js.map +1 -0
  28. package/dist/edge.d.ts +4 -0
  29. package/dist/edge.js +6 -0
  30. package/dist/edge.js.map +1 -0
  31. package/dist/errors.d.ts +25 -0
  32. package/dist/errors.js +44 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/handler.d.ts +100 -0
  35. package/dist/handler.js +213 -0
  36. package/dist/handler.js.map +1 -0
  37. package/dist/index.d.ts +22 -0
  38. package/dist/index.js +28 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/logger.d.ts +22 -0
  41. package/dist/logger.js +40 -0
  42. package/dist/logger.js.map +1 -0
  43. package/dist/mech-kysely.d.ts +22 -0
  44. package/dist/mech-kysely.js +88 -0
  45. package/dist/mech-kysely.js.map +1 -0
  46. package/dist/mech-sql-client.d.ts +85 -0
  47. package/dist/mech-sql-client.js +155 -0
  48. package/dist/mech-sql-client.js.map +1 -0
  49. package/dist/node.d.ts +4 -0
  50. package/dist/node.js +10 -0
  51. package/dist/node.js.map +1 -0
  52. package/dist/oauth/arctic-providers.d.ts +60 -0
  53. package/dist/oauth/arctic-providers.js +94 -0
  54. package/dist/oauth/arctic-providers.js.map +1 -0
  55. package/dist/oauth/callbacks.d.ts +155 -0
  56. package/dist/oauth/callbacks.js +286 -0
  57. package/dist/oauth/callbacks.js.map +1 -0
  58. package/dist/oauth/github.d.ts +47 -0
  59. package/dist/oauth/github.js +136 -0
  60. package/dist/oauth/github.js.map +1 -0
  61. package/dist/oauth/google.d.ts +49 -0
  62. package/dist/oauth/google.js +104 -0
  63. package/dist/oauth/google.js.map +1 -0
  64. package/dist/oauth/handler.d.ts +31 -0
  65. package/dist/oauth/handler.js +277 -0
  66. package/dist/oauth/handler.js.map +1 -0
  67. package/dist/password-hasher-argon2.d.ts +7 -0
  68. package/dist/password-hasher-argon2.js +16 -0
  69. package/dist/password-hasher-argon2.js.map +1 -0
  70. package/dist/password-hasher.d.ts +12 -0
  71. package/dist/password-hasher.js +115 -0
  72. package/dist/password-hasher.js.map +1 -0
  73. package/dist/react.d.ts +152 -0
  74. package/dist/react.js +296 -0
  75. package/dist/react.js.map +1 -0
  76. package/dist/types.d.ts +190 -0
  77. package/dist/types.js +7 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/utils/cors.d.ts +65 -0
  80. package/dist/utils/cors.js +152 -0
  81. package/dist/utils/cors.js.map +1 -0
  82. package/dist/utils/normalize-auth-path.d.ts +1 -0
  83. package/dist/utils/normalize-auth-path.js +8 -0
  84. package/dist/utils/normalize-auth-path.js.map +1 -0
  85. package/dist/validation.d.ts +23 -0
  86. package/dist/validation.js +70 -0
  87. package/dist/validation.js.map +1 -0
  88. package/package.json +93 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * User Registration
3
+ *
4
+ * Handles user registration with email and password:
5
+ * - Email and password validation
6
+ * - Password hashing using Argon2id
7
+ * - User creation with email_verified=false
8
+ * - Email verification token generation
9
+ * - Initial session creation
10
+ */
11
+ import type { Kysely } from 'kysely';
12
+ import type { Database, User } from '../database/schema.js';
13
+ import type { RequestContext } from '../types.js';
14
+ import type { PasswordHasher } from '../password-hasher.js';
15
+ /**
16
+ * Register a new user with email and password
17
+ *
18
+ * Creates a new user account with the provided email and password.
19
+ * The password is hashed using Argon2id before storage.
20
+ * An email verification token is generated and stored.
21
+ * An initial session is created for the user.
22
+ *
23
+ * @param db - Kysely database instance
24
+ * @param email - User's email address
25
+ * @param password - User's password (plain text, will be hashed)
26
+ * @param context - Optional request context (IP address, user agent)
27
+ * @returns User record, session ID, and verification token
28
+ * @throws {AuthError} If validation fails or user already exists
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const result = await registerUser(db, 'user@example.com', 'SecurePass123!', {
33
+ * ipAddress: '192.168.1.1',
34
+ * userAgent: 'Mozilla/5.0...'
35
+ * })
36
+ * console.log(result.user.id, result.sessionId, result.verificationToken)
37
+ * ```
38
+ */
39
+ export declare function registerUser(db: Kysely<Database>, email: string, password: string, context?: RequestContext, passwordHasher?: PasswordHasher): Promise<{
40
+ user: User;
41
+ sessionId: string;
42
+ verificationToken: string;
43
+ }>;
44
+ /**
45
+ * Register user result type (for HTTP responses)
46
+ */
47
+ export interface RegisterUserResult {
48
+ user: {
49
+ id: string;
50
+ email: string;
51
+ email_verified: boolean;
52
+ name: string | null;
53
+ avatar_url: string | null;
54
+ created_at: Date;
55
+ };
56
+ sessionId: string;
57
+ verificationToken: string;
58
+ }
59
+ /**
60
+ * Convert registration result to public format
61
+ *
62
+ * Removes sensitive fields like password_hash and provider IDs.
63
+ *
64
+ * @param result - Registration result
65
+ * @returns Public registration result
66
+ * @internal
67
+ */
68
+ export declare function toPublicRegisterResult(result: {
69
+ user: User;
70
+ sessionId: string;
71
+ verificationToken: string;
72
+ }): RegisterUserResult;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * User Registration
3
+ *
4
+ * Handles user registration with email and password:
5
+ * - Email and password validation
6
+ * - Password hashing using Argon2id
7
+ * - User creation with email_verified=false
8
+ * - Email verification token generation
9
+ * - Initial session creation
10
+ */
11
+ import { createSession } from '../oauth/callbacks.js';
12
+ import { createPbkdf2PasswordHasher } from '../password-hasher.js';
13
+ import { isValidEmail, validatePassword, normalizeEmail, generateSecureToken, createAuthError, } from './utils.js';
14
+ const defaultPasswordHasher = createPbkdf2PasswordHasher();
15
+ /**
16
+ * Email verification token expiration (24 hours)
17
+ */
18
+ const VERIFICATION_TOKEN_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
19
+ /**
20
+ * Register a new user with email and password
21
+ *
22
+ * Creates a new user account with the provided email and password.
23
+ * The password is hashed using Argon2id before storage.
24
+ * An email verification token is generated and stored.
25
+ * An initial session is created for the user.
26
+ *
27
+ * @param db - Kysely database instance
28
+ * @param email - User's email address
29
+ * @param password - User's password (plain text, will be hashed)
30
+ * @param context - Optional request context (IP address, user agent)
31
+ * @returns User record, session ID, and verification token
32
+ * @throws {AuthError} If validation fails or user already exists
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const result = await registerUser(db, 'user@example.com', 'SecurePass123!', {
37
+ * ipAddress: '192.168.1.1',
38
+ * userAgent: 'Mozilla/5.0...'
39
+ * })
40
+ * console.log(result.user.id, result.sessionId, result.verificationToken)
41
+ * ```
42
+ */
43
+ export async function registerUser(db, email, password, context, passwordHasher) {
44
+ const hasher = passwordHasher ?? defaultPasswordHasher;
45
+ // Validate email
46
+ if (!isValidEmail(email)) {
47
+ throw createAuthError('Invalid email address', 'INVALID_EMAIL', 400);
48
+ }
49
+ // Validate password
50
+ const passwordError = validatePassword(password);
51
+ if (passwordError) {
52
+ throw createAuthError(passwordError, 'INVALID_PASSWORD', 400);
53
+ }
54
+ // Normalize email
55
+ const normalizedEmail = normalizeEmail(email);
56
+ // Check if user already exists
57
+ const existingUser = await db
58
+ .selectFrom('users')
59
+ .select('id')
60
+ .where('email', '=', normalizedEmail)
61
+ .executeTakeFirst();
62
+ if (existingUser) {
63
+ throw createAuthError('User with this email already exists', 'EMAIL_EXISTS', 409);
64
+ }
65
+ // Hash password
66
+ const passwordHash = await hasher.hash(password);
67
+ // Create user
68
+ const newUser = {
69
+ email: normalizedEmail,
70
+ email_verified: false,
71
+ password_hash: passwordHash,
72
+ github_id: null,
73
+ google_id: null,
74
+ name: null,
75
+ avatar_url: null,
76
+ };
77
+ const user = await db.insertInto('users').values(newUser).returningAll().executeTakeFirstOrThrow();
78
+ // Generate email verification token
79
+ const verificationToken = generateSecureToken(32); // 256 bits of entropy
80
+ const expiresAt = new Date(Date.now() + VERIFICATION_TOKEN_EXPIRY);
81
+ const newToken = {
82
+ token: verificationToken,
83
+ user_id: user.id,
84
+ email: normalizedEmail,
85
+ expires_at: expiresAt,
86
+ };
87
+ await db.insertInto('email_verification_tokens').values(newToken).execute();
88
+ // Create initial session
89
+ const sessionId = await createSession(db, user.id, 2592000, {
90
+ ipAddress: context?.ipAddress,
91
+ userAgent: context?.userAgent,
92
+ });
93
+ return {
94
+ user,
95
+ sessionId,
96
+ verificationToken,
97
+ };
98
+ }
99
+ /**
100
+ * Convert registration result to public format
101
+ *
102
+ * Removes sensitive fields like password_hash and provider IDs.
103
+ *
104
+ * @param result - Registration result
105
+ * @returns Public registration result
106
+ * @internal
107
+ */
108
+ export function toPublicRegisterResult(result) {
109
+ return {
110
+ user: {
111
+ id: result.user.id,
112
+ email: result.user.email,
113
+ email_verified: result.user.email_verified,
114
+ name: result.user.name,
115
+ avatar_url: result.user.avatar_url,
116
+ created_at: result.user.created_at,
117
+ },
118
+ sessionId: result.sessionId,
119
+ verificationToken: result.verificationToken,
120
+ };
121
+ }
122
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/auth/register.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Password Reset
3
+ *
4
+ * Handles password reset flow:
5
+ * - Password reset token generation and storage
6
+ * - Token validation and expiration checking
7
+ * - Password update with new hash
8
+ * - Session invalidation for security
9
+ */
10
+ import type { Kysely } from 'kysely';
11
+ import type { Database } from '../database/schema.js';
12
+ import type { PasswordHasher } from '../password-hasher.js';
13
+ /**
14
+ * Request password reset
15
+ *
16
+ * Generates a password reset token and stores it in the database.
17
+ * The token expires after 1 hour for security.
18
+ *
19
+ * **IMPORTANT:** This function does NOT return the token to prevent email enumeration.
20
+ * The token should be sent via email by the caller using an email service.
21
+ * The function always returns success, even if the user doesn't exist.
22
+ *
23
+ * @param db - Kysely database instance
24
+ * @param email - User's email address
25
+ * @param onTokenGenerated - Optional callback to send the token via email
26
+ * @returns Success status and email (for sending the token)
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const result = await requestPasswordReset(db, 'user@example.com', async (email, token) => {
31
+ * await sendEmail({
32
+ * to: email,
33
+ * subject: 'Password Reset',
34
+ * template: 'password-reset',
35
+ * data: { token, resetUrl: `https://example.com/reset-password?token=${token}` }
36
+ * })
37
+ * })
38
+ * // Always returns { success: true } regardless of whether user exists
39
+ * ```
40
+ */
41
+ export declare function requestPasswordReset(db: Kysely<Database>, email: string, onTokenGenerated?: (email: string, token: string) => Promise<void>): Promise<{
42
+ success: true;
43
+ email: string;
44
+ }>;
45
+ /**
46
+ * Verify password reset token
47
+ *
48
+ * Checks if a password reset token is valid and not expired.
49
+ * Does not consume the token.
50
+ *
51
+ * @param db - Kysely database instance
52
+ * @param token - Password reset token
53
+ * @returns Validation result with user ID if valid
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const result = await verifyResetToken(db, 'abc123...')
58
+ * if (result.valid) {
59
+ * console.log('Token is valid for user:', result.userId)
60
+ * }
61
+ * ```
62
+ */
63
+ export declare function verifyResetToken(db: Kysely<Database>, token: string): Promise<{
64
+ valid: boolean;
65
+ userId?: string;
66
+ }>;
67
+ /**
68
+ * Reset password using token
69
+ *
70
+ * Updates the user's password with a new hash.
71
+ * Invalidates all existing sessions for security.
72
+ * Deletes the used reset token.
73
+ *
74
+ * @param db - Kysely database instance
75
+ * @param token - Password reset token
76
+ * @param newPassword - New password (plain text, will be hashed)
77
+ * @returns Success status
78
+ * @throws {AuthError} If token is invalid, expired, or password is invalid
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const result = await resetPassword(db, 'abc123...', 'NewSecurePass123!')
83
+ * if (result.success) {
84
+ * console.log('Password reset successful')
85
+ * }
86
+ * ```
87
+ */
88
+ export declare function resetPassword(db: Kysely<Database>, token: string, newPassword: string, passwordHasher?: PasswordHasher): Promise<{
89
+ success: boolean;
90
+ }>;
91
+ /**
92
+ * Clean up expired password reset tokens
93
+ *
94
+ * Removes all expired password reset tokens from the database.
95
+ * This should be run periodically as a background job.
96
+ *
97
+ * @param db - Kysely database instance
98
+ * @returns Number of tokens deleted
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const deleted = await cleanupExpiredResetTokens(db)
103
+ * console.log(`Cleaned up ${deleted} expired reset tokens`)
104
+ * ```
105
+ */
106
+ export declare function cleanupExpiredResetTokens(db: Kysely<Database>): Promise<number>;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Password Reset
3
+ *
4
+ * Handles password reset flow:
5
+ * - Password reset token generation and storage
6
+ * - Token validation and expiration checking
7
+ * - Password update with new hash
8
+ * - Session invalidation for security
9
+ */
10
+ import { deleteAllUserSessions } from '../oauth/callbacks.js';
11
+ import { generateSecureToken, normalizeEmail, validatePassword, createAuthError } from './utils.js';
12
+ import { isValidPasswordResetToken } from '../database/schema.js';
13
+ import { createPbkdf2PasswordHasher } from '../password-hasher.js';
14
+ /**
15
+ * Password reset token expiration (1 hour for security)
16
+ */
17
+ const RESET_TOKEN_EXPIRY = 60 * 60 * 1000; // 1 hour in milliseconds
18
+ const defaultPasswordHasher = createPbkdf2PasswordHasher();
19
+ /**
20
+ * Request password reset
21
+ *
22
+ * Generates a password reset token and stores it in the database.
23
+ * The token expires after 1 hour for security.
24
+ *
25
+ * **IMPORTANT:** This function does NOT return the token to prevent email enumeration.
26
+ * The token should be sent via email by the caller using an email service.
27
+ * The function always returns success, even if the user doesn't exist.
28
+ *
29
+ * @param db - Kysely database instance
30
+ * @param email - User's email address
31
+ * @param onTokenGenerated - Optional callback to send the token via email
32
+ * @returns Success status and email (for sending the token)
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const result = await requestPasswordReset(db, 'user@example.com', async (email, token) => {
37
+ * await sendEmail({
38
+ * to: email,
39
+ * subject: 'Password Reset',
40
+ * template: 'password-reset',
41
+ * data: { token, resetUrl: `https://example.com/reset-password?token=${token}` }
42
+ * })
43
+ * })
44
+ * // Always returns { success: true } regardless of whether user exists
45
+ * ```
46
+ */
47
+ export async function requestPasswordReset(db, email, onTokenGenerated) {
48
+ const normalizedEmail = normalizeEmail(email);
49
+ // Look up user by email
50
+ const user = await db
51
+ .selectFrom('users')
52
+ .select(['id', 'email', 'password_hash'])
53
+ .where('email', '=', normalizedEmail)
54
+ .executeTakeFirst();
55
+ // If user doesn't exist, return success but don't send email
56
+ // This prevents email enumeration attacks
57
+ if (!user) {
58
+ // Simulate work to prevent timing attacks
59
+ await generateSecureToken(32);
60
+ return { success: true, email: normalizedEmail };
61
+ }
62
+ // If user exists but has no password (OAuth-only), don't send reset email
63
+ // Return success to avoid revealing whether email exists
64
+ if (!user.password_hash) {
65
+ return { success: true, email: normalizedEmail };
66
+ }
67
+ // Delete any existing reset tokens for this user
68
+ await db.deleteFrom('password_reset_tokens').where('user_id', '=', user.id).execute();
69
+ // Generate password reset token
70
+ const resetToken = generateSecureToken(32); // 256 bits of entropy
71
+ const expiresAt = new Date(Date.now() + RESET_TOKEN_EXPIRY);
72
+ const newToken = {
73
+ token: resetToken,
74
+ user_id: user.id,
75
+ expires_at: expiresAt,
76
+ };
77
+ await db.insertInto('password_reset_tokens').values(newToken).execute();
78
+ // Call the optional callback to send the token via email
79
+ if (onTokenGenerated) {
80
+ await onTokenGenerated(normalizedEmail, resetToken);
81
+ }
82
+ return {
83
+ success: true,
84
+ email: normalizedEmail,
85
+ };
86
+ }
87
+ /**
88
+ * Verify password reset token
89
+ *
90
+ * Checks if a password reset token is valid and not expired.
91
+ * Does not consume the token.
92
+ *
93
+ * @param db - Kysely database instance
94
+ * @param token - Password reset token
95
+ * @returns Validation result with user ID if valid
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const result = await verifyResetToken(db, 'abc123...')
100
+ * if (result.valid) {
101
+ * console.log('Token is valid for user:', result.userId)
102
+ * }
103
+ * ```
104
+ */
105
+ export async function verifyResetToken(db, token) {
106
+ if (!token || token.trim() === '') {
107
+ return { valid: false };
108
+ }
109
+ // Look up token in database
110
+ const tokenRecord = await db
111
+ .selectFrom('password_reset_tokens')
112
+ .selectAll()
113
+ .where('token', '=', token)
114
+ .executeTakeFirst();
115
+ if (!tokenRecord) {
116
+ return { valid: false };
117
+ }
118
+ // Check if token is expired
119
+ if (!isValidPasswordResetToken(tokenRecord)) {
120
+ // Delete expired token
121
+ await db.deleteFrom('password_reset_tokens').where('token', '=', token).execute();
122
+ return { valid: false };
123
+ }
124
+ return {
125
+ valid: true,
126
+ userId: tokenRecord.user_id,
127
+ };
128
+ }
129
+ /**
130
+ * Reset password using token
131
+ *
132
+ * Updates the user's password with a new hash.
133
+ * Invalidates all existing sessions for security.
134
+ * Deletes the used reset token.
135
+ *
136
+ * @param db - Kysely database instance
137
+ * @param token - Password reset token
138
+ * @param newPassword - New password (plain text, will be hashed)
139
+ * @returns Success status
140
+ * @throws {AuthError} If token is invalid, expired, or password is invalid
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * const result = await resetPassword(db, 'abc123...', 'NewSecurePass123!')
145
+ * if (result.success) {
146
+ * console.log('Password reset successful')
147
+ * }
148
+ * ```
149
+ */
150
+ export async function resetPassword(db, token, newPassword, passwordHasher) {
151
+ const hasher = passwordHasher ?? defaultPasswordHasher;
152
+ if (!token || token.trim() === '') {
153
+ throw createAuthError('Reset token is required', 'INVALID_TOKEN', 400);
154
+ }
155
+ // Validate new password
156
+ const passwordError = validatePassword(newPassword);
157
+ if (passwordError) {
158
+ throw createAuthError(passwordError, 'INVALID_PASSWORD', 400);
159
+ }
160
+ // Look up token in database
161
+ const tokenRecord = await db
162
+ .selectFrom('password_reset_tokens')
163
+ .selectAll()
164
+ .where('token', '=', token)
165
+ .executeTakeFirst();
166
+ if (!tokenRecord) {
167
+ throw createAuthError('Invalid or expired reset token', 'INVALID_TOKEN', 400);
168
+ }
169
+ // Check if token is expired
170
+ if (!isValidPasswordResetToken(tokenRecord)) {
171
+ // Delete expired token
172
+ await db.deleteFrom('password_reset_tokens').where('token', '=', token).execute();
173
+ throw createAuthError('Reset token has expired', 'TOKEN_EXPIRED', 400);
174
+ }
175
+ // Hash new password
176
+ const passwordHash = await hasher.hash(newPassword);
177
+ // Update user's password
178
+ await db
179
+ .updateTable('users')
180
+ .set({ password_hash: passwordHash })
181
+ .where('id', '=', tokenRecord.user_id)
182
+ .execute();
183
+ // Delete used reset token
184
+ await db.deleteFrom('password_reset_tokens').where('token', '=', token).execute();
185
+ // Invalidate all sessions for security
186
+ await deleteAllUserSessions(db, tokenRecord.user_id);
187
+ return {
188
+ success: true,
189
+ };
190
+ }
191
+ /**
192
+ * Clean up expired password reset tokens
193
+ *
194
+ * Removes all expired password reset tokens from the database.
195
+ * This should be run periodically as a background job.
196
+ *
197
+ * @param db - Kysely database instance
198
+ * @returns Number of tokens deleted
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const deleted = await cleanupExpiredResetTokens(db)
203
+ * console.log(`Cleaned up ${deleted} expired reset tokens`)
204
+ * ```
205
+ */
206
+ export async function cleanupExpiredResetTokens(db) {
207
+ const result = await db
208
+ .deleteFrom('password_reset_tokens')
209
+ .where('expires_at', '<=', new Date())
210
+ .executeTakeFirst();
211
+ return Number(result.numDeletedRows ?? 0);
212
+ }
213
+ //# sourceMappingURL=reset-password.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-password.js","sourceRoot":"","sources":["../../src/auth/reset-password.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Authentication Utilities
3
+ *
4
+ * Shared utilities for email/password authentication including:
5
+ * - Email validation
6
+ * - Password validation
7
+ * - Secure token generation
8
+ */
9
+ /**
10
+ * Validate email format
11
+ *
12
+ * @param email - Email address to validate
13
+ * @returns True if valid, false otherwise
14
+ */
15
+ export declare function isValidEmail(email: string): boolean;
16
+ /**
17
+ * Validate password strength
18
+ *
19
+ * @param password - Password to validate
20
+ * @param minLength - Minimum password length (default: 8)
21
+ * @returns Error message if invalid, null if valid
22
+ */
23
+ export declare function validatePassword(password: string, minLength?: number): string | null;
24
+ /**
25
+ * Generate a secure random token
26
+ *
27
+ * Uses cryptographically secure random values from Web Crypto API.
28
+ *
29
+ * @param entropySize - Number of bytes of entropy (default: 32)
30
+ * @returns Base64url-encoded token
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const token = generateSecureToken(32) // 256 bits of entropy
35
+ * ```
36
+ */
37
+ export declare function generateSecureToken(entropySize?: number): string;
38
+ /**
39
+ * Normalize email address
40
+ *
41
+ * Converts email to lowercase for consistent storage and comparison.
42
+ *
43
+ * @param email - Email address to normalize
44
+ * @returns Normalized email address
45
+ */
46
+ export declare function normalizeEmail(email: string): string;
47
+ /**
48
+ * Authentication error types
49
+ */
50
+ export declare class AuthError extends Error {
51
+ code: string;
52
+ statusCode: number;
53
+ constructor(message: string, code: string, statusCode?: number);
54
+ }
55
+ /**
56
+ * Create a standardized auth error
57
+ */
58
+ export declare function createAuthError(message: string, code: string, statusCode?: number): AuthError;