@revealui/auth 0.3.7 → 0.3.8

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 (37) hide show
  1. package/README.md +14 -14
  2. package/dist/react/useMFA.d.ts +1 -1
  3. package/dist/react/useMFA.d.ts.map +1 -1
  4. package/dist/server/audit-bridge.d.ts +1 -1
  5. package/dist/server/audit-bridge.js +2 -2
  6. package/dist/server/auth.d.ts.map +1 -1
  7. package/dist/server/auth.js +10 -1
  8. package/dist/server/mfa-enforcement.js +1 -1
  9. package/dist/server/mfa.d.ts +1 -1
  10. package/dist/server/mfa.js +2 -2
  11. package/dist/server/oauth.d.ts +6 -4
  12. package/dist/server/oauth.d.ts.map +1 -1
  13. package/dist/server/oauth.js +18 -11
  14. package/dist/server/passkey.d.ts +3 -3
  15. package/dist/server/passkey.d.ts.map +1 -1
  16. package/dist/server/passkey.js +1 -1
  17. package/dist/server/password-reset.d.ts.map +1 -1
  18. package/dist/server/password-reset.js +11 -3
  19. package/dist/server/password-validation.d.ts +11 -0
  20. package/dist/server/password-validation.d.ts.map +1 -1
  21. package/dist/server/password-validation.js +37 -0
  22. package/dist/server/providers/github.d.ts +3 -3
  23. package/dist/server/providers/github.d.ts.map +1 -1
  24. package/dist/server/providers/github.js +20 -12
  25. package/dist/server/providers/google.d.ts +3 -3
  26. package/dist/server/providers/google.d.ts.map +1 -1
  27. package/dist/server/providers/google.js +21 -13
  28. package/dist/server/providers/vercel.d.ts +4 -4
  29. package/dist/server/providers/vercel.d.ts.map +1 -1
  30. package/dist/server/providers/vercel.js +7 -7
  31. package/dist/server/session.d.ts +2 -2
  32. package/dist/server/session.d.ts.map +1 -1
  33. package/dist/server/session.js +2 -2
  34. package/dist/server/signed-cookie.d.ts.map +1 -1
  35. package/dist/server/signed-cookie.js +4 -2
  36. package/dist/server/storage/index.js +2 -2
  37. package/package.json +17 -12
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # @revealui/auth
2
2
 
3
- Session-based authentication for RevealUIdatabase-backed sessions, rate limiting, brute force protection, and password reset.
3
+ Session-based authentication for RevealUI - database-backed sessions, rate limiting, brute force protection, and password reset.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Database Sessions**PostgreSQL/NeonDB-backed sessions with SHA-256 token hashing
8
- - **Secure Cookies**HTTP-only, SameSite, secure flag, cross-subdomain support
9
- - **Rate Limiting**Configurable per-endpoint rate limits stored in database
10
- - **Brute Force Protection**Progressive lockout on failed sign-in attempts
11
- - **Password Reset**Token-based password reset flow with email integration
12
- - **Password Validation**Strength requirements and common password checks
13
- - **React Hooks**Client-side session management (`useSession`, `useSignIn`, `useSignOut`)
14
- - **Framework Agnostic**Works with Next.js, Hono, and other Node.js frameworks
7
+ - **Database Sessions** - PostgreSQL/NeonDB-backed sessions with SHA-256 token hashing
8
+ - **Secure Cookies** - HTTP-only, SameSite, secure flag, cross-subdomain support
9
+ - **Rate Limiting** - Configurable per-endpoint rate limits stored in database
10
+ - **Brute Force Protection** - Progressive lockout on failed sign-in attempts
11
+ - **Password Reset** - Token-based password reset flow with email integration
12
+ - **Password Validation** - Strength requirements and common password checks
13
+ - **React Hooks** - Client-side session management (`useSession`, `useSignIn`, `useSignOut`)
14
+ - **Framework Agnostic** - Works with Next.js, Hono, and other Node.js frameworks
15
15
 
16
16
  ## Installation
17
17
 
@@ -95,8 +95,8 @@ pnpm test
95
95
  - You need session-based auth with database-backed sessions for a RevealUI app
96
96
  - You want built-in brute force protection and rate limiting without external services
97
97
  - You need React hooks for client-side session management (`useSession`, `useSignIn`, `useSignOut`)
98
- - **Not** for OAuth-only flowsuse a dedicated OAuth provider and wire tokens through this package
99
- - **Not** for stateless JWT auththis package uses database sessions by design
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
100
 
101
101
  ## JOSHUA Alignment
102
102
 
@@ -106,9 +106,9 @@ pnpm test
106
106
 
107
107
  ## Related
108
108
 
109
- - [Core Package](../core/README.md)Runtime engine (uses auth for access control)
110
- - [DB Package](../db/README.md)Database schema (sessions, users, rate_limits tables)
111
- - [Auth Guide](../../docs/AUTH.md)Architecture, usage patterns, and security design
109
+ - [Core Package](../core/README.md) - Runtime engine (uses auth for access control)
110
+ - [DB Package](../db/README.md) - Database schema (sessions, users, rate_limits tables)
111
+ - [Auth Guide](../../docs/AUTH.md) - Architecture, usage patterns, and security design
112
112
 
113
113
  ## License
114
114
 
@@ -15,7 +15,7 @@ export interface MFASetupData {
15
15
  backupCodes: string[];
16
16
  }
