@revealui/auth 0.4.6 → 0.4.7

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.
@@ -15,6 +15,7 @@ export interface UseSignInResult {
15
15
  } | {
16
16
  success: false;
17
17
  error: string;
18
+ code?: string;
18
19
  } | {
19
20
  success: false;
20
21
  requiresMfa: true;
@@ -1 +1 @@
1
- {"version":3,"file":"useSignIn.d.ts","sourceRoot":"","sources":["../../src/react/useSignIn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAkCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CACN,KAAK,EAAE,WAAW,KACf,OAAO,CACR;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACjC;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAC3D,CAAC;IACF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAsE3C"}
1
+ {"version":3,"file":"useSignIn.d.ts","sourceRoot":"","sources":["../../src/react/useSignIn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAuCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CACN,KAAK,EAAE,WAAW,KACf,OAAO,CACR;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAC3D,CAAC;IACF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CA2E3C"}
@@ -27,9 +27,14 @@ const SignInUserSchema = z
27
27
  function toUser(validated) {
28
28
  return validated;
29
29
  }
30
- // Validation schemas for sign-in response
30
+ // Validation schemas for sign-in response. The error body carries both a
31
+ // human-readable `message` and a machine `code` (see createApplicationErrorResponseData
32
+ // in @revealui/core); `error` mirrors the code. All optional so a terse body
33
+ // still parses.
31
34
  const SignInErrorResponseSchema = z.object({
32
35
  error: z.string().optional(),
36
+ message: z.string().optional(),
37
+ code: z.string().optional(),
33
38
  });
34
39
  const SignInSuccessResponseSchema = z.object({
35
40
  user: SignInUserSchema,
@@ -79,9 +84,14 @@ export function useSignIn() {
79
84
  const json = await response.json();
80
85
  if (!response.ok) {
81
86
  const errorData = SignInErrorResponseSchema.parse(json);
87
+ // Prefer the human-readable `message` for display; fall back to the
88
+ // machine `error` code, then a generic default. `code` (e.g.
89
+ // 'EMAIL_NOT_VERIFIED') lets callers branch — e.g. offer a
90
+ // resend-verification action — without string-matching display copy.
82
91
  return {
83
92
  success: false,
84
- error: errorData.error || 'Failed to sign in',
93
+ error: errorData.message || errorData.error || 'Failed to sign in',
94
+ code: errorData.code,
85
95
  };
86
96
  }
87
97
  // Check for MFA challenge before parsing as full success
@@ -1 +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"}
1
+ {"version":3,"file":"audit-bridge.d.ts","sourceRoot":"","sources":["../../src/server/audit-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyCH;;;;;;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"}
@@ -5,6 +5,7 @@
5
5
  * via the AuditSystem from @revealui/security/server. Uses lazy import to
6
6
  * avoid circular dependency issues at module load time.
7
7
  */
8
+ import { logger } from '@revealui/core/observability/logger';
8
9
  /**
9
10
  * Lazily resolve the global audit system from @revealui/security/server.
10
11
  * Returns null if the module cannot be loaded (e.g. in test environments
@@ -20,15 +21,24 @@ async function getAudit() {
20
21
  }
21
22
  }
22
23
  /**
23
- * Internal helper - logs an audit event, silently skipping if the
24
- * audit system is unavailable.
24
+ * Internal helper - logs an audit event best-effort. Silently skips when the
25
+ * audit system is unavailable, and swallows (but logs) a write failure so the
26
+ * operation being audited - e.g. a login - never fails because its audit
27
+ * record could not be persisted.
25
28
  */
26
29
  async function logAuditEvent(event) {
27
30
  const auditSystem = await getAudit();
28
31
  if (!auditSystem) {
29
32
  return;
30
33
  }
31
- await auditSystem.log(event);
34
+ try {
35
+ await auditSystem.log(event);
36
+ }
37
+ catch (error) {
38
+ logger.error('Failed to write audit event', error instanceof Error ? error : undefined, {
39
+ type: event.type,
40
+ });
41
+ }
32
42
  }
33
43
  /**
34
44
  * Record a successful login to the audit trail.
@@ -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,CAiLvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBtD;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"}
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;AAUpE;;;;;;;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,CAyLvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBtD;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,CAoOvB"}
@@ -6,9 +6,10 @@
6
6
  import { createHash, randomBytes } from 'node:crypto';
7
7
  import { logger } from '@revealui/core/observability/logger';
8
8
  import { getClient } from '@revealui/db/client';
9
- import { oauthAccounts, users } from '@revealui/db/schema';
9
+ import { accountMemberships, accounts, oauthAccounts, users } from '@revealui/db/schema';
10
10
  import bcrypt from 'bcryptjs';
11
11
  import { and, eq, isNull } from 'drizzle-orm';
12
+ import { auditLoginFailure, auditLoginSuccess } from './audit-bridge.js';
12
13
  import { clearFailedAttempts, isAccountLocked, recordFailedAttempt } from './brute-force.js';
13
14
  import { validatePasswordStrength } from './password-validation.js';
14
15
  import { checkRateLimit } from './rate-limit.js';
@@ -85,22 +86,25 @@ export async function signIn(email, password, options) {
85
86
  }
86
87
  // Always return same error message to prevent user enumeration
87
88
  const invalidCredentialsMessage = 'Invalid email or password';
88
- if (!user) {
89
+ // Record a failed credential attempt: bump the brute-force counter AND
90
+ // land a failed-login receipt in the audit trail. recordFailedAttempt only
91
+ // feeds lockout accounting; without the audit emit a failed sign-in leaves
92
+ // no receipt. auditLoginFailure is best-effort and never throws.
93
+ const failInvalidCredentials = async () => {
89
94
  await recordFailedAttempt(email);
95
+ await auditLoginFailure(email, options?.ipAddress ?? 'unknown', options?.userAgent ?? 'unknown', 'invalid_credentials');
90
96
  return {
91
97
  success: false,
92
98
  reason: 'invalid_credentials',
93
99
  error: invalidCredentialsMessage,
94
100
  };
101
+ };
102
+ if (!user) {
103
+ return failInvalidCredentials();
95
104
  }
96
105
  // Check if user has a password (not OAuth-only user)
97
106
  if (!user.password) {
98
- await recordFailedAttempt(email);
99
- return {
100
- success: false,
101
- reason: 'invalid_credentials',
102
- error: invalidCredentialsMessage,
103
- };
107
+ return failInvalidCredentials();
104
108
  }
105
109
  // Verify password hash
106
110
  let isValid;
@@ -109,20 +113,10 @@ export async function signIn(email, password, options) {
109
113
  }
110
114
  catch {
111
115
  logger.error('Error comparing password');
112
- await recordFailedAttempt(email);
113
- return {
114
- success: false,
115
- reason: 'invalid_credentials',
116
- error: invalidCredentialsMessage,
117
- };
116
+ return failInvalidCredentials();
118
117
  }
119
118
  if (!isValid) {
120
- await recordFailedAttempt(email);
121
- return {
122
- success: false,
123
- reason: 'invalid_credentials',
124
- error: invalidCredentialsMessage,
125
- };
119
+ return failInvalidCredentials();
126
120
  }
127
121
  // Successful login - clear failed attempts
128
122
  await clearFailedAttempts(email);
@@ -164,6 +158,10 @@ export async function signIn(email, password, options) {
164
158
  error: 'Failed to create session',
165
159
  };
166
160
  }
161
+ // A fresh session was minted: the login succeeded. Land a success receipt
162
+ // in the audit trail (best-effort; auditLoginSuccess never throws, so an
163
+ // audit-write failure cannot fail the login).
164
+ await auditLoginSuccess(user.id, options?.ipAddress ?? 'unknown', options?.userAgent ?? 'unknown');
167
165
  // Check if password rotation is required (e.g., bootstrapped admin accounts)
168
166
  if (user.mustRotatePassword) {
169
167
  return {
@@ -396,6 +394,38 @@ export async function signUp(email, password, name, options) {
396
394
  error: 'Failed to create session',
397
395
  };
398
396
  }
397
+ // Provision a personal billing account + owner membership so a hosted tenant
398
+ // exists from signup. Best-effort: the Stripe billing webhook
399
+ // (ensureHostedAccount) is idempotent on an active membership and backfills
400
+ // this if it fails here, so a transient error never blocks signup. Skipped on
401
+ // self-hosted (Forge) deployments — single-tenant, no per-user accounts (mode
402
+ // detected by the license signing key, as in apps/server validate-startup).
403
+ if (process.env.REVEALUI_LICENSE_PRIVATE_KEY) {
404
+ try {
405
+ const accountId = crypto.randomUUID();
406
+ const now = new Date();
407
+ await db.insert(accounts).values({
408
+ id: accountId,
409
+ name: `${user.name || 'RevealUI'} Workspace`,
410
+ slug: `acct-${accountId}`,
411
+ status: 'active',
412
+ createdAt: now,
413
+ updatedAt: now,
414
+ });
415
+ await db.insert(accountMemberships).values({
416
+ id: crypto.randomUUID(),
417
+ accountId,
418
+ userId: user.id,
419
+ role: 'owner',
420
+ status: 'active',
421
+ createdAt: now,
422
+ updatedAt: now,
423
+ });
424
+ }
425
+ catch {
426
+ logger.error('Failed to provision personal account at signup (billing webhook will backfill)', undefined, { userId: user.id });
427
+ }
428
+ }
399
429
  // Return the raw (unhashed) token so the caller can include it in the
400
430
  // verification email link. The DB holds only the hash.
401
431
  const userWithRawToken = { ...user, emailVerificationToken: rawEmailVerificationToken };
@@ -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,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;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BtE;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;AAaD,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,CAgI7B;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;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BtE;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"}
@@ -15,6 +15,8 @@ const DEFAULT_SESSION_BINDING = {
15
15
  enforceIp: false,
16
16
  warnOnIpChange: true,
17
17
  };
18
+ /** Minimum interval between users.lastActiveAt writes for a given user. */
19
+ const LAST_ACTIVE_THROTTLE_MS = 60 * 60 * 1000;
18
20
  let sessionBindingConfig = { ...DEFAULT_SESSION_BINDING };
19
21
  /** Override session binding behaviour (useful for tests or strict deployments). */
20
22
  export function configureSessionBinding(overrides) {
@@ -169,6 +171,19 @@ export async function getSession(headers, requestContext) {
169
171
  // Log but don't fail - last activity update is not critical
170
172
  logger.warn('Error updating last activity');
171
173
  }
174
+ // Throttled users.lastActiveAt writer - fire-and-forget, never awaited,
175
+ // so auth latency is unaffected. Only touches the row when the stored
176
+ // value is null or older than the throttle window, keeping write volume
177
+ // to roughly one row-update per active user per hour instead of one per
178
+ // request.
179
+ if (!user.lastActiveAt || Date.now() - user.lastActiveAt.getTime() > LAST_ACTIVE_THROTTLE_MS) {
180
+ db.update(users)
181
+ .set({ lastActiveAt: new Date() })
182
+ .where(eq(users.id, user.id))
183
+ .catch(() => {
184
+ logger.warn('Error updating user lastActiveAt');
185
+ });
186
+ }
172
187
  return {
173
188
  session,
174
189
  user,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/auth",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Database-backed session auth for Hono and Next.js — bcrypt, OAuth, brute-force protection, rate limiting, password reset. Ships with RevealUI.",
5
5
  "keywords": [
6
6
  "auth",
@@ -10,26 +10,26 @@
10
10
  ],
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
- "@simplewebauthn/server": "^13.3.1",
13
+ "@simplewebauthn/server": "^13.3.2",
14
14
  "bcryptjs": "^3.0.3",
15
15
  "drizzle-orm": "^0.45.2",
16
16
  "zod": "^4.4.3",
17
- "@revealui/config": "0.4.3",
18
- "@revealui/contracts": "0.6.1",
19
- "@revealui/core": "0.10.2",
20
- "@revealui/db": "0.7.2",
21
- "@revealui/security": "0.4.1"
17
+ "@revealui/config": "0.5.0",
18
+ "@revealui/contracts": "0.6.2",
19
+ "@revealui/core": "0.11.0",
20
+ "@revealui/db": "0.7.3",
21
+ "@revealui/security": "0.4.2"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@simplewebauthn/browser": "^13.3.0",
25
25
  "@testing-library/react": "^16.3.2",
26
- "@types/node": "^25.9.1",
26
+ "@types/node": "^25.9.4",
27
27
  "@types/react": "^19.2.17",
28
- "@vitest/coverage-v8": "^4.1.8",
29
- "happy-dom": "^20.10.3",
28
+ "@vitest/coverage-v8": "^4.1.10",
29
+ "happy-dom": "^20.10.6",
30
30
  "react": "^19.2.7",
31
31
  "typescript": "^6.0.3",
32
- "vitest": "^4.1.8",
32
+ "vitest": "^4.1.10",
33
33
  "@revealui/dev": "0.1.0"
34
34
  },
35
35
  "engines": {