@revealui/auth 0.3.2 → 0.3.5

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)
@@ -40,6 +40,9 @@ export declare function verifyMFASetup(userId: string, code: string): Promise<{
40
40
  }>;
41
41
  /**
42
42
  * Verify a TOTP code during login (step 2 of MFA login flow).
43
+ * Includes replay prevention (B-03): rejects codes whose time counter
44
+ * has already been used, preventing an attacker who intercepts a code
45
+ * from replaying it within the same 30-second window.
43
46
  */
44
47
  export declare function verifyMFACode(userId: string, code: string): Promise<{
45
48
  success: boolean;
@@ -1 +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"}
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;AA2ED,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;;;;;GAKG;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,CA+B/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,CAgD/C;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUnE"}
@@ -4,7 +4,7 @@
4
4
  * Uses the timing-safe TOTP implementation from @revealui/core/security/auth.
5
5
  * Backup codes are bcrypt-hashed for storage (one-time use, consumed on verify).
6
6
  */
7
- import { randomBytes } from 'node:crypto';
7
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
8
8
  import { TwoFactorAuth } from '@revealui/core/security';
9
9
  import { getClient } from '@revealui/db/client';
10
10
  import { users } from '@revealui/db/schema';
@@ -23,6 +23,33 @@ export function resetMFAConfig() {
23
23
  config = { ...DEFAULT_MFA_CONFIG };
24
24
  }
25
25
  // =============================================================================
26
+ // TOTP Replay Prevention (B-03)
27
+ // =============================================================================
28
+ /**
29
+ * Compute the TOTP time counter for a given timestamp.
30
+ * Counter = floor(unixMs / 30000). Each counter value represents one 30-second window.
31
+ */
32
+ function totpCounter(timestampMs = Date.now()) {
33
+ return Math.floor(timestampMs / 30000);
34
+ }
35
+ /**
36
+ * Verify a TOTP code and return the matched time counter, or null if invalid.
37
+ * This replicates the window logic from TwoFactorAuth.verifyCode so we know
38
+ * which counter matched (needed for replay prevention).
39
+ */
40
+ function verifyCodeWithCounter(secret, code, window = 1) {
41
+ const now = Date.now();
42
+ for (let i = -window; i <= window; i++) {
43
+ const testTime = now + i * 30000;
44
+ const testCode = TwoFactorAuth.generateCode(secret, testTime);
45
+ if (testCode.length === code.length &&
46
+ timingSafeEqual(Buffer.from(testCode), Buffer.from(code))) {
47
+ return totpCounter(testTime);
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ // =============================================================================
26
53
  // Backup Code Generation
27
54
  // =============================================================================
28
55
  /**
@@ -127,21 +154,34 @@ export async function verifyMFASetup(userId, code) {
127
154
  }
128
155
  /**
129
156
  * Verify a TOTP code during login (step 2 of MFA login flow).
157
+ * Includes replay prevention (B-03): rejects codes whose time counter
158
+ * has already been used, preventing an attacker who intercepts a code
159
+ * from replaying it within the same 30-second window.
130
160
  */
131
161
  export async function verifyMFACode(userId, code) {
132
162
  const db = getClient();
133
163
  const [user] = await db
134
- .select({ mfaSecret: users.mfaSecret, mfaEnabled: users.mfaEnabled })
164
+ .select({
165
+ mfaSecret: users.mfaSecret,
166
+ mfaEnabled: users.mfaEnabled,
167
+ mfaLastUsedCounter: users.mfaLastUsedCounter,
168
+ })
135
169
  .from(users)
136
170
  .where(eq(users.id, userId))
137
171
  .limit(1);
138
172
  if (!(user?.mfaEnabled && user.mfaSecret)) {
139
173
  return { success: false, error: 'MFA not enabled' };
140
174
  }
141
- const valid = TwoFactorAuth.verifyCode(user.mfaSecret, code);
142
- if (!valid) {
175
+ const matchedCounter = verifyCodeWithCounter(user.mfaSecret, code);
176
+ if (matchedCounter === null) {
177
+ return { success: false, error: 'Invalid code' };
178
+ }
179
+ // Replay prevention: reject if this counter was already used
180
+ if (user.mfaLastUsedCounter !== null && matchedCounter <= user.mfaLastUsedCounter) {
143
181
  return { success: false, error: 'Invalid code' };
144
182
  }
183
+ // Record the counter to prevent replay
184
+ await db.update(users).set({ mfaLastUsedCounter: matchedCounter }).where(eq(users.id, userId));
145
185
  return { success: true };
146
186
  }
147
187
  /**
@@ -244,6 +284,7 @@ export async function disableMFA(userId, proof) {
244
284
  mfaSecret: null,
245
285
  mfaBackupCodes: null,
246
286
  mfaVerifiedAt: null,
287
+ mfaLastUsedCounter: null,
247
288
  updatedAt: new Date(),
248
289
  })
249
290
  .where(eq(users.id, userId));
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"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"}
@@ -15,17 +15,27 @@ import { and, count, eq } from 'drizzle-orm';
15
15
  const DEFAULT_CONFIG = {
16
16
  maxPasskeysPerUser: 10,
17
17
  challengeTtlMs: 5 * 60 * 1000,
18
- rpId: 'localhost',
19
- rpName: 'RevealUI',
20
- origin: 'http://localhost:4000',
18
+ rpId: process.env.PASSKEY_RP_ID || 'localhost',
19
+ rpName: process.env.PASSKEY_RP_NAME || 'RevealUI',
20
+ origin: process.env.PASSKEY_ORIGIN || 'http://localhost:4000',
21
21
  };
22
22
  let config = { ...DEFAULT_CONFIG };
23
+ let _productionChecked = false;
23
24
  export function configurePasskey(overrides) {
24
25
  config = { ...DEFAULT_CONFIG, ...overrides };
26
+ _productionChecked = false;
25
27
  }
26
28
  export function resetPasskeyConfig() {
27
29
  config = { ...DEFAULT_CONFIG };
28
30
  }
31
+ function assertProductionConfig() {
32
+ if (_productionChecked)
33
+ return;
34
+ _productionChecked = true;
35
+ if (process.env.NODE_ENV === 'production' && config.rpId === 'localhost') {
36
+ throw new Error('Passkey rpId is "localhost" in production. Set PASSKEY_RP_ID or call configurePasskey() at startup.');
37
+ }
38
+ }
29
39
  // =============================================================================
30
40
  // Registration
31
41
  // =============================================================================
@@ -41,6 +51,7 @@ export function resetPasskeyConfig() {
41
51
  * @param existingCredentialIds - Credential IDs to exclude (prevent re-registration)
42
52
  */
43
53
  export async function generateRegistrationChallenge(userId, userEmail, existingCredentialIds) {
54
+ assertProductionConfig();
44
55
  const excludeCredentials = existingCredentialIds?.map((id) => ({
45
56
  id,
46
57
  }));
@@ -139,6 +150,7 @@ export async function storePasskey(userId, credential, deviceName) {
139
150
  * @param allowCredentials - Optional list of credential IDs to allow
140
151
  */
141
152
  export async function generateAuthenticationChallenge(allowCredentials) {
153
+ assertProductionConfig();
142
154
  const options = await generateAuthenticationOptions({
143
155
  rpID: config.rpId,
144
156
  allowCredentials: allowCredentials?.map((cred) => ({
@@ -50,14 +50,16 @@ export interface ChangePasswordResult {
50
50
  /**
51
51
  * Change password for an authenticated user.
52
52
  *
53
- * Verifies the current password before updating. Does not touch sessions —
54
- * the caller is responsible for revoking other sessions if desired.
53
+ * Verifies the current password before updating. After a successful change,
54
+ * invalidates all other sessions for the user (keeping the current session
55
+ * active) to ensure any compromised sessions cannot persist.
55
56
  *
56
57
  * @param userId - Authenticated user ID
57
58
  * @param currentPassword - Plain-text current password to verify
58
59
  * @param newPassword - Plain-text new password to hash and store
60
+ * @param currentSessionId - Session ID to preserve (all others will be deleted)
59
61
  */
60
- export declare function changePassword(userId: string, currentPassword: string, newPassword: string): Promise<ChangePasswordResult>;
62
+ export declare function changePassword(userId: string, currentPassword: string, newPassword: string, currentSessionId?: string): Promise<ChangePasswordResult>;
61
63
  /**
62
64
  * Invalidates a password reset token
63
65
  *
@@ -1 +1 @@
1
- {"version":3,"file":"password-reset.d.ts","sourceRoot":"","sources":["../../src/server/password-reset.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAuBD;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwE5F;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqCxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CA4E9B;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAqC/B;AAED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChG"}
1
+ {"version":3,"file":"password-reset.d.ts","sourceRoot":"","sources":["../../src/server/password-reset.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAuBD;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwE5F;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqCxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CA4E9B;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAiD/B;AAED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChG"}
@@ -9,7 +9,7 @@ import { logger } from '@revealui/core/observability/logger';
9
9
  import { getClient } from '@revealui/db/client';
10
10
  import { passwordResetTokens, sessions, users } from '@revealui/db/schema';
11
11
  import bcrypt from 'bcryptjs';
12
- import { and, eq, gt, isNull } from 'drizzle-orm';
12
+ import { and, eq, gt, isNull, ne } from 'drizzle-orm';
13
13
  const TOKEN_EXPIRY_MS = 15 * 60 * 1000; // 15 minutes
14
14
  /**
15
15
  * Hash a token using HMAC-SHA256 with a per-token salt.
@@ -201,14 +201,16 @@ export async function resetPasswordWithToken(tokenId, token, newPassword) {
201
201
  /**
202
202
  * Change password for an authenticated user.
203
203
  *
204
- * Verifies the current password before updating. Does not touch sessions —
205
- * the caller is responsible for revoking other sessions if desired.
204
+ * Verifies the current password before updating. After a successful change,
205
+ * invalidates all other sessions for the user (keeping the current session
206
+ * active) to ensure any compromised sessions cannot persist.
206
207
  *
207
208
  * @param userId - Authenticated user ID
208
209
  * @param currentPassword - Plain-text current password to verify
209
210
  * @param newPassword - Plain-text new password to hash and store
211
+ * @param currentSessionId - Session ID to preserve (all others will be deleted)
210
212
  */
211
- export async function changePassword(userId, currentPassword, newPassword) {
213
+ export async function changePassword(userId, currentPassword, newPassword, currentSessionId) {
212
214
  try {
213
215
  const db = getClient();
214
216
  const [user] = await db
@@ -231,6 +233,18 @@ export async function changePassword(userId, currentPassword, newPassword) {
231
233
  }
232
234
  const newHash = await bcrypt.hash(newPassword, 12);
233
235
  await db.update(users).set({ password: newHash }).where(eq(users.id, userId));
236
+ // Invalidate all other sessions so stolen/compromised sessions cannot persist.
237
+ // This mirrors resetPasswordWithToken which deletes ALL sessions. Here we keep
238
+ // the current session so the user stays logged in after changing their password.
239
+ if (currentSessionId) {
240
+ await db
241
+ .delete(sessions)
242
+ .where(and(eq(sessions.userId, userId), ne(sessions.id, currentSessionId)));
243
+ }
244
+ else {
245
+ // No current session ID provided — delete all sessions as a safe default
246
+ await db.delete(sessions).where(eq(sessions.userId, userId));
247
+ }
234
248
  return { success: true };
235
249
  }
236
250
  catch (error) {
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/auth",
3
- "version": "0.3.2",
3
+ "version": "0.3.5",
4
4
  "description": "Authentication system for RevealUI - database-backed sessions with Better Auth patterns",
5
5
  "keywords": [
6
6
  "auth",
@@ -14,10 +14,10 @@
14
14
  "bcryptjs": "^3.0.3",
15
15
  "drizzle-orm": "^0.45.2",
16
16
  "zod": "^4.3.6",
17
- "@revealui/contracts": "1.3.1",
18
- "@revealui/config": "0.3.0",
19
- "@revealui/db": "0.3.1",
20
- "@revealui/core": "0.5.0"
17
+ "@revealui/config": "0.3.1",
18
+ "@revealui/contracts": "1.3.4",
19
+ "@revealui/core": "0.5.3",
20
+ "@revealui/db": "0.3.4"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@simplewebauthn/browser": "^13.3.0",