@revealui/auth 0.2.0 → 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 (87) hide show
  1. package/README.md +58 -34
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/react/index.d.ts +4 -0
  4. package/dist/react/index.d.ts.map +1 -1
  5. package/dist/react/index.js +2 -0
  6. package/dist/react/useMFA.d.ts +83 -0
  7. package/dist/react/useMFA.d.ts.map +1 -0
  8. package/dist/react/useMFA.js +182 -0
  9. package/dist/react/usePasskey.d.ts +88 -0
  10. package/dist/react/usePasskey.d.ts.map +1 -0
  11. package/dist/react/usePasskey.js +203 -0
  12. package/dist/react/useSession.d.ts.map +1 -1
  13. package/dist/react/useSession.js +16 -5
  14. package/dist/react/useSignIn.d.ts +9 -3
  15. package/dist/react/useSignIn.d.ts.map +1 -1
  16. package/dist/react/useSignIn.js +32 -10
  17. package/dist/react/useSignOut.d.ts.map +1 -1
  18. package/dist/react/useSignUp.d.ts +1 -0
  19. package/dist/react/useSignUp.d.ts.map +1 -1
  20. package/dist/react/useSignUp.js +25 -9
  21. package/dist/server/auth.d.ts +2 -0
  22. package/dist/server/auth.d.ts.map +1 -1
  23. package/dist/server/auth.js +93 -5
  24. package/dist/server/brute-force.d.ts +10 -1
  25. package/dist/server/brute-force.d.ts.map +1 -1
  26. package/dist/server/brute-force.js +46 -23
  27. package/dist/server/errors.d.ts +4 -0
  28. package/dist/server/errors.d.ts.map +1 -1
  29. package/dist/server/errors.js +8 -0
  30. package/dist/server/index.d.ts +17 -6
  31. package/dist/server/index.d.ts.map +1 -1
  32. package/dist/server/index.js +12 -5
  33. package/dist/server/magic-link.d.ts +52 -0
  34. package/dist/server/magic-link.d.ts.map +1 -0
  35. package/dist/server/magic-link.js +111 -0
  36. package/dist/server/mfa.d.ts +87 -0
  37. package/dist/server/mfa.d.ts.map +1 -0
  38. package/dist/server/mfa.js +263 -0
  39. package/dist/server/oauth.d.ts +86 -0
  40. package/dist/server/oauth.d.ts.map +1 -0
  41. package/dist/server/oauth.js +355 -0
  42. package/dist/server/passkey.d.ts +132 -0
  43. package/dist/server/passkey.d.ts.map +1 -0
  44. package/dist/server/passkey.js +257 -0
  45. package/dist/server/password-reset.d.ts +32 -6
  46. package/dist/server/password-reset.d.ts.map +1 -1
  47. package/dist/server/password-reset.js +116 -47
  48. package/dist/server/password-validation.d.ts.map +1 -1
  49. package/dist/server/providers/github.d.ts +14 -0
  50. package/dist/server/providers/github.d.ts.map +1 -0
  51. package/dist/server/providers/github.js +89 -0
  52. package/dist/server/providers/google.d.ts +11 -0
  53. package/dist/server/providers/google.d.ts.map +1 -0
  54. package/dist/server/providers/google.js +69 -0
  55. package/dist/server/providers/vercel.d.ts +11 -0
  56. package/dist/server/providers/vercel.d.ts.map +1 -0
  57. package/dist/server/providers/vercel.js +63 -0
  58. package/dist/server/rate-limit.d.ts +10 -1
  59. package/dist/server/rate-limit.d.ts.map +1 -1
  60. package/dist/server/rate-limit.js +61 -43
  61. package/dist/server/session.d.ts +48 -1
  62. package/dist/server/session.d.ts.map +1 -1
  63. package/dist/server/session.js +126 -7
  64. package/dist/server/signed-cookie.d.ts +32 -0
  65. package/dist/server/signed-cookie.d.ts.map +1 -0
  66. package/dist/server/signed-cookie.js +67 -0
  67. package/dist/server/storage/database.d.ts +10 -1
  68. package/dist/server/storage/database.d.ts.map +1 -1
  69. package/dist/server/storage/database.js +43 -5
  70. package/dist/server/storage/in-memory.d.ts +4 -0
  71. package/dist/server/storage/in-memory.d.ts.map +1 -1
  72. package/dist/server/storage/in-memory.js +16 -6
  73. package/dist/server/storage/index.d.ts +11 -3
  74. package/dist/server/storage/index.d.ts.map +1 -1
  75. package/dist/server/storage/index.js +18 -4
  76. package/dist/server/storage/interface.d.ts +11 -1
  77. package/dist/server/storage/interface.d.ts.map +1 -1
  78. package/dist/server/storage/interface.js +1 -1
  79. package/dist/types.d.ts +23 -8
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +2 -2
  82. package/dist/utils/database.d.ts.map +1 -1
  83. package/dist/utils/database.js +12 -2
  84. package/dist/utils/token.d.ts +9 -1
  85. package/dist/utils/token.d.ts.map +1 -1
  86. package/dist/utils/token.js +9 -1
  87. package/package.json +26 -8
