@revealui/auth 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -34
- package/dist/react/useSignUp.d.ts +1 -0
- package/dist/react/useSignUp.d.ts.map +1 -1
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +18 -1
- package/dist/server/brute-force.d.ts.map +1 -1
- package/dist/server/brute-force.js +29 -20
- package/dist/server/errors.d.ts +4 -0
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +8 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/oauth.d.ts +49 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +223 -0
- package/dist/server/password-reset.d.ts +17 -6
- package/dist/server/password-reset.d.ts.map +1 -1
- package/dist/server/password-reset.js +72 -46
- package/dist/server/providers/github.d.ts +14 -0
- package/dist/server/providers/github.d.ts.map +1 -0
- package/dist/server/providers/github.js +73 -0
- package/dist/server/providers/google.d.ts +11 -0
- package/dist/server/providers/google.d.ts.map +1 -0
- package/dist/server/providers/google.js +53 -0
- package/dist/server/providers/vercel.d.ts +11 -0
- package/dist/server/providers/vercel.d.ts.map +1 -0
- package/dist/server/providers/vercel.js +47 -0
- package/dist/server/rate-limit.js +11 -11
- package/dist/server/session.js +1 -1
- package/dist/server/storage/database.d.ts +9 -0
- package/dist/server/storage/database.d.ts.map +1 -1
- package/dist/server/storage/database.js +30 -0
- package/dist/server/storage/in-memory.d.ts +4 -0
- package/dist/server/storage/in-memory.d.ts.map +1 -1
- package/dist/server/storage/in-memory.js +10 -0
- package/dist/server/storage/interface.d.ts +10 -0
- package/dist/server/storage/interface.d.ts.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +3 -0
- package/dist/utils/token.d.ts +9 -1
- package/dist/utils/token.d.ts.map +1 -1
- package/dist/utils/token.js +9 -1
- package/package.json +5 -5
|
@@ -12,6 +12,7 @@ export interface PasswordResetResult {
|
|
|
12
12
|
success: boolean;
|
|
13
13
|
error?: string;
|
|
14
14
|
token?: string;
|
|
15
|
+
tokenId?: string;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Generates a password reset token for a user
|
|
@@ -23,22 +24,32 @@ export declare function generatePasswordResetToken(email: string): Promise<Passw
|
|
|
23
24
|
/**
|
|
24
25
|
* Validates a password reset token
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
+
* Uses O(1) lookup by token ID, then verifies the token hash with timingSafeEqual
|
|
28
|
+
* against the single matching row.
|
|
29
|
+
*
|
|
30
|
+
* @param tokenId - Token row ID (from the reset URL)
|
|
31
|
+
* @param token - Reset token (plain text, from the reset URL)
|
|
27
32
|
* @returns User ID if valid, null otherwise
|
|
28
33
|
*/
|
|
29
|
-
export declare function validatePasswordResetToken(token: string): Promise<string | null>;
|
|
34
|
+
export declare function validatePasswordResetToken(tokenId: string, token: string): Promise<string | null>;
|
|
30
35
|
/**
|
|
31
36
|
* Resets password using a token
|
|
32
37
|
*
|
|
33
|
-
*
|
|
38
|
+
* Uses O(1) lookup by token ID, then verifies the token hash.
|
|
39
|
+
*
|
|
40
|
+
* @param tokenId - Token row ID (from the reset URL)
|
|
41
|
+
* @param token - Reset token (plain text, from the reset URL)
|
|
34
42
|
* @param newPassword - New password
|
|
35
43
|
* @returns Success result
|
|
36
44
|
*/
|
|
37
|
-
export declare function resetPasswordWithToken(token: string, newPassword: string): Promise<PasswordResetResult>;
|
|
45
|
+
export declare function resetPasswordWithToken(tokenId: string, token: string, newPassword: string): Promise<PasswordResetResult>;
|
|
38
46
|
/**
|
|
39
47
|
* Invalidates a password reset token
|
|
40
48
|
*
|
|
41
|
-
*
|
|
49
|
+
* Uses O(1) lookup by token ID, then verifies the token hash before marking as used.
|
|
50
|
+
*
|
|
51
|
+
* @param tokenId - Token row ID (from the reset URL)
|
|
52
|
+
* @param token - Reset token (plain text, from the reset URL)
|
|
42
53
|
*/
|
|
43
|
-
export declare function invalidatePasswordResetToken(token: string): Promise<void>;
|
|
54
|
+
export declare function invalidatePasswordResetToken(tokenId: string, token: string): Promise<void>;
|
|
44
55
|
//# sourceMappingURL=password-reset.d.ts.map
|
|
@@ -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,CAAA;IACb,SAAS,EAAE,IAAI,CAAA;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;
|
|
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,CAAA;IACb,SAAS,EAAE,IAAI,CAAA;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAqE5F;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;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChG"}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import crypto from 'node:crypto';
|
|
8
8
|
import { logger } from '@revealui/core/observability/logger';
|
|
9
9
|
import { getClient } from '@revealui/db/client';
|
|
10
|
-
import { passwordResetTokens, users } from '@revealui/db/schema';
|
|
10
|
+
import { passwordResetTokens, sessions, users } from '@revealui/db/schema';
|
|
11
11
|
import bcrypt from 'bcryptjs';
|
|
12
12
|
import { and, eq, gt, isNull } from 'drizzle-orm';
|
|
13
|
-
const TOKEN_EXPIRY_MS =
|
|
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.
|
|
16
16
|
* The salt is stored in the DB alongside the hash; this defeats rainbow
|
|
@@ -46,6 +46,13 @@ export async function generatePasswordResetToken(email) {
|
|
|
46
46
|
token: crypto.randomBytes(32).toString('hex'),
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
// Invalidate any existing unused reset tokens for this user before creating a new one.
|
|
50
|
+
// This limits active tokens to one per user, preventing table accumulation that would
|
|
51
|
+
// slow the time-bounded full-table scan in validatePasswordResetToken.
|
|
52
|
+
await db
|
|
53
|
+
.update(passwordResetTokens)
|
|
54
|
+
.set({ usedAt: new Date() })
|
|
55
|
+
.where(and(eq(passwordResetTokens.userId, user.id), isNull(passwordResetTokens.usedAt)));
|
|
49
56
|
// Generate secure token with per-token salt
|
|
50
57
|
const token = crypto.randomBytes(32).toString('hex');
|
|
51
58
|
const tokenSalt = generateSalt();
|
|
@@ -63,6 +70,7 @@ export async function generatePasswordResetToken(email) {
|
|
|
63
70
|
return {
|
|
64
71
|
success: true,
|
|
65
72
|
token,
|
|
73
|
+
tokenId: id,
|
|
66
74
|
};
|
|
67
75
|
}
|
|
68
76
|
catch (error) {
|
|
@@ -86,29 +94,31 @@ export async function generatePasswordResetToken(email) {
|
|
|
86
94
|
/**
|
|
87
95
|
* Validates a password reset token
|
|
88
96
|
*
|
|
89
|
-
*
|
|
97
|
+
* Uses O(1) lookup by token ID, then verifies the token hash with timingSafeEqual
|
|
98
|
+
* against the single matching row.
|
|
99
|
+
*
|
|
100
|
+
* @param tokenId - Token row ID (from the reset URL)
|
|
101
|
+
* @param token - Reset token (plain text, from the reset URL)
|
|
90
102
|
* @returns User ID if valid, null otherwise
|
|
91
103
|
*/
|
|
92
|
-
export async function validatePasswordResetToken(token) {
|
|
104
|
+
export async function validatePasswordResetToken(tokenId, token) {
|
|
93
105
|
try {
|
|
94
106
|
const db = getClient();
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
// To avoid full-table scans we include a time filter. In practice, there are
|
|
98
|
-
// very few active reset tokens at any given moment.
|
|
99
|
-
//
|
|
100
|
-
// A common alternative is to encode the token ID in the reset URL (e.g.,
|
|
101
|
-
// /reset?id=<uuid>&token=<token>), but this requires a URL format change.
|
|
102
|
-
// For now we do a time-bounded scan — acceptable at low token volume.
|
|
103
|
-
const candidates = await db
|
|
107
|
+
// O(1) lookup by primary key, filtered to unexpired and unused tokens
|
|
108
|
+
const [entry] = await db
|
|
104
109
|
.select()
|
|
105
110
|
.from(passwordResetTokens)
|
|
106
|
-
.where(and(gt(passwordResetTokens.expiresAt, new Date()), isNull(passwordResetTokens.usedAt)))
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
.where(and(eq(passwordResetTokens.id, tokenId), gt(passwordResetTokens.expiresAt, new Date()), isNull(passwordResetTokens.usedAt)))
|
|
112
|
+
.limit(1);
|
|
113
|
+
if (!entry) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// Verify the token hash using timing-safe comparison
|
|
117
|
+
const expectedHash = hashToken(token, entry.tokenSalt);
|
|
118
|
+
const expectedBuf = Buffer.from(expectedHash);
|
|
119
|
+
const actualBuf = Buffer.from(entry.tokenHash);
|
|
120
|
+
if (expectedBuf.length === actualBuf.length && crypto.timingSafeEqual(expectedBuf, actualBuf)) {
|
|
121
|
+
return entry.userId;
|
|
112
122
|
}
|
|
113
123
|
return null;
|
|
114
124
|
}
|
|
@@ -120,32 +130,38 @@ export async function validatePasswordResetToken(token) {
|
|
|
120
130
|
/**
|
|
121
131
|
* Resets password using a token
|
|
122
132
|
*
|
|
123
|
-
*
|
|
133
|
+
* Uses O(1) lookup by token ID, then verifies the token hash.
|
|
134
|
+
*
|
|
135
|
+
* @param tokenId - Token row ID (from the reset URL)
|
|
136
|
+
* @param token - Reset token (plain text, from the reset URL)
|
|
124
137
|
* @param newPassword - New password
|
|
125
138
|
* @returns Success result
|
|
126
139
|
*/
|
|
127
|
-
export async function resetPasswordWithToken(token, newPassword) {
|
|
140
|
+
export async function resetPasswordWithToken(tokenId, token, newPassword) {
|
|
128
141
|
try {
|
|
129
142
|
const db = getClient();
|
|
130
|
-
//
|
|
131
|
-
const
|
|
143
|
+
// O(1) lookup by primary key, filtered to unexpired and unused tokens
|
|
144
|
+
const [entry] = await db
|
|
132
145
|
.select()
|
|
133
146
|
.from(passwordResetTokens)
|
|
134
|
-
.where(and(gt(passwordResetTokens.expiresAt, new Date()), isNull(passwordResetTokens.usedAt)))
|
|
135
|
-
|
|
136
|
-
for (const candidate of candidates) {
|
|
137
|
-
const expectedHash = hashToken(token, candidate.tokenSalt);
|
|
138
|
-
if (expectedHash === candidate.tokenHash) {
|
|
139
|
-
entry = candidate;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
147
|
+
.where(and(eq(passwordResetTokens.id, tokenId), gt(passwordResetTokens.expiresAt, new Date()), isNull(passwordResetTokens.usedAt)))
|
|
148
|
+
.limit(1);
|
|
143
149
|
if (!entry) {
|
|
144
150
|
return {
|
|
145
151
|
success: false,
|
|
146
152
|
error: 'Invalid or expired reset token',
|
|
147
153
|
};
|
|
148
154
|
}
|
|
155
|
+
// Verify the token hash using timing-safe comparison
|
|
156
|
+
const expectedHash = hashToken(token, entry.tokenSalt);
|
|
157
|
+
const expectedBuf = Buffer.from(expectedHash);
|
|
158
|
+
const actualBuf = Buffer.from(entry.tokenHash);
|
|
159
|
+
if (!(expectedBuf.length === actualBuf.length && crypto.timingSafeEqual(expectedBuf, actualBuf))) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
error: 'Invalid or expired reset token',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
149
165
|
// Validate password strength
|
|
150
166
|
const { validatePasswordStrength } = await import('./password-validation.js');
|
|
151
167
|
const passwordValidation = validatePasswordStrength(newPassword);
|
|
@@ -159,6 +175,9 @@ export async function resetPasswordWithToken(token, newPassword) {
|
|
|
159
175
|
const password = await bcrypt.hash(newPassword, 12);
|
|
160
176
|
// Update user password
|
|
161
177
|
await db.update(users).set({ password }).where(eq(users.id, entry.userId));
|
|
178
|
+
// Invalidate all existing sessions for this user so any attacker who had
|
|
179
|
+
// a compromised session can no longer use it after the password change.
|
|
180
|
+
await db.delete(sessions).where(eq(sessions.userId, entry.userId));
|
|
162
181
|
// Mark token as used (single-use enforcement)
|
|
163
182
|
await db
|
|
164
183
|
.update(passwordResetTokens)
|
|
@@ -179,25 +198,32 @@ export async function resetPasswordWithToken(token, newPassword) {
|
|
|
179
198
|
/**
|
|
180
199
|
* Invalidates a password reset token
|
|
181
200
|
*
|
|
182
|
-
*
|
|
201
|
+
* Uses O(1) lookup by token ID, then verifies the token hash before marking as used.
|
|
202
|
+
*
|
|
203
|
+
* @param tokenId - Token row ID (from the reset URL)
|
|
204
|
+
* @param token - Reset token (plain text, from the reset URL)
|
|
183
205
|
*/
|
|
184
|
-
export async function invalidatePasswordResetToken(token) {
|
|
206
|
+
export async function invalidatePasswordResetToken(tokenId, token) {
|
|
185
207
|
try {
|
|
186
208
|
const db = getClient();
|
|
187
|
-
//
|
|
188
|
-
const
|
|
209
|
+
// O(1) lookup by primary key
|
|
210
|
+
const [entry] = await db
|
|
189
211
|
.select()
|
|
190
212
|
.from(passwordResetTokens)
|
|
191
|
-
.where(and(gt(passwordResetTokens.expiresAt, new Date()), isNull(passwordResetTokens.usedAt)))
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
.where(and(eq(passwordResetTokens.id, tokenId), gt(passwordResetTokens.expiresAt, new Date()), isNull(passwordResetTokens.usedAt)))
|
|
214
|
+
.limit(1);
|
|
215
|
+
if (!entry) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Verify the token hash before invalidating
|
|
219
|
+
const expectedHash = hashToken(token, entry.tokenSalt);
|
|
220
|
+
const expectedBuf = Buffer.from(expectedHash);
|
|
221
|
+
const actualBuf = Buffer.from(entry.tokenHash);
|
|
222
|
+
if (expectedBuf.length === actualBuf.length && crypto.timingSafeEqual(expectedBuf, actualBuf)) {
|
|
223
|
+
await db
|
|
224
|
+
.update(passwordResetTokens)
|
|
225
|
+
.set({ usedAt: new Date() })
|
|
226
|
+
.where(eq(passwordResetTokens.id, entry.id));
|
|
201
227
|
}
|
|
202
228
|
}
|
|
203
229
|
catch (error) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses native fetch — no additional npm dependencies.
|
|
5
|
+
* Scopes: read:user user:email
|
|
6
|
+
*
|
|
7
|
+
* Note: GitHub may return null email if user has set it private.
|
|
8
|
+
* In that case we fetch from /user/emails and pick the primary verified one.
|
|
9
|
+
*/
|
|
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>;
|
|
13
|
+
export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
|
|
14
|
+
//# sourceMappingURL=github.d.ts.map
|
|
@@ -0,0 +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,CAAA;AAE/C,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,CA4BrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA2C1E"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses native fetch — no additional npm dependencies.
|
|
5
|
+
* Scopes: read:user user:email
|
|
6
|
+
*
|
|
7
|
+
* Note: GitHub may return null email if user has set it private.
|
|
8
|
+
* In that case we fetch from /user/emails and pick the primary verified one.
|
|
9
|
+
*/
|
|
10
|
+
export function buildAuthUrl(clientId, redirectUri, state) {
|
|
11
|
+
const url = new URL('https://github.com/login/oauth/authorize');
|
|
12
|
+
url.searchParams.set('client_id', clientId);
|
|
13
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
14
|
+
url.searchParams.set('scope', 'read:user user:email');
|
|
15
|
+
url.searchParams.set('state', state);
|
|
16
|
+
return url.toString();
|
|
17
|
+
}
|
|
18
|
+
export async function exchangeCode(code, redirectUri) {
|
|
19
|
+
const response = await fetch('https://github.com/login/oauth/access_token', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
23
|
+
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
24
|
+
Accept: 'application/json',
|
|
25
|
+
},
|
|
26
|
+
body: new URLSearchParams({
|
|
27
|
+
code,
|
|
28
|
+
client_id: process.env.GITHUB_CLIENT_ID ?? '',
|
|
29
|
+
client_secret: process.env.GITHUB_CLIENT_SECRET ?? '',
|
|
30
|
+
redirect_uri: redirectUri,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`GitHub token exchange failed: ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
const data = (await response.json());
|
|
37
|
+
if (data.error) {
|
|
38
|
+
throw new Error(`GitHub token exchange error: ${data.error}`);
|
|
39
|
+
}
|
|
40
|
+
if (!data.access_token || typeof data.access_token !== 'string') {
|
|
41
|
+
throw new Error('GitHub token exchange returned no access_token');
|
|
42
|
+
}
|
|
43
|
+
return data.access_token;
|
|
44
|
+
}
|
|
45
|
+
export async function fetchUser(accessToken) {
|
|
46
|
+
const headers = {
|
|
47
|
+
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
48
|
+
Authorization: `Bearer ${accessToken}`,
|
|
49
|
+
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
50
|
+
Accept: 'application/vnd.github+json',
|
|
51
|
+
};
|
|
52
|
+
const userResponse = await fetch('https://api.github.com/user', { headers });
|
|
53
|
+
if (!userResponse.ok) {
|
|
54
|
+
throw new Error(`GitHub user fetch failed: ${userResponse.status}`);
|
|
55
|
+
}
|
|
56
|
+
const user = (await userResponse.json());
|
|
57
|
+
let email = user.email ?? null;
|
|
58
|
+
// Fetch emails if not public
|
|
59
|
+
if (!email) {
|
|
60
|
+
const emailsResponse = await fetch('https://api.github.com/user/emails', { headers });
|
|
61
|
+
if (emailsResponse.ok) {
|
|
62
|
+
const emails = (await emailsResponse.json());
|
|
63
|
+
const primary = emails.find((e) => e.primary && e.verified);
|
|
64
|
+
email = primary?.email ?? null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
id: String(user.id),
|
|
69
|
+
email,
|
|
70
|
+
name: user.name ?? user.login,
|
|
71
|
+
avatarUrl: user.avatar_url ?? null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth 2.0 Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses native fetch — no additional npm dependencies.
|
|
5
|
+
* Scopes: openid email profile
|
|
6
|
+
*/
|
|
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>;
|
|
10
|
+
export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
|
|
11
|
+
//# sourceMappingURL=google.d.ts.map
|
|
@@ -0,0 +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,CAAA;AAE/C,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,CAsBrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAuB1E"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth 2.0 Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses native fetch — no additional npm dependencies.
|
|
5
|
+
* Scopes: openid email profile
|
|
6
|
+
*/
|
|
7
|
+
export function buildAuthUrl(clientId, redirectUri, state) {
|
|
8
|
+
const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
|
|
9
|
+
url.searchParams.set('client_id', clientId);
|
|
10
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
11
|
+
url.searchParams.set('response_type', 'code');
|
|
12
|
+
url.searchParams.set('scope', 'openid email profile');
|
|
13
|
+
url.searchParams.set('state', state);
|
|
14
|
+
url.searchParams.set('access_type', 'online');
|
|
15
|
+
return url.toString();
|
|
16
|
+
}
|
|
17
|
+
export async function exchangeCode(code, redirectUri) {
|
|
18
|
+
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
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
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Google token exchange failed: ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
const data = (await response.json());
|
|
33
|
+
if (!data.access_token || typeof data.access_token !== 'string') {
|
|
34
|
+
throw new Error('Google token exchange returned no access_token');
|
|
35
|
+
}
|
|
36
|
+
return data.access_token;
|
|
37
|
+
}
|
|
38
|
+
export async function fetchUser(accessToken) {
|
|
39
|
+
const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
|
|
40
|
+
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
41
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Google userinfo fetch failed: ${response.status}`);
|
|
45
|
+
}
|
|
46
|
+
const data = (await response.json());
|
|
47
|
+
return {
|
|
48
|
+
id: data.sub,
|
|
49
|
+
email: data.email ?? null,
|
|
50
|
+
name: data.name ?? 'Google User',
|
|
51
|
+
avatarUrl: data.picture ?? null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel OAuth Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses native fetch — no additional npm dependencies.
|
|
5
|
+
* No scopes required — Vercel uses full access by default.
|
|
6
|
+
*/
|
|
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>;
|
|
10
|
+
export declare function fetchUser(accessToken: string): Promise<ProviderUser>;
|
|
11
|
+
//# sourceMappingURL=vercel.d.ts.map
|
|
@@ -0,0 +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,CAAA;AAE/C,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,CAkBrF;AAED,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA2B1E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel OAuth Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses native fetch — no additional npm dependencies.
|
|
5
|
+
* No scopes required — Vercel uses full access by default.
|
|
6
|
+
*/
|
|
7
|
+
export function buildAuthUrl(clientId, redirectUri, state) {
|
|
8
|
+
const url = new URL('https://vercel.com/oauth/authorize');
|
|
9
|
+
url.searchParams.set('client_id', clientId);
|
|
10
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
11
|
+
url.searchParams.set('state', state);
|
|
12
|
+
return url.toString();
|
|
13
|
+
}
|
|
14
|
+
export async function exchangeCode(code, redirectUri) {
|
|
15
|
+
const response = await fetch('https://api.vercel.com/v2/oauth/access_token', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
18
|
+
body: new URLSearchParams({
|
|
19
|
+
code,
|
|
20
|
+
client_id: process.env.VERCEL_CLIENT_ID ?? '',
|
|
21
|
+
client_secret: process.env.VERCEL_CLIENT_SECRET ?? '',
|
|
22
|
+
redirect_uri: redirectUri,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Vercel token exchange failed: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
const data = (await response.json());
|
|
29
|
+
return data.access_token;
|
|
30
|
+
}
|
|
31
|
+
export async function fetchUser(accessToken) {
|
|
32
|
+
const response = await fetch('https://api.vercel.com/v2/user', {
|
|
33
|
+
// biome-ignore lint/style/useNamingConvention: HTTP header names are case-sensitive per RFC 7230
|
|
34
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Vercel user fetch failed: ${response.status}`);
|
|
38
|
+
}
|
|
39
|
+
const data = (await response.json());
|
|
40
|
+
const u = data.user;
|
|
41
|
+
return {
|
|
42
|
+
id: u.id,
|
|
43
|
+
email: u.email,
|
|
44
|
+
name: u.name ?? u.username ?? 'Vercel User',
|
|
45
|
+
avatarUrl: u.avatar ? `https://avatar.vercel.sh/${u.avatar}` : null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -62,17 +62,17 @@ export async function checkRateLimit(key, config = DEFAULT_CONFIG) {
|
|
|
62
62
|
};
|
|
63
63
|
// Check if blocked
|
|
64
64
|
if (config.blockDurationMs && currentEntry.count >= config.maxAttempts) {
|
|
65
|
-
const blockUntil =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
const blockUntil = now + config.blockDurationMs;
|
|
66
|
+
// Extend the storage TTL so the entry outlives the block period.
|
|
67
|
+
// Without this, the entry expires at resetAt (end of the rate-limit window)
|
|
68
|
+
// before the block expires, letting the user back in prematurely.
|
|
69
|
+
const blockTtlSeconds = Math.ceil(config.blockDurationMs / 1000);
|
|
70
|
+
await storage.set(storageKey, serializeEntry(currentEntry), blockTtlSeconds);
|
|
71
|
+
return {
|
|
72
|
+
allowed: false,
|
|
73
|
+
remaining: 0,
|
|
74
|
+
resetAt: blockUntil,
|
|
75
|
+
};
|
|
76
76
|
}
|
|
77
77
|
// Check if within window
|
|
78
78
|
if (currentEntry.count >= config.maxAttempts) {
|
package/dist/server/session.js
CHANGED
|
@@ -235,7 +235,7 @@ function extractSessionToken(cookieHeader) {
|
|
|
235
235
|
if (!sessionCookie) {
|
|
236
236
|
return null;
|
|
237
237
|
}
|
|
238
|
-
return sessionCookie.substring('revealui-session='.length);
|
|
238
|
+
return decodeURIComponent(sessionCookie.substring('revealui-session='.length));
|
|
239
239
|
}
|
|
240
240
|
/**
|
|
241
241
|
* Generate a secure random session token
|
|
@@ -13,5 +13,14 @@ export declare class DatabaseStorage implements Storage {
|
|
|
13
13
|
del(key: string): Promise<void>;
|
|
14
14
|
incr(key: string): Promise<number>;
|
|
15
15
|
exists(key: string): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Atomically read and update a value using a database transaction.
|
|
18
|
+
* Falls back to non-atomic get-then-set if transactions are unavailable
|
|
19
|
+
* (e.g., Neon HTTP mode in environments that don't support advisory locks).
|
|
20
|
+
*/
|
|
21
|
+
atomicUpdate(key: string, updater: (existing: string | null) => {
|
|
22
|
+
value: string;
|
|
23
|
+
ttlSeconds: number;
|
|
24
|
+
}): Promise<void>;
|
|
16
25
|
}
|
|
17
26
|
//# sourceMappingURL=database.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/server/storage/database.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAE7C,qBAAa,eAAgB,YAAW,OAAO;IAC7C,OAAO,CAAC,EAAE,CAAU;gBAER,gBAAgB,CAAC,EAAE,MAAM;IAkB/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAcxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBnE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/server/storage/database.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAE7C,qBAAa,eAAgB,YAAW,OAAO;IAC7C,OAAO,CAAC,EAAE,CAAU;gBAER,gBAAgB,CAAC,EAAE,MAAM;IAkB/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAcxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBnE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK3C;;;;OAIG;IACG,YAAY,CAChB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC1E,OAAO,CAAC,IAAI,CAAC;CAwBjB"}
|
|
@@ -69,4 +69,34 @@ export class DatabaseStorage {
|
|
|
69
69
|
const result = await this.get(key);
|
|
70
70
|
return result !== null;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Atomically read and update a value using a database transaction.
|
|
74
|
+
* Falls back to non-atomic get-then-set if transactions are unavailable
|
|
75
|
+
* (e.g., Neon HTTP mode in environments that don't support advisory locks).
|
|
76
|
+
*/
|
|
77
|
+
async atomicUpdate(key, updater) {
|
|
78
|
+
try {
|
|
79
|
+
await this.db.transaction(async (tx) => {
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const result = await tx.query.rateLimits.findFirst({
|
|
82
|
+
where: and(eq(rateLimits.key, key), gte(rateLimits.resetAt, now)),
|
|
83
|
+
});
|
|
84
|
+
const { value, ttlSeconds } = updater(result?.value ?? null);
|
|
85
|
+
const resetAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
86
|
+
await tx
|
|
87
|
+
.insert(rateLimits)
|
|
88
|
+
.values({ key, value, resetAt })
|
|
89
|
+
.onConflictDoUpdate({
|
|
90
|
+
target: rateLimits.key,
|
|
91
|
+
set: { value, resetAt, updatedAt: new Date() },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Transaction not supported (e.g., Neon HTTP serverless) — fall back to non-atomic
|
|
97
|
+
const existing = await this.get(key);
|
|
98
|
+
const { value, ttlSeconds } = updater(existing);
|
|
99
|
+
await this.set(key, value, ttlSeconds);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
72
102
|
}
|
|
@@ -12,6 +12,10 @@ export declare class InMemoryStorage implements Storage {
|
|
|
12
12
|
del(key: string): Promise<void>;
|
|
13
13
|
incr(key: string): Promise<number>;
|
|
14
14
|
exists(key: string): Promise<boolean>;
|
|
15
|
+
atomicUpdate(key: string, updater: (existing: string | null) => {
|
|
16
|
+
value: string;
|
|
17
|
+
ttlSeconds: number;
|
|
18
|
+
}): Promise<void>;
|
|
15
19
|
/**
|
|
16
20
|
* Clean up expired entries (should be called periodically)
|
|
17
21
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../../../src/server/storage/in-memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAO7C,qBAAa,eAAgB,YAAW,OAAO;IAC7C,OAAO,CAAC,KAAK,CAAuC;IAG9C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiBxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUnE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../../../src/server/storage/in-memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAO7C,qBAAa,eAAgB,YAAW,OAAO;IAC7C,OAAO,CAAC,KAAK,CAAuC;IAG9C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiBxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUnE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBrC,YAAY,CAChB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC1E,OAAO,CAAC,IAAI,CAAC;IAUhB;;OAEG;IACH,OAAO,IAAI,IAAI;IASf;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -50,6 +50,16 @@ export class InMemoryStorage {
|
|
|
50
50
|
}
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
54
|
+
async atomicUpdate(key, updater) {
|
|
55
|
+
// Read synchronously from the Map to avoid yielding to the event loop between
|
|
56
|
+
// read and write (JavaScript is single-threaded; no I/O = no interleaving).
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const entry = this.store.get(key);
|
|
59
|
+
const existing = entry && (!entry.expiresAt || entry.expiresAt >= now) ? entry.value : null;
|
|
60
|
+
const { value, ttlSeconds } = updater(existing);
|
|
61
|
+
this.store.set(key, { value, expiresAt: now + ttlSeconds * 1000 });
|
|
62
|
+
}
|
|
53
63
|
/**
|
|
54
64
|
* Clean up expired entries (should be called periodically)
|
|
55
65
|
*/
|
|
@@ -32,5 +32,15 @@ export interface Storage {
|
|
|
32
32
|
* Set multiple key-value pairs
|
|
33
33
|
*/
|
|
34
34
|
mset?(pairs: Array<[string, string]>, ttlSeconds?: number): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Atomically read and update a value.
|
|
37
|
+
* The updater receives the current value (or null) and returns the new value + TTL.
|
|
38
|
+
* Implementations that support DB transactions should do so; others fall back to
|
|
39
|
+
* a non-atomic get-then-set, which is safe for low-concurrency scenarios.
|
|
40
|
+
*/
|
|
41
|
+
atomicUpdate?(key: string, updater: (existing: string | null) => {
|
|
42
|
+
value: string;
|
|
43
|
+
ttlSeconds: number;
|
|
44
|
+
}): Promise<void>;
|
|
35
45
|
}
|
|
36
46
|
//# sourceMappingURL=interface.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/server/storage/interface.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,OAAO;IACtB;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAExC;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnE;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE/B;;OAEG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAElC;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAErC;;OAEG;IACH,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;IAEjD;;OAEG;IACH,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/server/storage/interface.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,OAAO;IACtB;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAExC;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnE;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE/B;;OAEG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAElC;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAErC;;OAEG;IACH,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;IAEjD;;OAEG;IACH,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEzE;;;;;OAKG;IACH,YAAY,CAAC,CACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC1E,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB"}
|
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,9 @@ export interface User {
|
|
|
22
22
|
agentModel: string | null;
|
|
23
23
|
agentCapabilities: string[] | null;
|
|
24
24
|
agentConfig: unknown;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
emailVerificationToken: string | null;
|
|
27
|
+
emailVerifiedAt: Date | null;
|
|
25
28
|
preferences: unknown;
|
|
26
29
|
createdAt: Date;
|
|
27
30
|
updatedAt: Date;
|