@revealui/auth 0.3.0 → 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/auth.d.ts.map +1 -1
- package/dist/server/auth.js +10 -4
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/magic-link.js +1 -1
- 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/oauth.d.ts +15 -4
- package/dist/server/oauth.d.ts.map +1 -1
- package/dist/server/oauth.js +32 -17
- 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/dist/server/providers/github.d.ts.map +1 -1
- package/dist/server/providers/github.js +0 -3
- package/dist/server/providers/google.d.ts.map +1 -1
- package/dist/server/providers/google.js +0 -1
- package/dist/server/providers/vercel.d.ts.map +1 -1
- package/dist/server/providers/vercel.js +0 -1
- package/dist/server/session.d.ts +17 -0
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +51 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -10
|
@@ -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,
|
|
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,CAqKvB;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"}
|
package/dist/server/auth.js
CHANGED
|
@@ -51,8 +51,10 @@ export async function signIn(email, password, options) {
|
|
|
51
51
|
try {
|
|
52
52
|
db = getClient();
|
|
53
53
|
}
|
|
54
|
-
catch {
|
|
55
|
-
logger.error('Error getting database client'
|
|
54
|
+
catch (clientError) {
|
|
55
|
+
logger.error('Error getting database client', clientError instanceof Error ? clientError : undefined, {
|
|
56
|
+
message: clientError instanceof Error ? clientError.message : String(clientError),
|
|
57
|
+
});
|
|
56
58
|
return {
|
|
57
59
|
success: false,
|
|
58
60
|
reason: 'database_error',
|
|
@@ -69,8 +71,12 @@ export async function signIn(email, password, options) {
|
|
|
69
71
|
.limit(1);
|
|
70
72
|
user = result[0];
|
|
71
73
|
}
|
|
72
|
-
catch {
|
|
73
|
-
logger.error('Error querying user'
|
|
74
|
+
catch (dbError) {
|
|
75
|
+
logger.error('Error querying user', dbError instanceof Error ? dbError : undefined, {
|
|
76
|
+
message: dbError instanceof Error ? dbError.message : String(dbError),
|
|
77
|
+
name: dbError instanceof Error ? dbError.name : 'unknown',
|
|
78
|
+
stack: dbError instanceof Error ? dbError.stack : undefined,
|
|
79
|
+
});
|
|
74
80
|
return {
|
|
75
81
|
success: false,
|
|
76
82
|
reason: 'database_error',
|
package/dist/server/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { MagicLinkConfig } from './magic-link.js';
|
|
|
12
12
|
export { configureMagicLink, createMagicLink, resetMagicLinkConfig, verifyMagicLink, } from './magic-link.js';
|
|
13
13
|
export type { MFAConfig, MFADisableProof, MFASetupResult } from './mfa.js';
|
|
14
14
|
export { configureMFA, disableMFA, initiateMFASetup, isMFAEnabled, regenerateBackupCodes, resetMFAConfig, verifyBackupCode, verifyMFACode, verifyMFASetup, } from './mfa.js';
|
|
15
|
-
export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getLinkedProviders, linkOAuthAccount, type ProviderUser, unlinkOAuthAccount, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
|
|
15
|
+
export { buildAuthUrl, exchangeCode, fetchProviderUser, generateOAuthState, getLinkedProviders, linkOAuthAccount, type ProviderUser, type UpsertOAuthOptions, unlinkOAuthAccount, upsertOAuthUser, verifyOAuthState, } from './oauth.js';
|
|
16
16
|
export type { PasskeyConfig } from './passkey.js';
|
|
17
17
|
export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenticationChallenge, generateRegistrationChallenge, listPasskeys, renamePasskey, resetPasskeyConfig, storePasskey, verifyAuthentication, verifyRegistration, } from './passkey.js';
|
|
18
18
|
export type { ChangePasswordResult, PasswordResetResult, PasswordResetToken, } from './password-reset.js';
|
|
@@ -20,7 +20,7 @@ export { changePassword, generatePasswordResetToken, invalidatePasswordResetToke
|
|
|
20
20
|
export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
|
|
21
21
|
export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
|
|
22
22
|
export type { RequestContext, SessionBindingConfig, SessionData } from './session.js';
|
|
23
|
-
export { configureSessionBinding, createSession, deleteAllUserSessions, deleteSession, getSession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
|
|
23
|
+
export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
|
|
24
24
|
export { signCookiePayload, verifyCookiePayload } from './signed-cookie.js';
|
|
25
25
|
export type { Storage } from './storage/index.js';
|
|
26
26
|
export { createStorage, DatabaseStorage, getStorage, InMemoryStorage, resetStorage, } from './storage/index.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,YAAY,EACjB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,+BAA+B,EAC/B,6BAA6B,EAC7B,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,aAAa,EACb,UAAU,EACV,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,+BAA+B,EAC/B,6BAA6B,EAC7B,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { configurePasskey, countUserCredentials, deletePasskey, generateAuthenti
|
|
|
14
14
|
export { changePassword, generatePasswordResetToken, invalidatePasswordResetToken, resetPasswordWithToken, validatePasswordResetToken, } from './password-reset.js';
|
|
15
15
|
export { meetsMinimumPasswordRequirements, validatePasswordStrength, } from './password-validation.js';
|
|
16
16
|
export { checkRateLimit, configureRateLimit, getRateLimitStatus, resetRateLimit, resetRateLimitConfig, } from './rate-limit.js';
|
|
17
|
-
export { configureSessionBinding, createSession, deleteAllUserSessions, deleteSession, getSession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
|
|
17
|
+
export { configureSessionBinding, createSession, deleteAllUserSessions, deleteOtherUserSessions, deleteSession, getSession, isRecoverySession, resetSessionBindingConfig, rotateSession, validateSessionBinding, } from './session.js';
|
|
18
18
|
// Signed Cookie
|
|
19
19
|
export { signCookiePayload, verifyCookiePayload } from './signed-cookie.js';
|
|
20
20
|
export { createStorage, DatabaseStorage, getStorage, InMemoryStorage, resetStorage, } from './storage/index.js';
|
|
@@ -92,7 +92,7 @@ export async function createMagicLink(userId) {
|
|
|
92
92
|
*/
|
|
93
93
|
export async function verifyMagicLink(token) {
|
|
94
94
|
const db = getClient();
|
|
95
|
-
// Select unexpired, unused magic links
|
|
95
|
+
// Select all unexpired, unused magic links
|
|
96
96
|
const rows = await db
|
|
97
97
|
.select()
|
|
98
98
|
.from(magicLinks)
|
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));
|
package/dist/server/oauth.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ export interface ProviderUser {
|
|
|
19
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
|
-
export declare function generateOAuthState(provider: string, redirectTo: string
|
|
22
|
+
export declare function generateOAuthState(provider: string, redirectTo: string, options?: {
|
|
23
|
+
linkConsent?: boolean;
|
|
24
|
+
}): {
|
|
23
25
|
state: string;
|
|
24
26
|
cookieValue: string;
|
|
25
27
|
};
|
|
@@ -31,21 +33,30 @@ export declare function generateOAuthState(provider: string, redirectTo: string)
|
|
|
31
33
|
export declare function verifyOAuthState(state: string | null | undefined, cookieValue: string | null | undefined): {
|
|
32
34
|
provider: string;
|
|
33
35
|
redirectTo: string;
|
|
36
|
+
linkConsent?: boolean;
|
|
34
37
|
} | null;
|
|
35
38
|
export declare function buildAuthUrl(provider: string, redirectUri: string, state: string): string;
|
|
36
39
|
export declare function exchangeCode(provider: string, code: string, redirectUri: string): Promise<string>;
|
|
37
40
|
export declare function fetchProviderUser(provider: string, accessToken: string): Promise<ProviderUser>;
|
|
41
|
+
export interface UpsertOAuthOptions {
|
|
42
|
+
/**
|
|
43
|
+
* When true, the user has explicitly consented to link their OAuth
|
|
44
|
+
* provider to an existing local account with the same email.
|
|
45
|
+
* Without consent, an email-match throws OAuthAccountConflictError.
|
|
46
|
+
*/
|
|
47
|
+
linkConsent?: boolean;
|
|
48
|
+
}
|
|
38
49
|
/**
|
|
39
50
|
* Find or create a local user for the given OAuth identity.
|
|
40
51
|
*
|
|
41
52
|
* Flow:
|
|
42
53
|
* 1. Look up oauth_accounts by (provider, providerUserId) → get userId
|
|
43
54
|
* 2. If found: refresh metadata + return user
|
|
44
|
-
* 3. If not found: check users by email → link if match
|
|
45
|
-
* 4. If no match: create new user (role: '
|
|
55
|
+
* 3. If not found: check users by email → link if match (with consent) or throw
|
|
56
|
+
* 4. If no match: create new user (role: 'user', no password)
|
|
46
57
|
* 5. Insert oauth_accounts row
|
|
47
58
|
*/
|
|
48
|
-
export declare function upsertOAuthUser(provider: string, providerUser: ProviderUser): Promise<User>;
|
|
59
|
+
export declare function upsertOAuthUser(provider: string, providerUser: ProviderUser, options?: UpsertOAuthOptions): Promise<User>;
|
|
49
60
|
/**
|
|
50
61
|
* Link an OAuth provider to an existing authenticated user.
|
|
51
62
|
*
|
|
@@ -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,
|
|
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"}
|
package/dist/server/oauth.js
CHANGED
|
@@ -24,9 +24,14 @@ import * as vercel from './providers/vercel.js';
|
|
|
24
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
|
-
export function generateOAuthState(provider, redirectTo) {
|
|
27
|
+
export function generateOAuthState(provider, redirectTo, options) {
|
|
28
28
|
const nonce = crypto.randomBytes(16).toString('hex');
|
|
29
|
-
const payload = JSON.stringify({
|
|
29
|
+
const payload = JSON.stringify({
|
|
30
|
+
provider,
|
|
31
|
+
redirectTo,
|
|
32
|
+
nonce,
|
|
33
|
+
...(options?.linkConsent ? { linkConsent: true } : {}),
|
|
34
|
+
});
|
|
30
35
|
const state = Buffer.from(payload).toString('base64url');
|
|
31
36
|
const secret = process.env.REVEALUI_SECRET;
|
|
32
37
|
if (!secret) {
|
|
@@ -75,7 +80,11 @@ export function verifyOAuthState(state, cookieValue) {
|
|
|
75
80
|
}
|
|
76
81
|
try {
|
|
77
82
|
const parsed = JSON.parse(Buffer.from(state, 'base64url').toString());
|
|
78
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
provider: parsed.provider,
|
|
85
|
+
redirectTo: parsed.redirectTo,
|
|
86
|
+
...(parsed.linkConsent ? { linkConsent: true } : {}),
|
|
87
|
+
};
|
|
79
88
|
}
|
|
80
89
|
catch {
|
|
81
90
|
return null;
|
|
@@ -130,20 +139,17 @@ export async function fetchProviderUser(provider, accessToken) {
|
|
|
130
139
|
};
|
|
131
140
|
return fetchers[provider](accessToken);
|
|
132
141
|
}
|
|
133
|
-
// =============================================================================
|
|
134
|
-
// User Upsert
|
|
135
|
-
// =============================================================================
|
|
136
142
|
/**
|
|
137
143
|
* Find or create a local user for the given OAuth identity.
|
|
138
144
|
*
|
|
139
145
|
* Flow:
|
|
140
146
|
* 1. Look up oauth_accounts by (provider, providerUserId) → get userId
|
|
141
147
|
* 2. If found: refresh metadata + return user
|
|
142
|
-
* 3. If not found: check users by email → link if match
|
|
143
|
-
* 4. If no match: create new user (role: '
|
|
148
|
+
* 3. If not found: check users by email → link if match (with consent) or throw
|
|
149
|
+
* 4. If no match: create new user (role: 'user', no password)
|
|
144
150
|
* 5. Insert oauth_accounts row
|
|
145
151
|
*/
|
|
146
|
-
export async function upsertOAuthUser(provider, providerUser) {
|
|
152
|
+
export async function upsertOAuthUser(provider, providerUser, options) {
|
|
147
153
|
const db = getClient();
|
|
148
154
|
// 1. Check for existing linked account
|
|
149
155
|
const [existingAccount] = await db
|
|
@@ -173,12 +179,11 @@ export async function upsertOAuthUser(provider, providerUser) {
|
|
|
173
179
|
}
|
|
174
180
|
return user;
|
|
175
181
|
}
|
|
176
|
-
// 2. Check for existing user by email
|
|
182
|
+
// 2. Check for existing user by email
|
|
177
183
|
// If an account with this email already exists but was not linked via OAuth,
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// via linkOAuthAccount().
|
|
184
|
+
// either link it (when the user has given explicit consent) or reject the
|
|
185
|
+
// login. Auto-linking without consent is an account takeover vector: an
|
|
186
|
+
// attacker who controls a provider email instantly owns the existing account.
|
|
182
187
|
let userId;
|
|
183
188
|
let isNewUser = false;
|
|
184
189
|
if (providerUser.email) {
|
|
@@ -188,10 +193,20 @@ export async function upsertOAuthUser(provider, providerUser) {
|
|
|
188
193
|
.where(eq(users.email, providerUser.email))
|
|
189
194
|
.limit(1);
|
|
190
195
|
if (existingUser) {
|
|
191
|
-
|
|
196
|
+
if (options?.linkConsent) {
|
|
197
|
+
// User explicitly consented to link — use the existing account
|
|
198
|
+
userId = existingUser.id;
|
|
199
|
+
isNewUser = false;
|
|
200
|
+
logger.info(`Linking ${provider} account to existing user ${userId} (consent-based)`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
throw new OAuthAccountConflictError(providerUser.email);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
isNewUser = true;
|
|
208
|
+
userId = crypto.randomUUID();
|
|
192
209
|
}
|
|
193
|
-
isNewUser = true;
|
|
194
|
-
userId = crypto.randomUUID();
|
|
195
210
|
}
|
|
196
211
|
else {
|
|
197
212
|
isNewUser = true;
|
|
@@ -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) {
|
|
@@ -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,
|
|
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"}
|
|
@@ -20,7 +20,6 @@ export async function exchangeCode(code, redirectUri) {
|
|
|
20
20
|
method: 'POST',
|
|
21
21
|
headers: {
|
|
22
22
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
23
|
-
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
24
23
|
Accept: 'application/json',
|
|
25
24
|
},
|
|
26
25
|
body: new URLSearchParams({
|
|
@@ -52,9 +51,7 @@ export async function exchangeCode(code, redirectUri) {
|
|
|
52
51
|
}
|
|
53
52
|
export async function fetchUser(accessToken) {
|
|
54
53
|
const headers = {
|
|
55
|
-
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
56
54
|
Authorization: `Bearer ${accessToken}`,
|
|
57
|
-
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
58
55
|
Accept: 'application/vnd.github+json',
|
|
59
56
|
};
|
|
60
57
|
const userResponse = await fetch('https://api.github.com/user', { headers });
|
|
@@ -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,
|
|
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"}
|
|
@@ -45,7 +45,6 @@ export async function exchangeCode(code, redirectUri) {
|
|
|
45
45
|
}
|
|
46
46
|
export async function fetchUser(accessToken) {
|
|
47
47
|
const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
|
|
48
|
-
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
49
48
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
50
49
|
});
|
|
51
50
|
if (!response.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/server/providers/vercel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/server/providers/vercel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzF;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiC1E"}
|
|
@@ -38,7 +38,6 @@ export async function exchangeCode(code, redirectUri) {
|
|
|
38
38
|
}
|
|
39
39
|
export async function fetchUser(accessToken) {
|
|
40
40
|
const response = await fetch('https://api.vercel.com/v2/user', {
|
|
41
|
-
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
42
41
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
43
42
|
});
|
|
44
43
|
if (!response.ok) {
|
package/dist/server/session.d.ts
CHANGED
|
@@ -33,6 +33,18 @@ export interface SessionData {
|
|
|
33
33
|
session: Session;
|
|
34
34
|
user: User;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if a session is a recovery session (created via magic link recovery).
|
|
38
|
+
*
|
|
39
|
+
* Recovery sessions are restricted — they should only be used for:
|
|
40
|
+
* - Changing the password (`/api/auth/change-password`)
|
|
41
|
+
* - Signing out (`/api/auth/sign-out`)
|
|
42
|
+
* - Viewing current session (`/api/auth/me`, `/api/auth/session`)
|
|
43
|
+
*
|
|
44
|
+
* All other operations (MFA management, passkey management, OAuth linking,
|
|
45
|
+
* admin actions) should reject recovery sessions.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isRecoverySession(sessionData: SessionData | null): boolean;
|
|
36
48
|
/**
|
|
37
49
|
* Get session from request headers (cookie)
|
|
38
50
|
*
|
|
@@ -90,5 +102,10 @@ export declare function deleteSession(headers: Headers): Promise<boolean>;
|
|
|
90
102
|
*
|
|
91
103
|
* @param userId - User ID
|
|
92
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* Delete all sessions for a user EXCEPT the specified session.
|
|
107
|
+
* Used for "sign out all other devices" functionality.
|
|
108
|
+
*/
|
|
109
|
+
export declare function deleteOtherUserSessions(userId: string, exceptSessionId: string): Promise<void>;
|
|
93
110
|
export declare function deleteAllUserSessions(userId: string): Promise<void>;
|
|
94
111
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -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;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,
|
|
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"}
|
package/dist/server/session.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { logger } from '@revealui/core/observability/logger';
|
|
8
8
|
import { getClient } from '@revealui/db/client';
|
|
9
9
|
import { sessions, users } from '@revealui/db/schema';
|
|
10
|
-
import { and, eq, gt, isNull } from 'drizzle-orm';
|
|
10
|
+
import { and, eq, gt, isNull, ne } from 'drizzle-orm';
|
|
11
11
|
import { hashToken } from '../utils/token.js';
|
|
12
12
|
import { DatabaseError, TokenError } from './errors.js';
|
|
13
13
|
const DEFAULT_SESSION_BINDING = {
|
|
@@ -54,6 +54,23 @@ export function validateSessionBinding(session, ctx) {
|
|
|
54
54
|
}
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a session is a recovery session (created via magic link recovery).
|
|
59
|
+
*
|
|
60
|
+
* Recovery sessions are restricted — they should only be used for:
|
|
61
|
+
* - Changing the password (`/api/auth/change-password`)
|
|
62
|
+
* - Signing out (`/api/auth/sign-out`)
|
|
63
|
+
* - Viewing current session (`/api/auth/me`, `/api/auth/session`)
|
|
64
|
+
*
|
|
65
|
+
* All other operations (MFA management, passkey management, OAuth linking,
|
|
66
|
+
* admin actions) should reject recovery sessions.
|
|
67
|
+
*/
|
|
68
|
+
export function isRecoverySession(sessionData) {
|
|
69
|
+
if (!sessionData)
|
|
70
|
+
return false;
|
|
71
|
+
const metadata = sessionData.session.metadata;
|
|
72
|
+
return metadata?.recovery === true;
|
|
73
|
+
}
|
|
57
74
|
/**
|
|
58
75
|
* Get session from request headers (cookie)
|
|
59
76
|
*
|
|
@@ -95,7 +112,7 @@ export async function getSession(headers, requestContext) {
|
|
|
95
112
|
const result = await db
|
|
96
113
|
.select()
|
|
97
114
|
.from(sessions)
|
|
98
|
-
.where(and(eq(sessions.tokenHash, tokenHash), gt(sessions.expiresAt, new Date())))
|
|
115
|
+
.where(and(eq(sessions.tokenHash, tokenHash), gt(sessions.expiresAt, new Date()), isNull(sessions.deletedAt)))
|
|
99
116
|
.limit(1);
|
|
100
117
|
session = result[0];
|
|
101
118
|
}
|
|
@@ -317,6 +334,38 @@ export async function deleteSession(headers) {
|
|
|
317
334
|
*
|
|
318
335
|
* @param userId - User ID
|
|
319
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* Delete all sessions for a user EXCEPT the specified session.
|
|
339
|
+
* Used for "sign out all other devices" functionality.
|
|
340
|
+
*/
|
|
341
|
+
export async function deleteOtherUserSessions(userId, exceptSessionId) {
|
|
342
|
+
try {
|
|
343
|
+
let db;
|
|
344
|
+
try {
|
|
345
|
+
db = getClient();
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
logger.error('Error getting database client');
|
|
349
|
+
throw new DatabaseError('Database connection failed', error);
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
await db
|
|
353
|
+
.delete(sessions)
|
|
354
|
+
.where(and(eq(sessions.userId, userId), ne(sessions.id, exceptSessionId)));
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
logger.error('Error deleting other user sessions');
|
|
358
|
+
throw new DatabaseError('Failed to delete other user sessions', error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
if (err instanceof DatabaseError) {
|
|
363
|
+
throw err;
|
|
364
|
+
}
|
|
365
|
+
logger.error('Unexpected error in deleteOtherUserSessions');
|
|
366
|
+
throw new DatabaseError('Unexpected error deleting other user sessions', err);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
320
369
|
export async function deleteAllUserSessions(userId) {
|
|
321
370
|
try {
|
|
322
371
|
let db;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,IAAI,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,IAAI,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,IAAI,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,0GAA0G;AAC1G,MAAM,MAAM,YAAY,GACpB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EACF,qBAAqB,GACrB,gBAAgB,GAChB,cAAc,GACd,gBAAgB,GAChB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
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",
|
|
@@ -12,23 +12,23 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@simplewebauthn/server": "^13.3.0",
|
|
14
14
|
"bcryptjs": "^3.0.3",
|
|
15
|
-
"drizzle-orm": "^0.45.
|
|
15
|
+
"drizzle-orm": "^0.45.2",
|
|
16
16
|
"zod": "^4.3.6",
|
|
17
|
-
"@revealui/core": "0.
|
|
18
|
-
"@revealui/db": "0.3.
|
|
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",
|
|
24
24
|
"@testing-library/react": "^16.3.2",
|
|
25
|
-
"@types/node": "^25.
|
|
25
|
+
"@types/node": "^25.5.0",
|
|
26
26
|
"@types/react": "^19.2.14",
|
|
27
|
-
"@vitest/coverage-v8": "^4.0
|
|
27
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
28
28
|
"happy-dom": "^20.8.4",
|
|
29
29
|
"react": "^19.2.3",
|
|
30
|
-
"typescript": "^
|
|
31
|
-
"vitest": "^4.0
|
|
30
|
+
"typescript": "^6.0.2",
|
|
31
|
+
"vitest": "^4.1.0",
|
|
32
32
|
"dev": "0.0.1"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|