17
17
  export interface UseMFASetupResult {
18
- /** Initiate MFA setupreturns secret, QR URI, and backup codes */
18
+ /** Initiate MFA setup - returns secret, QR URI, and backup codes */
19
19
  setup: () => Promise<MFASetupData | null>;
20
20
  /** Verify a TOTP code to confirm setup */
21
21
  verifySetup: (code: string) => Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"useMFA.d.ts","sourceRoot":"","sources":["../../src/react/useMFA.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,KAAK,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC1C,0CAA0C;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAqE/C;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,wCAAwC;IACxC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,IAAI,kBAAkB,CAsEjD"}
1
+ {"version":3,"file":"useMFA.d.ts","sourceRoot":"","sources":["../../src/react/useMFA.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,KAAK,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC1C,0CAA0C;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAqE/C;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,wCAAwC;IACxC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,IAAI,kBAAkB,CAsEjD"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Security Event BridgeConnects auth events to the audit trail.
2
+ * Security Event Bridge - Connects auth events to the audit trail.
3
3
  *
4
4
  * Each function wraps an auth operation with structured audit logging
5
5
  * via the AuditSystem from @revealui/security. Uses lazy import to
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Security Event BridgeConnects auth events to the audit trail.
2
+ * Security Event Bridge - Connects auth events to the audit trail.
3
3
  *
4
4
  * Each function wraps an auth operation with structured audit logging
5
5
  * via the AuditSystem from @revealui/security. Uses lazy import to
@@ -20,7 +20,7 @@ async function getAudit() {
20
20
  }
21
21
  }
22
22
  /**
23
- * Internal helperlogs an audit event, silently skipping if the
23
+ * Internal helper - logs an audit event, silently skipping if the
24
24
  * audit system is unavailable.
25
25
  */
26
26
  async function logAuditEvent(event) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AASpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CAuKvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAsLvB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AASpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CAuKvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAgMvB"}
@@ -137,7 +137,7 @@ export async function signIn(email, password, options) {
137
137
  };
138
138
  }
139
139
  }
