@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.
- package/README.md +58 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/useMFA.d.ts +83 -0
- package/dist/react/useMFA.d.ts.map +1 -0
- package/dist/react/useMFA.js +182 -0
- package/dist/react/usePasskey.d.ts +88 -0
- package/dist/react/usePasskey.d.ts.map +1 -0
- package/dist/react/usePasskey.js +203 -0
- package/dist/react/useSession.d.ts.map +1 -1
- package/dist/react/useSession.js +16 -5
- package/dist/react/useSignIn.d.ts +9 -3
- package/dist/react/useSignIn.d.ts.map +1 -1
- package/dist/react/useSignIn.js +32 -10
- package/dist/react/useSignOut.d.ts.map +1 -1
- package/dist/react/useSignUp.d.ts +1 -0
- package/dist/react/useSignUp.d.ts.map +1 -1
- package/dist/react/useSignUp.js +25 -9
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +93 -5
- package/dist/server/brute-force.d.ts +10 -1
- package/dist/server/brute-force.d.ts.map +1 -1
- package/dist/server/brute-force.js +46 -23
- package/dist/server/errors.d.ts +4 -0
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +8 -0
- package/dist/server/index.d.ts +17 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +12 -5
- package/dist/server/magic-link.d.ts +52 -0
- package/dist/server/magic-link.d.ts.map +1 -0
- package/dist/server/magic-link.js +111 -0
- package/dist/server/mfa.d.ts +87 -0
- package/dist/server/mfa.d.ts.map +1 -0
- package/dist/server/mfa.js +263 -0
- package/dist/server/oauth.d.ts +86 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +355 -0
- package/dist/server/passkey.d.ts +132 -0
- package/dist/server/passkey.d.ts.map +1 -0
- package/dist/server/passkey.js +257 -0
- package/dist/server/password-reset.d.ts +32 -6
- package/dist/server/password-reset.d.ts.map +1 -1
- package/dist/server/password-reset.js +116 -47
- package/dist/server/password-validation.d.ts.map +1 -1
- package/dist/server/providers/github.d.ts +14 -0
- package/dist/server/providers/github.d.ts.map +1 -0
- package/dist/server/providers/github.js +89 -0
- package/dist/server/providers/google.d.ts +11 -0
- package/dist/server/providers/google.d.ts.map +1 -0
- package/dist/server/providers/google.js +69 -0
- package/dist/server/providers/vercel.d.ts +11 -0
- package/dist/server/providers/vercel.d.ts.map +1 -0
- package/dist/server/providers/vercel.js +63 -0
- package/dist/server/rate-limit.d.ts +10 -1
- package/dist/server/rate-limit.d.ts.map +1 -1
- package/dist/server/rate-limit.js +61 -43
- package/dist/server/session.d.ts +48 -1
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +126 -7
- package/dist/server/signed-cookie.d.ts +32 -0
- package/dist/server/signed-cookie.d.ts.map +1 -0
- package/dist/server/signed-cookie.js +67 -0
- package/dist/server/storage/database.d.ts +10 -1
- package/dist/server/storage/database.d.ts.map +1 -1
- package/dist/server/storage/database.js +43 -5
- package/dist/server/storage/in-memory.d.ts +4 -0
- package/dist/server/storage/in-memory.d.ts.map +1 -1
- package/dist/server/storage/in-memory.js +16 -6
- package/dist/server/storage/index.d.ts +11 -3
- package/dist/server/storage/index.d.ts.map +1 -1
- package/dist/server/storage/index.js +18 -4
- package/dist/server/storage/interface.d.ts +11 -1
- package/dist/server/storage/interface.d.ts.map +1 -1
- package/dist/server/storage/interface.js +1 -1
- package/dist/types.d.ts +23 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +12 -2
- package/dist/utils/token.d.ts +9 -1
- package/dist/utils/token.d.ts.map +1 -1
- package/dist/utils/token.js +9 -1
- 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 (
|
|
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,
|
|
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 (
|
|
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 =
|
|
59
|
+
export async function recordFailedAttempt(email, config = globalConfig) {
|
|
46
60
|
const storage = getStorage();
|
|
47
61
|
const storageKey = getStorageKey(email);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
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();
|
package/dist/server/errors.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/server/errors.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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 {
|
|
12
|
-
export {
|
|
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 {
|
|
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,
|
|
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"}
|
package/dist/server/index.js
CHANGED
|
@@ -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 {
|
|
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"}
|