@@ -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,CAiCf;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,31 +56,40 @@ 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
- const now = Date.now();
49
- const entryData = await storage.get(storageKey);
50
- const entry = deserializeEntry(entryData) || { count: 0, windowStart: now };
51
- // Reset if lock expired
52
- if (entry.lockUntil && entry.lockUntil < now) {
53
- entry.count = 0;
54
- entry.lockUntil = undefined;
55
- entry.windowStart = now;
56
- }
57
- // Reset if window expired
58
- if (now - entry.windowStart > config.windowMs) {
59
- entry.count = 0;
60
- entry.windowStart = now;
62
+ const updater = (entryData) => {
63
+ const now = Date.now();
64
+ const entry = deserializeEntry(entryData) || { count: 0, windowStart: now };
65
+ // Reset if lock expired
66
+ if (entry.lockUntil && entry.lockUntil < now) {
67
+ entry.count = 0;
68
+ entry.lockUntil = undefined;
69
+ entry.windowStart = now;
70
+ }
71
+ // Reset if window expired
72
+ if (now - entry.windowStart > config.windowMs) {
73
+ entry.count = 0;
74
+ entry.windowStart = now;
75
+ }
76
+ entry.count++;
77
+ // Lock account if threshold reached
78
+ if (entry.count >= config.maxAttempts) {
79
+ entry.lockUntil = now + config.lockDurationMs;
80
+ }
81
+ const ttlSeconds = Math.ceil(Math.max(config.windowMs, entry.lockUntil ? entry.lockUntil - now : config.windowMs) / 1000);
82
+ return { value: serializeEntry(entry), ttlSeconds };
83
+ };
84
+ if (storage.atomicUpdate) {
85
+ await storage.atomicUpdate(storageKey, updater);
61
86
  }
62
- entry.count++;
63
- // Lock account if threshold reached
64
- if (entry.count >= config.maxAttempts) {
65
- entry.lockUntil = now + config.lockDurationMs;
87
+ else {
88
+ // Fallback for storage backends that don't support atomic updates
89
+ const existing = await storage.get(storageKey);
90
+ const { value, ttlSeconds } = updater(existing);
91
+ await storage.set(storageKey, value, ttlSeconds);
66
92
  }
67
- // Store with TTL (window duration or lock duration, whichever is longer)
68
- const ttlSeconds = Math.ceil(Math.max(config.windowMs, entry.lockUntil ? entry.lockUntil - now : config.windowMs) / 1000);
69
- await storage.set(storageKey, serializeEntry(entry), ttlSeconds);
70
93
  }
71
94
  /**
72
95
  * Clears failed attempts for an email (on successful login)
@@ -85,7 +108,7 @@ export async function clearFailedAttempts(email) {
85
108
  * @param config - Brute force configuration
86
109
  * @returns Lock status
87
110
  */
88
- export async function isAccountLocked(email, config = DEFAULT_CONFIG) {
111
+ export async function isAccountLocked(email, config = globalConfig) {
89
112
  const storage = getStorage();
90
113
  const storageKey = getStorageKey(email);
91
114
  const now = Date.now();
@@ -21,4 +21,8 @@ export declare class DatabaseError extends AuthError {
21
21
  export declare class TokenError extends AuthError {
22
22
  constructor(message?: string, statusCode?: number);
23
23
  }
24
+ export declare class OAuthAccountConflictError extends AuthError {
25
+ email: string;
26
+ constructor(email: string);
27
+ }
24
28
  //# sourceMappingURL=errors.d.ts.map
@@ -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"}
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"}
@@ -41,3 +41,11 @@ export class TokenError extends AuthError {
41
41
  this.name = 'TokenError';
42
42
  }
43
43
  }
44
+ export class OAuthAccountConflictError extends AuthError {
45
+ email;
46
+ constructor(email) {
47
+ super('An account with this email already exists. Sign in with your password or original provider.', 'OAUTH_ACCOUNT_CONFLICT', 409);
48
+ this.name = 'OAuthAccountConflictError';
49
+ this.email = email;
50
+ }
51
+ }
@@ -6,11 +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';
10
- export { AuthError, AuthenticationError, DatabaseError, SessionError, TokenError, } from './errors.js';
11
- export type { PasswordResetResult, PasswordResetToken } from './password-reset.js';
12
- export { generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
9
+ export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
10
+ export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.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, 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';
13
20
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
14
- export { checkRateLimit, getRateLimitStatus, resetRateLimit, } from './rate-limit.js';
15
- 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, deleteSession, getSession, 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';
16
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,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,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,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,aAAa,EACb,UAAU,EACV,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,9 +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';
9
- export { AuthError, AuthenticationError, DatabaseError, SessionError, TokenError, } from './errors.js';
10
- export { generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
8
+ export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
9
+ export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.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';
11
15
  export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
12
- export { checkRateLimit, getRateLimitStatus, resetRateLimit, } from './rate-limit.js';
13
- export { createSession, deleteAllUserSessions, deleteSession, getSession, } from './session.js';
16
+ export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
17
+ export { configureSessionBinding, createSession, deleteAllUserSessions, deleteSession, getSession, 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"}
@@ -0,0 +1,111 @@
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
+ import crypto from 'node:crypto';
11
+ import { getClient } from '@revealui/db/client';
12
+ import { magicLinks } from '@revealui/db/schema';
13
+ import { and, eq, gt, isNull, lt } from 'drizzle-orm';
14
+ const DEFAULT_CONFIG = {
15
+ tokenExpiryMs: 15 * 60 * 1000,
16
+ tempSessionDurationMs: 30 * 60 * 1000,
17
+ maxRequestsPerHour: 3,
18
+ };
19
+ let config = { ...DEFAULT_CONFIG };
20
+ export function configureMagicLink(overrides) {
21
+ config = { ...DEFAULT_CONFIG, ...overrides };
22
+ }
23
+ export function resetMagicLinkConfig() {
24
+ config = { ...DEFAULT_CONFIG };
25
+ }
26
+ // =============================================================================
27
+ // Crypto helpers (same pattern as password-reset.ts)
28
+ // =============================================================================
29
+ /**
30
+ * Hash a token using HMAC-SHA256 with a per-token salt.
31
+ * The salt is stored in the DB alongside the hash; this defeats rainbow
32
+ * table attacks even if the database is fully compromised.
33
+ */
34
+ function hashToken(token, salt) {
35
+ return crypto.createHmac('sha256', salt).update(token).digest('hex');
36
+ }
37
+ /**
38
+ * Generate a 16-byte random salt (hex string).
39
+ */
40
+ function generateSalt() {
41
+ return crypto.randomBytes(16).toString('hex');
42
+ }
43
+ // =============================================================================
44
+ // Public API
45
+ // =============================================================================
46
+ /**
47
+ * Creates a magic link token for a user.
48
+ *
49
+ * - Generates a 32-byte random token
50
+ * - Hashes it with HMAC-SHA256 + per-token salt
51
+ * - Cleans up expired magic links for the same user (opportunistic)
52
+ * - Inserts the hashed token into the database
53
+ *
54
+ * @param userId - User ID to create the magic link for
55
+ * @returns The plaintext token (to embed in the email link) and its expiry
56
+ */
57
+ export async function createMagicLink(userId) {
58
+ const db = getClient();
59
+ // Generate secure token with per-token salt
60
+ const token = crypto.randomBytes(32).toString('hex');
61
+ const tokenSalt = generateSalt();
62
+ const tokenHash = hashToken(token, tokenSalt);
63
+ const expiresAt = new Date(Date.now() + config.tokenExpiryMs);
64
+ const id = crypto.randomUUID();
65
+ // Opportunistic cleanup: delete expired magic links for this user
66
+ await db
67
+ .delete(magicLinks)
68
+ .where(and(eq(magicLinks.userId, userId), lt(magicLinks.expiresAt, new Date())));
69
+ // Store hashed token + salt in database
70
+ await db.insert(magicLinks).values({
71
+ id,
72
+ userId,
73
+ tokenHash,
74
+ tokenSalt,
75
+ expiresAt,
76
+ });
77
+ return { token, expiresAt };
78
+ }
79
+ /**
80
+ * Verifies a magic link token.
81
+ *
82
+ * Selects all unexpired, unused magic links and checks each one against the
83
+ * provided token using HMAC-SHA256 + timingSafeEqual. This is a table scan
84
+ * by design (same approach as password-reset.ts validation). The table stays
85
+ * small due to opportunistic cleanup in createMagicLink.
86
+ *
87
+ * On match: marks the token as used and returns the userId.
88
+ * On no match: returns null.
89
+ *
90
+ * @param token - Plaintext token from the magic link URL
91
+ * @returns Object with userId if valid, null otherwise
92
+ */
93
+ export async function verifyMagicLink(token) {
94
+ const db = getClient();
95
+ // Select unexpired, unused magic links only — expired tokens can never match
96
+ const rows = await db
97
+ .select()
98
+ .from(magicLinks)
99
+ .where(and(isNull(magicLinks.usedAt), gt(magicLinks.expiresAt, new Date())));
100
+ for (const row of rows) {
101
+ const expectedHash = hashToken(token, row.tokenSalt);
102
+ const expectedBuf = Buffer.from(expectedHash);
103
+ const actualBuf = Buffer.from(row.tokenHash);
104
+ if (expectedBuf.length === actualBuf.length && crypto.timingSafeEqual(expectedBuf, actualBuf)) {
105
+ // Mark token as used (single-use enforcement)
106
+ await db.update(magicLinks).set({ usedAt: new Date() }).where(eq(magicLinks.id, row.id));
107
+ return { userId: row.userId };
108
+ }
109
+ }
110
+ return null;
111
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * MFA/2FA — TOTP-based Multi-Factor Authentication
3
+ *
4
+ * Uses the timing-safe TOTP implementation from @revealui/core/security/auth.
5
+ * Backup codes are bcrypt-hashed for storage (one-time use, consumed on verify).
6
+ */
7
+ export interface MFAConfig {
8
+ /** Number of backup codes to generate (default: 8) */
9
+ backupCodeCount: number;
10
+ /** Length of each backup code in bytes (default: 5, produces 10 hex chars) */
11
+ backupCodeLength: number;
12
+ /** Issuer name shown in authenticator apps */
13
+ issuer: string;
14
+ }
15
+ export declare function configureMFA(overrides: Partial<MFAConfig>): void;
16
+ export declare function resetMFAConfig(): void;
17
+ export interface MFASetupResult {
18
+ success: boolean;
19
+ /** Base32-encoded TOTP secret (show once) */
20
+ secret?: string;
21
+ /** otpauth:// URI for QR code */
22
+ uri?: string;
23
+ /** Plaintext backup codes (show once) */
24
+ backupCodes?: string[];
25
+ error?: string;
26
+ }
27
+ /**
28
+ * Initiate MFA setup for a user.
29
+ * Generates a TOTP secret and backup codes. The user must verify with a TOTP
30
+ * code before MFA is activated (see `verifyMFASetup`).
31
+ */
32
+ export declare function initiateMFASetup(userId: string, email: string): Promise<MFASetupResult>;
33
+ /**
34
+ * Verify MFA setup by confirming the user's authenticator app works.
35
+ * This activates MFA on the account.
36
+ */
37
+ export declare function verifyMFASetup(userId: string, code: string): Promise<{
38
+ success: boolean;
39
+ error?: string;
40
+ }>;
41
+ /**
42
+ * Verify a TOTP code during login (step 2 of MFA login flow).
43
+ */
44
+ export declare function verifyMFACode(userId: string, code: string): Promise<{
45
+ success: boolean;
46
+ error?: string;
47
+ }>;
48
+ /**
49
+ * Verify a backup code (one-time use). Consumes the code on success.
50
+ */
51
+ export declare function verifyBackupCode(userId: string, code: string): Promise<{
52
+ success: boolean;
53
+ remainingCodes?: number;
54
+ error?: string;
55
+ }>;
56
+ /**
57
+ * Regenerate backup codes (requires active MFA).
58
+ */
59
+ export declare function regenerateBackupCodes(userId: string): Promise<{
60
+ success: boolean;
61
+ backupCodes?: string[];
62
+ error?: string;
63
+ }>;
64
+ /**
65
+ * Discriminated union for MFA disable re-authentication proof.
66
+ * - `password`: traditional password confirmation
67
+ * - `passkey`: WebAuthn assertion already verified by the API route
68
+ */
69
+ export type MFADisableProof = {
70
+ method: 'password';
71
+ password: string;
72
+ } | {
73
+ method: 'passkey';
74
+ verified: true;
75
+ };
76
+ /**
77
+ * Disable MFA on a user account. Requires re-authentication proof.
78
+ */
79
+ export declare function disableMFA(userId: string, proof: MFADisableProof): Promise<{
80
+ success: boolean;
81
+ error?: string;
82
+ }>;
83
+ /**
84
+ * Check if a user has MFA enabled.
85
+ */
86
+ export declare function isMFAEnabled(userId: string): Promise<boolean>;
87
+ //# sourceMappingURL=mfa.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa.d.ts","sourceRoot":"","sources":["../../src/server/mfa.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,eAAe,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD,wBAAgB,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAEhE;AAED,wBAAgB,cAAc,IAAI,IAAI,CAErC;AA2CD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAuC7F;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsC/C;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmB/C;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAuCxE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwBvE;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC;AAE1C;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+C/C;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUnE"}