@revealui/auth 0.3.4 → 0.3.6

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 CHANGED
@@ -90,6 +90,20 @@ pnpm typecheck
90
90
  pnpm test
91
91
  ```
92
92
 
93
+ ## When to Use This
94
+
95
+ - You need session-based auth with database-backed sessions for a RevealUI app
96
+ - You want built-in brute force protection and rate limiting without external services
97
+ - You need React hooks for client-side session management (`useSession`, `useSignIn`, `useSignOut`)
98
+ - **Not** for OAuth-only flows — use a dedicated OAuth provider and wire tokens through this package
99
+ - **Not** for stateless JWT auth — this package uses database sessions by design
100
+
101
+ ## JOSHUA Alignment
102
+
103
+ - **Sovereign**: Sessions live in your PostgreSQL database, not a third-party auth service
104
+ - **Hermetic**: HTTP-only, SameSite cookies and SHA-256 token hashing prevent cross-boundary leaks
105
+ - **Justifiable**: Every security layer (bcrypt, progressive lockout, rate limiting) exists because the threat model demands it
106
+
93
107
  ## Related
94
108
 
95
109
  - [Core Package](../core/README.md) — CMS engine (uses auth for access control)
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Security Event Bridge — Connects auth events to the audit trail.
3
+ *
4
+ * Each function wraps an auth operation with structured audit logging
5
+ * via the AuditSystem from @revealui/security. Uses lazy import to
6
+ * avoid circular dependency issues at module load time.
7
+ */
8
+ /**
9
+ * Record a successful login to the audit trail.
10
+ *
11
+ * @param userId - The authenticated user's ID
12
+ * @param ip - Client IP address
13
+ * @param userAgent - Client User-Agent header
14
+ */
15
+ export declare function auditLoginSuccess(userId: string, ip: string, userAgent: string): Promise<void>;
16
+ /**
17
+ * Record a failed login attempt to the audit trail.
18
+ *
19
+ * @param email - The email used in the failed attempt
20
+ * @param ip - Client IP address
21
+ * @param userAgent - Client User-Agent header
22
+ * @param reason - Why the login failed (e.g. 'invalid_password', 'account_locked')
23
+ */
24
+ export declare function auditLoginFailure(email: string, ip: string, userAgent: string, reason: string): Promise<void>;
25
+ /**
26
+ * Record a password change to the audit trail.
27
+ *
28
+ * @param userId - The user who changed their password
29
+ * @param ip - Client IP address
30
+ */
31
+ export declare function auditPasswordChange(userId: string, ip: string): Promise<void>;
32
+ /**
33
+ * Record a password reset request to the audit trail.
34
+ *
35
+ * @param email - The email for which a reset was requested
36
+ * @param ip - Client IP address
37
+ */
38
+ export declare function auditPasswordReset(email: string, ip: string): Promise<void>;
39
+ /**
40
+ * Record MFA being enabled on an account to the audit trail.
41
+ *
42
+ * @param userId - The user who enabled MFA
43
+ * @param ip - Client IP address
44
+ */
45
+ export declare function auditMfaEnabled(userId: string, ip: string): Promise<void>;
46
+ /**
47
+ * Record MFA being disabled on an account to the audit trail.
48
+ *
49
+ * @param userId - The user who disabled MFA
50
+ * @param ip - Client IP address
51
+ */
52
+ export declare function auditMfaDisabled(userId: string, ip: string): Promise<void>;
53
+ /**
54
+ * Record a session revocation to the audit trail.
55
+ *
56
+ * @param userId - The user whose session was revoked
57
+ * @param sessionId - The revoked session's ID
58
+ * @param ip - Client IP address
59
+ */
60
+ export declare function auditSessionRevoked(userId: string, sessionId: string, ip: string): Promise<void>;
61
+ /**
62
+ * Record an account lockout to the audit trail.
63
+ *
64
+ * @param email - The locked account's email
65
+ * @param ip - Client IP address
66
+ * @param failedAttempts - Number of failed attempts that triggered the lockout
67
+ */
68
+ export declare function auditAccountLocked(email: string, ip: string, failedAttempts: number): Promise<void>;
69
+ //# sourceMappingURL=audit-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-bridge.d.ts","sourceRoot":"","sources":["../../src/server/audit-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgCH;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASnF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAShF;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Security Event Bridge — Connects auth events to the audit trail.
3
+ *
4
+ * Each function wraps an auth operation with structured audit logging
5
+ * via the AuditSystem from @revealui/security. Uses lazy import to
6
+ * avoid circular dependency issues at module load time.
7
+ */
8
+ /**
9
+ * Lazily resolve the global audit system from @revealui/security.
10
+ * Returns null if the module cannot be loaded (e.g. in test environments
11
+ * where @revealui/security is not available).
12
+ */
13
+ async function getAudit() {
14
+ try {
15
+ const { audit } = await import('@revealui/security');
16
+ return audit;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /**
23
+ * Internal helper — logs an audit event, silently skipping if the
24
+ * audit system is unavailable.
25
+ */
26
+ async function logAuditEvent(event) {
27
+ const auditSystem = await getAudit();
28
+ if (!auditSystem) {
29
+ return;
30
+ }
31
+ await auditSystem.log(event);
32
+ }
33
+ /**
34
+ * Record a successful login to the audit trail.
35
+ *
36
+ * @param userId - The authenticated user's ID
37
+ * @param ip - Client IP address
38
+ * @param userAgent - Client User-Agent header
39
+ */
40
+ export async function auditLoginSuccess(userId, ip, userAgent) {
41
+ await logAuditEvent({
42
+ type: 'auth.login',
43
+ severity: 'low',
44
+ actor: { id: userId, type: 'user', ip, userAgent },
45
+ action: 'login',
46
+ result: 'success',
47
+ message: 'User logged in successfully',
48
+ });
49
+ }
50
+ /**
51
+ * Record a failed login attempt to the audit trail.
52
+ *
53
+ * @param email - The email used in the failed attempt
54
+ * @param ip - Client IP address
55
+ * @param userAgent - Client User-Agent header
56
+ * @param reason - Why the login failed (e.g. 'invalid_password', 'account_locked')
57
+ */
58
+ export async function auditLoginFailure(email, ip, userAgent, reason) {
59
+ await logAuditEvent({
60
+ type: 'auth.failed_login',
61
+ severity: 'medium',
62
+ actor: { id: email, type: 'user', ip, userAgent },
63
+ action: 'login',
64
+ result: 'failure',
65
+ message: `Login failed: ${reason}`,
66
+ metadata: { email, reason },
67
+ });
68
+ }
69
+ /**
70
+ * Record a password change to the audit trail.
71
+ *
72
+ * @param userId - The user who changed their password
73
+ * @param ip - Client IP address
74
+ */
75
+ export async function auditPasswordChange(userId, ip) {
76
+ await logAuditEvent({
77
+ type: 'auth.password_change',
78
+ severity: 'medium',
79
+ actor: { id: userId, type: 'user', ip },
80
+ action: 'password_change',
81
+ result: 'success',
82
+ message: 'Password changed',
83
+ });
84
+ }
85
+ /**
86
+ * Record a password reset request to the audit trail.
87
+ *
88
+ * @param email - The email for which a reset was requested
89
+ * @param ip - Client IP address
90
+ */
91
+ export async function auditPasswordReset(email, ip) {
92
+ await logAuditEvent({
93
+ type: 'auth.password_reset',
94
+ severity: 'medium',
95
+ actor: { id: email, type: 'user', ip },
96
+ action: 'password_reset',
97
+ result: 'success',
98
+ message: 'Password reset requested',
99
+ metadata: { email },
100
+ });
101
+ }
102
+ /**
103
+ * Record MFA being enabled on an account to the audit trail.
104
+ *
105
+ * @param userId - The user who enabled MFA
106
+ * @param ip - Client IP address
107
+ */
108
+ export async function auditMfaEnabled(userId, ip) {
109
+ await logAuditEvent({
110
+ type: 'auth.mfa_enabled',
111
+ severity: 'medium',
112
+ actor: { id: userId, type: 'user', ip },
113
+ action: 'mfa_enabled',
114
+ result: 'success',
115
+ message: 'MFA enabled',
116
+ });
117
+ }
118
+ /**
119
+ * Record MFA being disabled on an account to the audit trail.
120
+ *
121
+ * @param userId - The user who disabled MFA
122
+ * @param ip - Client IP address
123
+ */
124
+ export async function auditMfaDisabled(userId, ip) {
125
+ await logAuditEvent({
126
+ type: 'auth.mfa_disabled',
127
+ severity: 'high',
128
+ actor: { id: userId, type: 'user', ip },
129
+ action: 'mfa_disabled',
130
+ result: 'success',
131
+ message: 'MFA disabled',
132
+ });
133
+ }
134
+ /**
135
+ * Record a session revocation to the audit trail.
136
+ *
137
+ * @param userId - The user whose session was revoked
138
+ * @param sessionId - The revoked session's ID
139
+ * @param ip - Client IP address
140
+ */
141
+ export async function auditSessionRevoked(userId, sessionId, ip) {
142
+ await logAuditEvent({
143
+ type: 'security.alert',
144
+ severity: 'medium',
145
+ actor: { id: userId, type: 'user', ip },
146
+ action: 'session_revoked',
147
+ result: 'success',
148
+ message: 'Session revoked',
149
+ metadata: { sessionId },
150
+ });
151
+ }
152
+ /**
153
+ * Record an account lockout to the audit trail.
154
+ *
155
+ * @param email - The locked account's email
156
+ * @param ip - Client IP address
157
+ * @param failedAttempts - Number of failed attempts that triggered the lockout
158
+ */
159
+ export async function auditAccountLocked(email, ip, failedAttempts) {
160
+ await logAuditEvent({
161
+ type: 'security.alert',
162
+ severity: 'high',
163
+ actor: { id: email, type: 'user', ip },
164
+ action: 'account_locked',
165
+ result: 'failure',
166
+ message: `Account locked after ${String(failedAttempts)} failed attempts`,
167
+ metadata: { email, failedAttempts },
168
+ });
169
+ }
@@ -5,6 +5,7 @@
5
5
  * Inspired by Better Auth and Neon Auth patterns.
6
6
  */
7
7
  export type { SignInResult, SignUpResult } from '../types.js';
8
+ export { auditAccountLocked, auditLoginFailure, auditLoginSuccess, auditMfaDisabled, auditMfaEnabled, auditPasswordChange, auditPasswordReset, auditSessionRevoked, } from './audit-bridge.js';
8
9
  export { isSignupAllowed, signIn, signUp } from './auth.js';
9
10
  export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
10
11
  export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.js';
@@ -12,6 +13,8 @@ export type { MagicLinkConfig } from './magic-link.js';
12
13
  export { configureMagicLink, createMagicLink, resetMagicLinkConfig, verifyMagicLink, } from './magic-link.js';
13
14
  export type { MFAConfig, MFADisableProof, MFASetupResult } from './mfa.js';
14
15
  export { configureMFA, disableMFA, initiateMFASetup, isMFAEnabled, regenerateBackupCodes, resetMFAConfig, verifyBackupCode, verifyMFACode, verifyMFASetup, } from './mfa.js';
16
+ export type { MfaCheckResult, MfaEnforcementOptions, MfaErrorResponse, MfaRequest, MfaSession, MfaSessionUser, } from './mfa-enforcement.js';
17
+ export { requireMfa } from './mfa-enforcement.js';
15
18
  export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getLinkedProviders, linkOAuthAccount, type ProviderUser, type UpsertOAuthOptions, unlinkOAuthAccount, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
16
19
  export type { PasskeyConfig } from './passkey.js';
17
20
  export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenticationChallenge, generateRegistrationChallenge, listPasskeys, renamePasskey, resetPasskeyConfig, storePasskey, verifyAuthentication, verifyRegistration, } from './passkey.js';
@@ -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,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,+BAA+B,EAC/B,6BAA6B,EAC7B,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
@@ -4,11 +4,14 @@
4
4
  * Server-side authentication functions for Next.js and TanStack Start.
5
5
  * Inspired by Better Auth and Neon Auth patterns.
6
6
  */
7
+ // Audit bridge
8
+ export { auditAccountLocked, auditLoginFailure, auditLoginSuccess, auditMfaDisabled, auditMfaEnabled, auditPasswordChange, auditPasswordReset, auditSessionRevoked, } from './audit-bridge.js';
7
9
  export { isSignupAllowed, signIn, signUp } from './auth.js';
8
10
  export { clearFailedAttempts, configureBruteForce, getFailedAttemptCount, isAccountLocked, recordFailedAttempt, resetBruteForceConfig, } from './brute-force.js';
9
11
  export { AuthError, AuthenticationError, DatabaseError, OAuthAccountConflictError, SessionError, TokenError, } from './errors.js';
10
12
  export { configureMagicLink, createMagicLink, resetMagicLinkConfig, verifyMagicLink, } from './magic-link.js';
11
13
  export { configureMFA, disableMFA, initiateMFASetup, isMFAEnabled, regenerateBackupCodes, resetMFAConfig, verifyBackupCode, verifyMFACode, verifyMFASetup, } from './mfa.js';
14
+ export { requireMfa } from './mfa-enforcement.js';
12
15
  export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getLinkedProviders, linkOAuthAccount, unlinkOAuthAccount, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
13
16
  export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenticationChallenge, generateRegistrationChallenge, listPasskeys, renamePasskey, resetPasskeyConfig, storePasskey, verifyAuthentication, verifyRegistration, } from './passkey.js';
14
17
  export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * MFA Enforcement Middleware
3
+ *
4
+ * Provides a middleware factory that enforces Multi-Factor Authentication
5
+ * for specific roles and sensitive operations. SOC2 6.2 compliant.
6
+ */
7
+ /** Options for the MFA enforcement middleware. */
8
+ export interface MfaEnforcementOptions {
9
+ /** Roles that require MFA to be enabled (default: ['admin']). */
10
+ roles?: string[];
11
+ /** Operations that require MFA (e.g. 'delete_user', 'change_role'). */
12
+ operations?: string[];
13
+ }
14
+ /** Shape of a session user as expected by the middleware. */
15
+ export interface MfaSessionUser {
16
+ /** User ID. */
17
+ id: string;
18
+ /** User's role. */
19
+ role: string;
20
+ /** Whether MFA is enabled on the account. */
21
+ mfaEnabled: boolean;
22
+ /** Whether MFA was verified in this session. */
23
+ mfaVerified?: boolean;
24
+ }
25
+ /** Shape of the session object the middleware reads from. */
26
+ export interface MfaSession {
27
+ user: MfaSessionUser;
28
+ }
29
+ /** Request shape expected by the middleware. */
30
+ export interface MfaRequest {
31
+ /** The current session, if any. */
32
+ session?: MfaSession | null;
33
+ /** The operation being performed (matched against `options.operations`). */
34
+ operation?: string;
35
+ }
36
+ /** Standard JSON error body returned when MFA is not satisfied. */
37
+ export interface MfaErrorResponse {
38
+ error: string;
39
+ code: 'MFA_REQUIRED' | 'MFA_VERIFY_REQUIRED';
40
+ }
41
+ /** Result of the MFA enforcement check. */
42
+ export interface MfaCheckResult {
43
+ /** Whether the request may proceed. */
44
+ allowed: boolean;
45
+ /** HTTP status code (403 when blocked, undefined when allowed). */
46
+ status?: number;
47
+ /** Error body (present only when blocked). */
48
+ body?: MfaErrorResponse;
49
+ }
50
+ /**
51
+ * Create an MFA enforcement checker.
52
+ *
53
+ * Returns a function that inspects a request's session and determines
54
+ * whether MFA requirements are satisfied. Consumers are responsible
55
+ * for translating the result into their framework's response format
56
+ * (Hono, Express, Next.js, etc.).
57
+ *
58
+ * @param options - Roles and operations that require MFA
59
+ * @returns A check function that evaluates MFA requirements
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const checkMfa = requireMfa({ roles: ['admin'], operations: ['delete_user'] });
64
+ *
65
+ * // In a Hono route:
66
+ * app.delete('/users/:id', async (c) => {
67
+ * const result = checkMfa({ session: c.get('session'), operation: 'delete_user' });
68
+ * if (!result.allowed) {
69
+ * return c.json(result.body, result.status);
70
+ * }
71
+ * // proceed...
72
+ * });
73
+ * ```
74
+ */
75
+ export declare function requireMfa(options?: MfaEnforcementOptions): (request: MfaRequest) => MfaCheckResult;
76
+ //# sourceMappingURL=mfa-enforcement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-enforcement.d.ts","sourceRoot":"","sources":["../../src/server/mfa-enforcement.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,kDAAkD;AAClD,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,eAAe;IACf,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,UAAU,EAAE,OAAO,CAAC;IACpB,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;CACtB;AAED,gDAAgD;AAChD,MAAM,WAAW,UAAU;IACzB,mCAAmC;IACnC,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,GAAG,qBAAqB,CAAC;CAC9C;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAQD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,UAAU,CACxB,OAAO,GAAE,qBAA0B,GAClC,CAAC,OAAO,EAAE,UAAU,KAAK,cAAc,CAgDzC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * MFA Enforcement Middleware
3
+ *
4
+ * Provides a middleware factory that enforces Multi-Factor Authentication
5
+ * for specific roles and sensitive operations. SOC2 6.2 compliant.
6
+ */
7
+ // =============================================================================
8
+ // Middleware
9
+ // =============================================================================
10
+ const DEFAULT_ROLES = ['admin'];
11
+ /**
12
+ * Create an MFA enforcement checker.
13
+ *
14
+ * Returns a function that inspects a request's session and determines
15
+ * whether MFA requirements are satisfied. Consumers are responsible
16
+ * for translating the result into their framework's response format
17
+ * (Hono, Express, Next.js, etc.).
18
+ *
19
+ * @param options - Roles and operations that require MFA
20
+ * @returns A check function that evaluates MFA requirements
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const checkMfa = requireMfa({ roles: ['admin'], operations: ['delete_user'] });
25
+ *
26
+ * // In a Hono route:
27
+ * app.delete('/users/:id', async (c) => {
28
+ * const result = checkMfa({ session: c.get('session'), operation: 'delete_user' });
29
+ * if (!result.allowed) {
30
+ * return c.json(result.body, result.status);
31
+ * }
32
+ * // proceed...
33
+ * });
34
+ * ```
35
+ */
36
+ export function requireMfa(options = {}) {
37
+ const requiredRoles = options.roles ?? DEFAULT_ROLES;
38
+ const requiredOperations = options.operations ?? [];
39
+ return (request) => {
40
+ const session = request.session;
41
+ if (!session) {
42
+ // No session — nothing to enforce (auth middleware handles this)
43
+ return { allowed: true };
44
+ }
45
+ const user = session.user;
46
+ // Determine if MFA is required for this request
47
+ const roleRequiresMfa = requiredRoles.includes(user.role);
48
+ const operationRequiresMfa = request.operation !== undefined && requiredOperations.includes(request.operation);
49
+ if (!(roleRequiresMfa || operationRequiresMfa)) {
50
+ return { allowed: true };
51
+ }
52
+ // MFA is required but not enabled on the account
53
+ if (!user.mfaEnabled) {
54
+ return {
55
+ allowed: false,
56
+ status: 403,
57
+ body: {
58
+ error: 'MFA required',
59
+ code: 'MFA_REQUIRED',
60
+ },
61
+ };
62
+ }
63
+ // MFA is enabled but not verified in this session
64
+ if (!user.mfaVerified) {
65
+ return {
66
+ allowed: false,
67
+ status: 403,
68
+ body: {
69
+ error: 'MFA verification required',
70
+ code: 'MFA_VERIFY_REQUIRED',
71
+ },
72
+ };
73
+ }
74
+ return { allowed: true };
75
+ };
76
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"password-validation.d.ts","sourceRoot":"","sources":["../../src/server/password-validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CAgCnF;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1E"}
1
+ {"version":3,"file":"password-validation.d.ts","sourceRoot":"","sources":["../../src/server/password-validation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CA2BnF;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1E"}
@@ -3,6 +3,15 @@
3
3
  *
4
4
  * Password strength validation and requirements.
5
5
  */
6
+ /** Check if any character in the string falls within the given char code range (inclusive) */
7
+ function hasCharInRange(str, low, high) {
8
+ for (let i = 0; i < str.length; i++) {
9
+ const code = str.charCodeAt(i);
10
+ if (code >= low && code <= high)
11
+ return true;
12
+ }
13
+ return false;
14
+ }
6
15
  /**
7
16
  * Validates password strength
8
17
  *
@@ -17,19 +26,15 @@ export function validatePasswordStrength(password) {
17
26
  if (password.length > 128) {
18
27
  errors.push('Password must be less than 128 characters');
19
28
  }
20
- if (!/[a-z]/.test(password)) {
29
+ if (!hasCharInRange(password, 97, 122)) {
21
30
  errors.push('Password must contain at least one lowercase letter');
22
31
  }
23
- if (!/[A-Z]/.test(password)) {
32
+ if (!hasCharInRange(password, 65, 90)) {
24
33
  errors.push('Password must contain at least one uppercase letter');
25
34
  }
26
- if (!/[0-9]/.test(password)) {
35
+ if (!hasCharInRange(password, 48, 57)) {
27
36
  errors.push('Password must contain at least one number');
28
37
  }
29
- // Optional: special characters (not too strict)
30
- // if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
31
- // errors.push('Password must contain at least one special character')
32
- // }
33
38
  return {
34
39
  valid: errors.length === 0,
35
40
  errors,
@@ -1 +1 @@
1
- {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/server/providers/vercel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiC1E"}
1
+ {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/server/providers/vercel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiC1E"}
@@ -34,6 +34,12 @@ export async function exchangeCode(code, redirectUri) {
34
34
  throw new Error(`Vercel token exchange failed: ${response.status}${detail ? ` — ${detail}` : ''}`);
35
35
  }
36
36
  const data = (await response.json());
37
+ if (data.error) {
38
+ throw new Error(`Vercel token exchange error: ${data.error}`);
39
+ }
40
+ if (!data.access_token || typeof data.access_token !== 'string') {
41
+ throw new Error('Vercel token exchange returned no access_token');
42
+ }
37
43
  return data.access_token;
38
44
  }
39
45
  export async function fetchUser(accessToken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/auth",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Authentication system for RevealUI - database-backed sessions with Better Auth patterns",
5
5
  "keywords": [
6
6
  "auth",
@@ -14,10 +14,11 @@
14
14
  "bcryptjs": "^3.0.3",
15
15
  "drizzle-orm": "^0.45.2",
16
16
  "zod": "^4.3.6",
17
- "@revealui/core": "0.5.2",
18
- "@revealui/db": "0.3.3",
19
- "@revealui/contracts": "1.3.3",
20
- "@revealui/config": "0.3.0"
17
+ "@revealui/config": "0.3.2",
18
+ "@revealui/contracts": "1.3.5",
19
+ "@revealui/core": "0.5.4",
20
+ "@revealui/db": "0.3.5",
21
+ "@revealui/security": "0.2.5"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@simplewebauthn/browser": "^13.3.0",