140
- // Check if MFA is enabledif so, return early and require TOTP verification
140
+ // Check if MFA is enabled - if so, return early and require TOTP verification
141
141
  if (user.mfaEnabled) {
142
142
  return {
143
143
  success: true,
@@ -240,6 +240,15 @@ export async function signUp(email, password, name, options) {
240
240
  error: passwordValidation.errors.join('. '),
241
241
  };
242
242
  }
243
+ // Check password against known data breaches (non-blocking on failure)
244
+ const { checkPasswordBreach } = await import('./password-validation.js');
245
+ const breachCount = await checkPasswordBreach(password);
246
+ if (breachCount > 0) {
247
+ return {
248
+ success: false,
249
+ error: `This password has appeared in ${breachCount.toLocaleString()} data breaches. Please choose a different password.`,
250
+ };
251
+ }
243
252
  let db;
244
253
  try {
245
254
  db = getClient();
@@ -39,7 +39,7 @@ export function requireMfa(options = {}) {
39
39
  return (request) => {
40
40
  const session = request.session;
41
41
  if (!session) {
42
- // No sessionnothing to enforce (auth middleware handles this)
42
+ // No session - nothing to enforce (auth middleware handles this)
43
43
  return { allowed: true };
44
44
  }
45
45
  const user = session.user;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MFA/2FATOTP-based Multi-Factor Authentication
2
+ * MFA/2FA - TOTP-based Multi-Factor Authentication
3
3
  *
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).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MFA/2FATOTP-based Multi-Factor Authentication
2
+ * MFA/2FA - TOTP-based Multi-Factor Authentication
3
3
  *
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).
@@ -274,7 +274,7 @@ export async function disableMFA(userId, proof) {
274
274
  return { success: false, error: 'Invalid password' };
275
275
  }
276
276
  }
277
- // For passkey proof, the API route has already performed the WebAuthn assertion
277
+ // For passkey proof, the API route has already performed the WebAuthn assertion -
278
278
  // the `verified: true` flag is trusted as a server-side signal.
279
279
  // Clear all MFA data
280
280
  await db
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OAuth CoreState Management + User Upsert
2
+ * OAuth Core - State Management + User Upsert
3
3
  *
4
4
  * CSRF state: signed cookie using HMAC-SHA256 over a base64url payload.
5
5
  * Provider dispatch: routes to Google / GitHub / Vercel provider modules.
@@ -16,7 +16,7 @@ export interface ProviderUser {
16
16
  * Generate a signed OAuth state token.
17
17
  *
18
18
  * State encodes provider + redirectTo + nonce as base64url JSON.
19
- * Cookie value is `<state>.<hmac>`the HMAC is over the state string
19
+ * Cookie value is `<state>.<hmac>` - the HMAC is over the state string
20
20
  * using REVEALUI_SECRET, providing CSRF protection without a DB table.
21
21
  */
22
22
  export declare function generateOAuthState(provider: string, redirectTo: string, options?: {
@@ -24,6 +24,7 @@ export declare function generateOAuthState(provider: string, redirectTo: string,
24
24
  }): {
25
25
  state: string;
26
26
  cookieValue: string;
27
+ codeChallenge: string;
27
28
  };
28
29
  /**
29
30
  * Verify a signed OAuth state token from the callback.
@@ -34,9 +35,10 @@ export declare function verifyOAuthState(state: string | null | undefined, cooki
34
35
  provider: string;
35
36
  redirectTo: string;
36
37
  linkConsent?: boolean;
38
+ codeVerifier?: string;
37
39
  } | null;
38
- export declare function buildAuthUrl(provider: string, redirectUri: string, state: string): string;
39
- export declare function exchangeCode(provider: string, code: string, redirectUri: string): Promise<string>;
40
+ export declare function buildAuthUrl(provider: string, redirectUri: string, state: string, codeChallenge?: string): string;
41
+ export declare function exchangeCode(provider: string, code: string, redirectUri: string, codeVerifier?: string): Promise<string>;
40
42
  export declare function fetchProviderUser(provider: string, accessToken: string): Promise<ProviderUser>;
41
43
  export interface UpsertOAuthOptions {
42
44
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/server/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAMxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAkBxC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAqDxE;AAwBD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CASzF;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAQvB;AAMD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmGf;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,IAAI,CAAC,CAiEf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCxF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC,CAajG"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/server/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAMxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAuB/D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwD/F;AAwBD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CASR;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAQvB;AAMD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmGf;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,IAAI,CAAC,CAiEf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCxF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC,CAajG"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OAuth CoreState Management + User Upsert
2
+ * OAuth Core - State Management + User Upsert
3
3
  *
4
4
  * CSRF state: signed cookie using HMAC-SHA256 over a base64url payload.
5
5
  * Provider dispatch: routes to Google / GitHub / Vercel provider modules.
@@ -21,15 +21,19 @@ import * as vercel from './providers/vercel.js';
21
21
  * Generate a signed OAuth state token.
22
22
  *
23
23
  * State encodes provider + redirectTo + nonce as base64url JSON.
24
- * Cookie value is `<state>.<hmac>`the HMAC is over the state string
24
+ * Cookie value is `<state>.<hmac>` - the HMAC is over the state string
25
25
  * using REVEALUI_SECRET, providing CSRF protection without a DB table.
26
26
  */
27
27
  export function generateOAuthState(provider, redirectTo, options) {
28
28
  const nonce = crypto.randomBytes(16).toString('hex');
29
+ // PKCE: generate a code_verifier (RFC 7636 §4.1 - 32 random bytes → 43 base64url chars)
30
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
31
+ const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
29
32
  const payload = JSON.stringify({
30
33
  provider,
31
34
  redirectTo,
32
35
  nonce,
36
+ cv: codeVerifier,
33
37
  ...(options?.linkConsent ? { linkConsent: true } : {}),
34
38
  });
35
39
  const state = Buffer.from(payload).toString('base64url');
@@ -38,8 +42,9 @@ export function generateOAuthState(provider, redirectTo, options) {
38
42
  throw new Error('REVEALUI_SECRET is required for OAuth state signing. ' +
39
43
  'Set it in your environment variables.');
40
44
  }
45
+ // lgtm[js/insufficient-password-hash] - HMAC-SHA256 for CSRF state signing, not password hashing
41
46
  const hmac = crypto.createHmac('sha256', secret).update(state).digest('hex');
42
- return { state, cookieValue: `${state}.${hmac}` };
47
+ return { state, cookieValue: `${state}.${hmac}`, codeChallenge };
43
48
  }
44
49
  /**
45
50
  * Verify a signed OAuth state token from the callback.
@@ -64,8 +69,9 @@ export function verifyOAuthState(state, cookieValue) {
64
69
  throw new Error('REVEALUI_SECRET is required for OAuth state verification. ' +
65
70
  'Set it in your environment variables.');
66
71
  }
72
+ // lgtm[js/insufficient-password-hash] - HMAC-SHA256 for CSRF state verification, not password hashing
67
73
  const expectedHmac = crypto.createHmac('sha256', secret).update(state).digest('hex');
68
- // Both are hex-encoded SHA-256 HMACsmust be exactly 64 hex characters.
74
+ // Both are hex-encoded SHA-256 HMACs - must be exactly 64 hex characters.
69
75
  // Reject wrong-length inputs immediately; do NOT pad (padding enables forged matches
70
76
  // where a short storedHmac is zero-padded to collide with the expected hash).
71
77
  if (storedHmac.length !== 64 || expectedHmac.length !== 64)
@@ -84,6 +90,7 @@ export function verifyOAuthState(state, cookieValue) {
84
90
  provider: parsed.provider,
85
91
  redirectTo: parsed.redirectTo,
86
92
  ...(parsed.linkConsent ? { linkConsent: true } : {}),
93
+ ...(parsed.cv ? { codeVerifier: parsed.cv } : {}),
87
94
  };
88
95
  }
89
96
  catch {
@@ -108,7 +115,7 @@ function getClientId(provider) {
108
115
  throw new Error(`Missing client ID for provider: ${provider}`);
109
116
  return id;
110
117
  }
111
- export function buildAuthUrl(provider, redirectUri, state) {
118
+ export function buildAuthUrl(provider, redirectUri, state, codeChallenge) {
112
119
  if (!isProvider(provider))
113
120
  throw new Error(`Unknown provider: ${provider}`);
114
121
  const clientId = getClientId(provider);
@@ -117,9 +124,9 @@ export function buildAuthUrl(provider, redirectUri, state) {
117
124
  github: github.buildAuthUrl,
118
125
  vercel: vercel.buildAuthUrl,
119
126
  };
120
- return builders[provider](clientId, redirectUri, state);
127
+ return builders[provider](clientId, redirectUri, state, codeChallenge);
121
128
  }
122
- export async function exchangeCode(provider, code, redirectUri) {
129
+ export async function exchangeCode(provider, code, redirectUri, codeVerifier) {
123
130
  if (!isProvider(provider))
124
131
  throw new Error(`Unknown provider: ${provider}`);
125
132
  const exchangers = {
@@ -127,7 +134,7 @@ export async function exchangeCode(provider, code, redirectUri) {
127
134
  github: github.exchangeCode,
128
135
  vercel: vercel.exchangeCode,
129
136
  };
130
- return exchangers[provider](code, redirectUri);
137
+ return exchangers[provider](code, redirectUri, codeVerifier);
131
138
  }
132
139
  export async function fetchProviderUser(provider, accessToken) {
133
140
  if (!isProvider(provider))
@@ -194,7 +201,7 @@ export async function upsertOAuthUser(provider, providerUser, options) {
194
201
  .limit(1);
195
202
  if (existingUser) {
196
203
  if (options?.linkConsent) {
197
- // User explicitly consented to linkuse the existing account
204
+ // User explicitly consented to link - use the existing account
198
205
  userId = existingUser.id;
199
206
  isNewUser = false;
200
207
  logger.info(`Linking ${provider} account to existing user ${userId} (consent-based)`);
@@ -266,7 +273,7 @@ export async function linkOAuthAccount(userId, provider, providerUser) {
266
273
  .limit(1);
267
274
  if (existingLink) {
268
275
  if (existingLink.userId === userId) {
269
- // Already linked to this userrefresh metadata and return
276
+ // Already linked to this user - refresh metadata and return
270
277
  await db
271
278
  .update(oauthAccounts)
272
279
  .set({
@@ -281,7 +288,7 @@ export async function linkOAuthAccount(userId, provider, providerUser) {
281
288
  throw new Error('Authenticated user not found in database');
282
289
  return user;
283
290
  }
284
- // Linked to a different usercannot steal the identity
291
+ // Linked to a different user - cannot steal the identity
285
292
  throw new Error('This provider account is already linked to another user. Unlink it from the other account first.');
286
293
  }
287
294
  // 2. Check the authenticated user exists
@@ -13,9 +13,9 @@ export interface PasskeyConfig {
13
13
  maxPasskeysPerUser: number;
14
14
  /** Challenge TTL in ms (default: 5 minutes) */
15
15
  challengeTtlMs: number;
16
- /** Relying Party IDdomain name (default: 'localhost') */
16
+ /** Relying Party ID - domain name (default: 'localhost') */
17
17
  rpId: string;
18
- /** Relying Party nameuser-visible (default: 'RevealUI') */
18
+ /** Relying Party name - user-visible (default: 'RevealUI') */
19
19
  rpName: string;
20
20
  /** Expected origin(s) for verification (default: 'http://localhost:4000') */
21
21
  origin: string | string[];
@@ -93,7 +93,7 @@ export declare function verifyAuthentication(response: AuthenticationResponseJSO
93
93
  newCounter: number;
94
94
  }>;
95
95
  /**
96
- * List all passkeys for a user (safe for clientexcludes publicKey and counter).
96
+ * List all passkeys for a user (safe for client - excludes publicKey and counter).
97
97
  */
98
98
  export declare function listPasskeys(userId: string): Promise<{
99
99
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EACV,0BAA0B,EAC1B,sCAAsC,EACtC,qCAAqC,EACrC,wBAAwB,EACxB,4BAA4B,EAC5B,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAahC,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC3B;AAaD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAGxE;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,qBAAqB,CAAC,EAAE,MAAM,EAAE,GAC/B,OAAO,CAAC,sCAAsC,CAAC,CAwBjD;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,wBAAwB,EAClC,iBAAiB,EAAE,MAAM,EACzB,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GACjC,OAAO,CAAC,4BAA4B,CAAC,CAavC;AAMD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE;IACV,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,EACD,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC;IACT,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC,CAqCD;AAMD;;;;;;GAMG;AACH,wBAAsB,+BAA+B,CACnD,gBAAgB,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,GACzD,OAAO,CAAC,qCAAqC,CAAC,CAqBhD;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,0BAA0B,EACpC,UAAU,EAAE,kBAAkB,EAC9B,iBAAiB,EAAE,MAAM,EACzB,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GACjC,OAAO,CAAC;IACT,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAyBD;AAMD;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CACzD;IACE,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB,EAAE,CACJ,CAgBA;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASpF;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAkBzD"}
1
+ {"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"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"}
@@ -199,7 +199,7 @@ export async function verifyAuthentication(response, credential, expectedChallen
199
199
  // Management
200
200
  // =============================================================================
201
201
  /**
202
- * List all passkeys for a user (safe for clientexcludes publicKey and counter).
202
+ * List all passkeys for a user (safe for client - excludes publicKey and counter).
203
203
  */
204
204
  export async function listPasskeys(userId) {
205
205
  const db = getClient();
@@ -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;;;;;;;;;;;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"}
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,CAuF9B;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"}
@@ -37,7 +37,7 @@ function generateSalt() {
37
37
  export async function generatePasswordResetToken(email) {
38
38
  try {
39
39
  const db = getClient();
40
- // Find user by emailintentionally does NOT check user.password.
40
+ // Find user by email - intentionally does NOT check user.password.
41
41
  // OAuth-only users (password: null) can use this flow to set a password,
42
42
  // giving them a fallback login method independent of their OAuth provider.
43
43
  // This is safe because the reset link is sent to their verified email.
@@ -166,7 +166,7 @@ export async function resetPasswordWithToken(tokenId, token, newPassword) {
166
166
  };
167
167
  }
168
168
  // Validate password strength
169
- const { validatePasswordStrength } = await import('./password-validation.js');
169
+ const { validatePasswordStrength, checkPasswordBreach } = await import('./password-validation.js');
170
170
  const passwordValidation = validatePasswordStrength(newPassword);
171
171
  if (!passwordValidation.valid) {
172
172
  return {
@@ -174,6 +174,14 @@ export async function resetPasswordWithToken(tokenId, token, newPassword) {
174
174
  error: passwordValidation.errors.join('. '),
175
175
  };
176
176
  }
177
+ // Check password against known data breaches (non-blocking on failure)
178
+ const breachCount = await checkPasswordBreach(newPassword);
179
+ if (breachCount > 0) {
180
+ return {
181
+ success: false,
182
+ error: `This password has appeared in ${breachCount.toLocaleString()} data breaches. Please choose a different password.`,
183
+ };
184
+ }
177
185
  // Hash new password
178
186
  const password = await bcrypt.hash(newPassword, 12);
179
187
  // Update user password
@@ -242,7 +250,7 @@ export async function changePassword(userId, currentPassword, newPassword, curre
242
250
  .where(and(eq(sessions.userId, userId), ne(sessions.id, currentSessionId)));
243
251
  }
244
252
  else {
245
- // No current session ID provideddelete all sessions as a safe default
253
+ // No current session ID provided - delete all sessions as a safe default
246
254
  await db.delete(sessions).where(eq(sessions.userId, userId));
247
255
  }
248
256
  return { success: true };
@@ -22,4 +22,15 @@ export declare function validatePasswordStrength(password: string): PasswordVali
22
22
  * @returns True if meets minimum requirements
23
23
  */
24
24
  export declare function meetsMinimumPasswordRequirements(password: string): boolean;
25
+ /**
26
+ * Check if a password appears in known data breaches via the
27
+ * HaveIBeenPwned Passwords API (k-anonymity model).
28
+ *
29
+ * Only the first 5 characters of the SHA-1 hash are sent to the API.
30
+ * The full hash never leaves the server.
31
+ *
32
+ * @returns The number of times this password has been seen in breaches, or 0 if clean.
33
+ * Returns -1 if the check could not be performed (network error).
34
+ */
35
+ export declare function checkPasswordBreach(password: string): Promise<number>;
25
36
  //# sourceMappingURL=password-validation.d.ts.map
@@ -1 +1 @@
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"}
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;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2B3E"}
@@ -50,3 +50,40 @@ export function validatePasswordStrength(password) {
50
50
  export function meetsMinimumPasswordRequirements(password) {
51
51
  return password.length >= 8 && password.length <= 128;
52
52
  }
53
+ /**
54
+ * Check if a password appears in known data breaches via the
55
+ * HaveIBeenPwned Passwords API (k-anonymity model).
56
+ *
57
+ * Only the first 5 characters of the SHA-1 hash are sent to the API.
58
+ * The full hash never leaves the server.
59
+ *
60
+ * @returns The number of times this password has been seen in breaches, or 0 if clean.
61
+ * Returns -1 if the check could not be performed (network error).
62
+ */
63
+ export async function checkPasswordBreach(password) {
64
+ const { createHash } = await import('node:crypto');
65
+ // lgtm[js/insufficient-password-hash] - SHA-1 required by HIBP k-anonymity API, not used for password storage
66
+ const sha1 = createHash('sha1').update(password).digest('hex').toUpperCase();
67
+ const prefix = sha1.slice(0, 5);
68
+ const suffix = sha1.slice(5);
69
+ try {
70
+ const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
71
+ headers: { 'Add-Padding': 'true' },
72
+ signal: AbortSignal.timeout(5000),
73
+ });
74
+ if (!response.ok)
75
+ return -1;
76
+ const text = await response.text();
77
+ for (const line of text.split('\n')) {
78
+ const [hashSuffix, countStr] = line.split(':');
79
+ if (hashSuffix?.trim() === suffix) {
80
+ return Number.parseInt(countStr?.trim() ?? '0', 10);
81
+ }
82
+ }
83
+ return 0;
84
+ }
85
+ catch {
86
+ // Network error, timeout, or API issue - don't block the user
87
+ return -1;
88
+ }
89
+ }
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * GitHub OAuth Provider
3
3
  *
4
- * Uses native fetchno additional npm dependencies.
4
+ * Uses native fetch - no additional npm dependencies.
5
5
  * Scopes: read:user user:email
6
6
  *
7
7
  * Note: GitHub may return null email if user has set it private.
8
8
  * In that case we fetch from /user/emails and pick the primary verified one.
9
9
  */
10
10
  import type { ProviderUser } from '../oauth.js';
11
- export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
12
- export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
11
+ export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string, codeChallenge?: string): string;
12
+ export declare function exchangeCode(code: string, redirectUri: string, codeVerifier?: string): Promise<string>;
13
13
  export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
14
14
  //# sourceMappingURL=github.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../src/server/providers/github.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;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,CAOzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoCrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAkD1E"}
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../src/server/providers/github.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAWR;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAwCjB;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAkD1E"}
@@ -1,33 +1,41 @@
1
1
  /**
2
2
  * GitHub OAuth Provider
3
3
  *
4
- * Uses native fetchno additional npm dependencies.
4
+ * Uses native fetch - no additional npm dependencies.
5
5
  * Scopes: read:user user:email
6
6
  *
7
7
  * Note: GitHub may return null email if user has set it private.
8
8
  * In that case we fetch from /user/emails and pick the primary verified one.
9
9
  */
10
- export function buildAuthUrl(clientId, redirectUri, state) {
10
+ export function buildAuthUrl(clientId, redirectUri, state, codeChallenge) {
11
11
  const url = new URL('https://github.com/login/oauth/authorize');
12
12
  url.searchParams.set('client_id', clientId);
13
13
  url.searchParams.set('redirect_uri', redirectUri);
14
14
  url.searchParams.set('scope', 'read:user user:email');
15
15
  url.searchParams.set('state', state);
16
+ if (codeChallenge) {
17
+ url.searchParams.set('code_challenge', codeChallenge);
18
+ url.searchParams.set('code_challenge_method', 'S256');
19
+ }
16
20
  return url.toString();
17
21
  }
18
- export async function exchangeCode(code, redirectUri) {
22
+ export async function exchangeCode(code, redirectUri, codeVerifier) {
23
+ const params = {
24
+ code,
25
+ client_id: process.env.GITHUB_CLIENT_ID ?? '',
26
+ client_secret: process.env.GITHUB_CLIENT_SECRET ?? '',
27
+ redirect_uri: redirectUri,
28
+ };
29
+ if (codeVerifier) {
30
+ params.code_verifier = codeVerifier;
31
+ }
19
32
  const response = await fetch('https://github.com/login/oauth/access_token', {
20
33
  method: 'POST',
21
34
  headers: {
22
35
  'Content-Type': 'application/x-www-form-urlencoded',
23
36
  Accept: 'application/json',
24
37
  },
25
- body: new URLSearchParams({
26
- code,
27
- client_id: process.env.GITHUB_CLIENT_ID ?? '',
28
- client_secret: process.env.GITHUB_CLIENT_SECRET ?? '',
29
- redirect_uri: redirectUri,
30
- }),
38
+ body: new URLSearchParams(params),
31
39
  });
32
40
  if (!response.ok) {
33
41
  let detail = '';
@@ -36,9 +44,9 @@ export async function exchangeCode(code, redirectUri) {
36
44
  detail = err.error_description ?? err.error ?? '';
37
45
  }
38
46
  catch {
39
- // Response body not JSONuse status only
47
+ // Response body not JSON - use status only
40
48
  }
41
- throw new Error(`GitHub token exchange failed: ${response.status}${detail ? `${detail}` : ''}`);
49
+ throw new Error(`GitHub token exchange failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
42
50
  }
43
51
  const data = (await response.json());
44
52
  if (data.error) {
@@ -64,7 +72,7 @@ export async function fetchUser(accessToken) {
64
72
  catch {
65
73
  // Response body not JSON
66
74
  }
67
- throw new Error(`GitHub user fetch failed: ${userResponse.status}${detail ? `${detail}` : ''}`);
75
+ throw new Error(`GitHub user fetch failed: ${userResponse.status}${detail ? ` - ${detail}` : ''}`);
68
76
  }
69
77
  const user = (await userResponse.json());
70
78
  let email = user.email ?? null;
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Google OAuth 2.0 Provider
3
3
  *
4
- * Uses native fetchno additional npm dependencies.
4
+ * Uses native fetch - no additional npm dependencies.
5
5
  * Scopes: openid email profile
6
6
  */
7
7
  import type { ProviderUser } from '../oauth.js';
8
- export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
9
- export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
8
+ export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string, codeChallenge?: string): string;
9
+ export declare function exchangeCode(code: string, redirectUri: string, codeVerifier?: string): Promise<string>;
10
10
  export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
11
11
  //# sourceMappingURL=google.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../src/server/providers/google.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,CASzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA+B1E"}
1
+ {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../../src/server/providers/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAaR;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAmCjB;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA+B1E"}
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Google OAuth 2.0 Provider
3
3
  *
4
- * Uses native fetchno additional npm dependencies.
4
+ * Uses native fetch - no additional npm dependencies.
5
5
  * Scopes: openid email profile
6
6
  */
7
- export function buildAuthUrl(clientId, redirectUri, state) {
7
+ export function buildAuthUrl(clientId, redirectUri, state, codeChallenge) {
8
8
  const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
9
9
  url.searchParams.set('client_id', clientId);
10
10
  url.searchParams.set('redirect_uri', redirectUri);
@@ -12,19 +12,27 @@ export function buildAuthUrl(clientId, redirectUri, state) {
12
12
  url.searchParams.set('scope', 'openid email profile');
13
13
  url.searchParams.set('state', state);
14
14
  url.searchParams.set('access_type', 'online');
15
+ if (codeChallenge) {
16
+ url.searchParams.set('code_challenge', codeChallenge);
17
+ url.searchParams.set('code_challenge_method', 'S256');
18
+ }
15
19
  return url.toString();
16
20
  }
17
- export async function exchangeCode(code, redirectUri) {
21
+ export async function exchangeCode(code, redirectUri, codeVerifier) {
22
+ const params = {
23
+ code,
24
+ client_id: process.env.GOOGLE_CLIENT_ID ?? '',
25
+ client_secret: process.env.GOOGLE_CLIENT_SECRET ?? '',
26
+ redirect_uri: redirectUri,
27
+ grant_type: 'authorization_code',
28
+ };
29
+ if (codeVerifier) {
30
+ params.code_verifier = codeVerifier;
31
+ }
18
32
  const response = await fetch('https://oauth2.googleapis.com/token', {
19
33
  method: 'POST',
20
34
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
21
- body: new URLSearchParams({
22
- code,
23
- client_id: process.env.GOOGLE_CLIENT_ID ?? '',
24
- client_secret: process.env.GOOGLE_CLIENT_SECRET ?? '',
25
- redirect_uri: redirectUri,
26
- grant_type: 'authorization_code',
27
- }),
35
+ body: new URLSearchParams(params),
28
36
  });
29
37
  if (!response.ok) {
30
38
  let detail = '';
@@ -33,9 +41,9 @@ export async function exchangeCode(code, redirectUri) {
33
41
  detail = err.error_description ?? err.error ?? '';
34
42
  }
35
43
  catch {
36
- // Response body not JSONuse status only
44
+ // Response body not JSON - use status only
37
45
  }
38
- throw new Error(`Google token exchange failed: ${response.status}${detail ? `${detail}` : ''}`);
46
+ throw new Error(`Google token exchange failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
39
47
  }
40
48
  const data = (await response.json());
41
49
  if (!data.access_token || typeof data.access_token !== 'string') {
@@ -56,7 +64,7 @@ export async function fetchUser(accessToken) {
56
64
  catch {
57
65
  // Response body not JSON
58
66
  }
59
- throw new Error(`Google userinfo fetch failed: ${response.status}${detail ? `${detail}` : ''}`);
67
+ throw new Error(`Google userinfo fetch failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
60
68
  }
61
69
  const data = (await response.json());
62
70
  return {
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Vercel OAuth Provider
3
3
  *
4
- * Uses native fetchno additional npm dependencies.
5
- * No scopes requiredVercel uses full access by default.
4
+ * Uses native fetch - no additional npm dependencies.
5
+ * No scopes required - Vercel uses full access by default.
6
6
  */
7
7
  import type { ProviderUser } from '../oauth.js';
8
- export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string): string;
9
- export declare function exchangeCode(code: string, redirectUri: string): Promise<string>;
8
+ export declare function buildAuthUrl(clientId: string, redirectUri: string, state: string, _codeChallenge?: string): string;
9
+ export declare function exchangeCode(code: string, redirectUri: string, _codeVerifier?: string): Promise<string>;
10
10
  export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
11
11
  //# sourceMappingURL=vercel.d.ts.map
@@ -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,CAiCrF;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,CAC1B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAMR;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,CAiCjB;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAmC1E"}
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Vercel OAuth Provider
3
3
  *
4
- * Uses native fetchno additional npm dependencies.
5
- * No scopes requiredVercel uses full access by default.
4
+ * Uses native fetch - no additional npm dependencies.
5
+ * No scopes required - Vercel uses full access by default.
6
6
  */
7
- export function buildAuthUrl(clientId, redirectUri, state) {
7
+ export function buildAuthUrl(clientId, redirectUri, state, _codeChallenge) {
8
8
  const url = new URL('https://vercel.com/oauth/authorize');
9
9
  url.searchParams.set('client_id', clientId);
10
10
  url.searchParams.set('redirect_uri', redirectUri);
11
11
  url.searchParams.set('state', state);
12
12
  return url.toString();
13
13
  }
14
- export async function exchangeCode(code, redirectUri) {
14
+ export async function exchangeCode(code, redirectUri, _codeVerifier) {
15
15
  const response = await fetch('https://api.vercel.com/v2/oauth/access_token', {
16
16
  method: 'POST',
17
17
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -29,9 +29,9 @@ export async function exchangeCode(code, redirectUri) {
29
29
  detail = err.error_description ?? err.error ?? '';
30
30
  }
31
31
  catch {
32
- // Response body not JSONuse status only
32
+ // Response body not JSON - use status only
33
33
  }
34
- throw new Error(`Vercel token exchange failed: ${response.status}${detail ? `${detail}` : ''}`);
34
+ throw new Error(`Vercel token exchange failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
35
35
  }
36
36
  const data = (await response.json());
37
37
  if (data.error) {
@@ -55,7 +55,7 @@ export async function fetchUser(accessToken) {
55
55
  catch {
56
56
  // Response body not JSON
57
57
  }
58
- throw new Error(`Vercel user fetch failed: ${response.status}${detail ? `${detail}` : ''}`);
58
+ throw new Error(`Vercel user fetch failed: ${response.status}${detail ? ` - ${detail}` : ''}`);
59
59
  }
60
60
  const data = (await response.json());
61
61
  const u = data.user;
@@ -8,7 +8,7 @@ import type { Session, User } from '../types.js';
8
8
  export interface SessionBindingConfig {
9
9
  /** Invalidate session when user-agent changes (default: true) */
10
10
  enforceUserAgent: boolean;
11
- /** Invalidate session when IP address changes (default: falseusers roam) */
11
+ /** Invalidate session when IP address changes (default: false - users roam) */
12
12
  enforceIp: boolean;
13
13
  /** Log a warning when IP changes but don't invalidate (default: true) */
14
14
  warnOnIpChange: boolean;
@@ -36,7 +36,7 @@ export interface SessionData {
36
36
  /**
37
37
  * Check if a session is a recovery session (created via magic link recovery).
38
38
  *
39
- * Recovery sessions are restrictedthey should only be used for:
39
+ * Recovery sessions are restricted - they should only be used for:
40
40
  * - Changing the password (`/api/auth/change-password`)
41
41
  * - Signing out (`/api/auth/sign-out`)
42
42
  * - Viewing current session (`/api/auth/me`, `/api/auth/session`)
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,+EAA+E;IAC/E,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;CACzB;AAUD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAEtF;AAED,qCAAqC;AACrC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAMD,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CA0B3F;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAkH7B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAmE9C;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAoC9C;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBtE;AAED;;;;GAIG;AACH;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;CACzB;AAUD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAEtF;AAED,qCAAqC;AACrC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAMD,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CA0B3F;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAI1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAkH7B;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAmE9C;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAoC9C;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBtE;AAED;;;;GAIG;AACH;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
@@ -57,7 +57,7 @@ export function validateSessionBinding(session, ctx) {
57
57
  /**
58
58
  * Check if a session is a recovery session (created via magic link recovery).
59
59
  *
60
- * Recovery sessions are restrictedthey should only be used for:
60
+ * Recovery sessions are restricted - they should only be used for:
61
61
  * - Changing the password (`/api/auth/change-password`)
62
62
  * - Signing out (`/api/auth/sign-out`)
63
63
  * - Viewing current session (`/api/auth/me`, `/api/auth/session`)
@@ -127,7 +127,7 @@ export async function getSession(headers, requestContext) {
127
127
  if (requestContext) {
128
128
  const bindingError = validateSessionBinding(session, requestContext);
129
129
  if (bindingError) {
130
- logger.warn('Session binding violationinvalidating session', {
130
+ logger.warn('Session binding violation - invalidating session', {
131
131
  sessionId: session.id,
132
132
  reason: bindingError,
133
133
  });
@@ -1 +1 @@
1
- {"version":3,"file":"signed-cookie.d.ts","sourceRoot":"","sources":["../../src/server/signed-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC/D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,MAAM,GACb,MAAM,CAQR;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EACjE,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,CAAC,GAAG,IAAI,CAyCV"}
1
+ {"version":3,"file":"signed-cookie.d.ts","sourceRoot":"","sources":["../../src/server/signed-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC/D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,MAAM,GACb,MAAM,CASR;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EACjE,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,CAAC,GAAG,IAAI,CA0CV"}
@@ -18,6 +18,7 @@ import crypto from 'node:crypto';
18
18
  export function signCookiePayload(payload, secret) {
19
19
  const payloadJson = JSON.stringify(payload);
20
20
  const payloadB64 = Buffer.from(payloadJson).toString('base64url');
21
+ // lgtm[js/insufficient-password-hash] - HMAC-SHA256 for cookie payload signing, not password hashing
21
22
  const signature = crypto.createHmac('sha256', secret).update(payloadB64).digest();
22
23
  const signatureB64 = signature.toString('base64url');
23
24
  return `${payloadB64}.${signatureB64}`;
@@ -42,16 +43,17 @@ export function verifyCookiePayload(signed, secret) {
42
43
  }
43
44
  const [payloadB64, signatureB64] = parts;
44
45
  // Recompute the expected signature
46
+ // lgtm[js/insufficient-password-hash] - HMAC-SHA256 for cookie verification, not password hashing
45
47
  const expectedSignature = crypto.createHmac('sha256', secret).update(payloadB64).digest();
46
48
  const actualSignature = Buffer.from(signatureB64, 'base64url');
47
- // Timing-safe comparisonbuffers must be same length
49
+ // Timing-safe comparison - buffers must be same length
48
50
  if (expectedSignature.length !== actualSignature.length) {
49
51
  return null;
50
52
  }
51
53
  if (!crypto.timingSafeEqual(expectedSignature, actualSignature)) {
52
54
  return null;
53
55
  }
54
- // Signature validdecode and parse the payload
56
+ // Signature valid - decode and parse the payload
55
57
  const payloadJson = Buffer.from(payloadB64, 'base64url').toString('utf8');
56
58
  const payload = JSON.parse(payloadJson);
57
59
  // Check expiry
@@ -39,7 +39,7 @@ export function getStorage() {
39
39
  }
40
40
  }
41
41
  catch {
42
- // Config validation failedtry process.env fallback below
42
+ // Config validation failed - try process.env fallback below
43
43
  }
44
44
  dbUrl = dbUrl || process.env.POSTGRES_URL || process.env.DATABASE_URL;
45
45
  if (dbUrl) {
@@ -79,7 +79,7 @@ export function createStorage() {
79
79
  }
80
80
  }
81
81
  catch {
82
- // Config validation failedfall through to in-memory
82
+ // Config validation failed - fall through to in-memory
83
83
  }
84
84
  return new InMemoryStorage();
85
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/auth",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Authentication system for RevealUI - database-backed sessions with Better Auth patterns",
5
5
  "keywords": [
6
6
  "auth",
@@ -14,23 +14,23 @@
14
14
  "bcryptjs": "^3.0.3",
15
15
  "drizzle-orm": "^0.45.2",
16
16
  "zod": "^4.3.6",
17
- "@revealui/config": "0.3.3",
18
- "@revealui/contracts": "1.3.6",
19
- "@revealui/core": "0.5.5",
20
- "@revealui/db": "0.3.6",
21
- "@revealui/security": "0.2.6"
17
+ "@revealui/config": "0.3.4",
18
+ "@revealui/contracts": "1.3.7",
19
+ "@revealui/core": "0.5.6",
20
+ "@revealui/db": "0.3.7",
21
+ "@revealui/security": "0.2.7"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@simplewebauthn/browser": "^13.3.0",
25
25
  "@testing-library/react": "^16.3.2",
26
- "@types/node": "^25.5.0",
26
+ "@types/node": "^25.5.2",
27
27
  "@types/react": "^19.2.14",
28
- "@vitest/coverage-v8": "^4.1.0",
29
- "happy-dom": "^20.8.4",
30
- "react": "^19.2.3",
28
+ "@vitest/coverage-v8": "^4.1.3",
29
+ "happy-dom": "^20.8.9",
30
+ "react": "^19.2.5",
31
31
  "typescript": "^6.0.2",
32
- "vitest": "^4.1.0",
33
- "dev": "0.0.1"
32
+ "vitest": "^4.1.3",
33
+ "@revealui/dev": "0.1.0"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=24.13.0"
@@ -77,6 +77,11 @@
77
77
  "url": "https://github.com/RevealUIStudio/revealui.git",
78
78
  "directory": "packages/auth"
79
79
  },
80
+ "homepage": "https://revealui.com",
81
+ "author": "RevealUI Studio <founder@revealui.com>",
82
+ "bugs": {
83
+ "url": "https://github.com/RevealUIStudio/revealui/issues"
84
+ },
80
85
  "scripts": {
81
86
  "build": "tsc",
82
87
  "clean": "rm -rf dist",