@revealui/auth 0.3.2 → 0.3.4
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/dist/server/mfa.d.ts +3 -0
- package/dist/server/mfa.d.ts.map +1 -1
- package/dist/server/mfa.js +45 -4
- package/dist/server/passkey.d.ts.map +1 -1
- package/dist/server/passkey.js +15 -3
- package/dist/server/password-reset.d.ts +5 -3
- package/dist/server/password-reset.d.ts.map +1 -1
- package/dist/server/password-reset.js +18 -4
- package/package.json +5 -5
package/dist/server/mfa.d.ts
CHANGED
|
@@ -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;
|
package/dist/server/mfa.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/server/mfa.js
CHANGED
|
@@ -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({
|
|
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
|
|
142
|
-
if (
|
|
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":"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;
|
|
1
|
+
{"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"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"}
|
package/dist/server/passkey.js
CHANGED
|
@@ -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.
|
|
54
|
-
*
|
|
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
|
|
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.
|
|
205
|
-
*
|
|
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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/auth",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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/
|
|
18
|
-
"@revealui/
|
|
19
|
-
"@revealui/
|
|
20
|
-
"@revealui/
|
|
17
|
+
"@revealui/core": "0.5.2",
|
|
18
|
+
"@revealui/db": "0.3.3",
|
|
19
|
+
"@revealui/contracts": "1.3.3",
|
|
20
|
+
"@revealui/config": "0.3.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@simplewebauthn/browser": "^13.3.0",
|