@devmoods/express-extras 0.73.0 → 0.74.0
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/auth/emailAndPassword.js +14 -8
- package/dist/auth/google.d.ts +2 -1
- package/dist/auth/google.js +14 -6
- package/dist/auth/passwordless.d.ts +1 -1
- package/dist/auth/passwordless.js +23 -11
- package/dist/auth/token.js +2 -1
- package/dist/auth/tokenStorage.d.ts +1 -1
- package/dist/auth/tokenStorage.js +1 -1
- package/dist/auth/types.d.ts +6 -2
- package/dist/auth.d.ts +3 -3
- package/dist/auth.js +50 -10
- package/dist/cache.d.ts +26 -5
- package/dist/cache.js +129 -66
- package/dist/composeMiddleware.js +2 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/csrf.js +4 -0
- package/dist/listen.js +1 -2
- package/dist/logger.js +2 -2
- package/dist/manage.js +3 -3
- package/dist/middleware.js +29 -2
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js +0 -1
- package/dist/vite/ssr.js +1 -1
- package/package.json +8 -7
|
@@ -6,7 +6,7 @@ import {} from './types.js';
|
|
|
6
6
|
import { Unauthorized } from '../errors.js';
|
|
7
7
|
import { getLogger } from '../logger.js';
|
|
8
8
|
import { jsonSchema } from '../validate.js';
|
|
9
|
-
const passwordSchema = { type: 'string', minLength:
|
|
9
|
+
const passwordSchema = { type: 'string', minLength: 8 };
|
|
10
10
|
const emailSchema = { type: 'string', minLength: 4 };
|
|
11
11
|
const logger = getLogger('auth');
|
|
12
12
|
export function EmailAndPasswordAuthProvider(options) {
|
|
@@ -35,7 +35,7 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
35
35
|
catch (error) {
|
|
36
36
|
logger.error(error);
|
|
37
37
|
await context.jitter(timer.getDuration());
|
|
38
|
-
throw Unauthorized(
|
|
38
|
+
throw Unauthorized('Invalid email or password', { cause: error });
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
router.post('/change-password', {
|
|
@@ -73,11 +73,18 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
73
73
|
}, async (req) => {
|
|
74
74
|
const { email } = req.body;
|
|
75
75
|
const timer = startTimer();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
try {
|
|
77
|
+
const user = await options.login(email, skipPasswordCheck);
|
|
78
|
+
const temporaryToken = await context.createTemporaryToken(user);
|
|
79
|
+
await options.onEmailVerificationRequest?.(user, temporaryToken);
|
|
80
|
+
await context.jitter(timer.getDuration());
|
|
81
|
+
return 202;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error(error);
|
|
85
|
+
await context.jitter(timer.getDuration());
|
|
86
|
+
return 202;
|
|
87
|
+
}
|
|
81
88
|
});
|
|
82
89
|
router.post('/verify-email', {
|
|
83
90
|
request: jsonSchema({
|
|
@@ -93,7 +100,6 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
93
100
|
}
|
|
94
101
|
catch (error) {
|
|
95
102
|
logger.warn('failed to verify token', {
|
|
96
|
-
token,
|
|
97
103
|
user: context.filterUser(user),
|
|
98
104
|
error: error.message,
|
|
99
105
|
});
|
package/dist/auth/google.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ interface GoogleAuthProviderOptions<UserModel = AnyUser> {
|
|
|
7
7
|
clientId?: string;
|
|
8
8
|
clientSecret?: string;
|
|
9
9
|
redirectUri?: string;
|
|
10
|
+
hostedDomain?: string;
|
|
10
11
|
getOrCreateUserFromUserIdentifier(userIdentifier: string, profile: GoogleProfile): Promise<UserModel>;
|
|
11
12
|
}
|
|
12
|
-
export declare function GoogleAuthProvider<UserModel extends AnyUser>({ clientId, clientSecret, redirectUri, getOrCreateUserFromUserIdentifier }: GoogleAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
13
|
+
export declare function GoogleAuthProvider<UserModel extends AnyUser>({ clientId, clientSecret, redirectUri, hostedDomain, getOrCreateUserFromUserIdentifier }: GoogleAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
13
14
|
export {};
|
package/dist/auth/google.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { config } from '@devmoods/config';
|
|
2
2
|
import { type } from 'arktype';
|
|
3
3
|
import { OAuth2Client } from 'google-auth-library';
|
|
4
|
-
import { BadRequest, Unauthorized } from '../errors.js';
|
|
5
|
-
import { getLogger } from '../logger.js';
|
|
6
4
|
import { createTokenResponse } from './token.js';
|
|
7
5
|
import {} from './types.js';
|
|
6
|
+
import { BadRequest, Unauthorized } from '../errors.js';
|
|
7
|
+
import { getLogger } from '../logger.js';
|
|
8
8
|
const logger = getLogger('auth');
|
|
9
|
-
export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, clientSecret = config.GOOGLE_AUTH_CLIENT_SECRET, redirectUri = `${config.PUBLIC_URL}/api/v1/auth/google/callback`, getOrCreateUserFromUserIdentifier, }) {
|
|
9
|
+
export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, clientSecret = config.GOOGLE_AUTH_CLIENT_SECRET, redirectUri = `${config.PUBLIC_URL}/api/v1/auth/google/callback`, hostedDomain, getOrCreateUserFromUserIdentifier, }) {
|
|
10
10
|
const client = new OAuth2Client({
|
|
11
11
|
clientId,
|
|
12
12
|
clientSecret,
|
|
@@ -79,10 +79,18 @@ export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, cl
|
|
|
79
79
|
throw Unauthorized('Invalid id token');
|
|
80
80
|
}
|
|
81
81
|
const payload = await verifyIdToken(idToken);
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if (!payload.sub) {
|
|
83
|
+
throw Unauthorized('Token from Google is missing subject');
|
|
84
|
+
}
|
|
85
|
+
if (!payload.email || payload.email_verified !== true) {
|
|
86
|
+
throw Unauthorized('Token from Google contains unverified email');
|
|
87
|
+
}
|
|
88
|
+
if (hostedDomain && payload.hd !== hostedDomain) {
|
|
89
|
+
throw Unauthorized('Token from Google contains invalid hosted domain');
|
|
90
|
+
}
|
|
91
|
+
const user = await getOrCreateUserFromUserIdentifier(payload.sub, {
|
|
84
92
|
email: payload.email,
|
|
85
|
-
fullName: payload.name,
|
|
93
|
+
fullName: payload.name ?? '',
|
|
86
94
|
});
|
|
87
95
|
const accessToken = await context.login(req, res, user);
|
|
88
96
|
logger.info('google-login', { email: user.email });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AnyUser, type AuthProvider } from './types.js';
|
|
2
2
|
export interface PasswordlessAuthProviderOptions<UserModel = AnyUser> {
|
|
3
3
|
getUserByEmail(email: string): Promise<UserModel | null>;
|
|
4
|
-
onLoginRequest(user: UserModel, temporaryToken: string, otp: string): Promise<void>;
|
|
4
|
+
onLoginRequest(user: UserModel, challengeId: string, temporaryToken: string, otp: string): Promise<void>;
|
|
5
5
|
}
|
|
6
6
|
export declare function PasswordlessAuthProvider<UserModel extends AnyUser>(options: PasswordlessAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
1
2
|
import { DoesNotExistError } from '@devmoods/postgres';
|
|
2
3
|
import { createTokenResponse } from './token.js';
|
|
3
4
|
import {} from './types.js';
|
|
5
|
+
import { BadRequest } from '../errors.js';
|
|
4
6
|
import { getLogger } from '../logger.js';
|
|
5
7
|
import { jsonSchema } from '../validate.js';
|
|
6
8
|
const logger = getLogger('auth');
|
|
@@ -25,20 +27,30 @@ export function PasswordlessAuthProvider(options) {
|
|
|
25
27
|
type: 'object',
|
|
26
28
|
properties: {
|
|
27
29
|
type: { type: 'string', enum: ['otp', 'token'] },
|
|
30
|
+
challengeId: { type: 'string' },
|
|
28
31
|
otp: { type: 'string' },
|
|
29
32
|
token: { type: 'string' },
|
|
30
33
|
},
|
|
31
34
|
required: ['type'],
|
|
32
35
|
}),
|
|
33
36
|
}, async (req, res) => {
|
|
34
|
-
const { token: temporaryToken, otp, type: tokenType } = req.body;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const { challengeId, token: temporaryToken, otp, type: tokenType, } = req.body;
|
|
38
|
+
if (tokenType === 'otp' && (!challengeId || !otp)) {
|
|
39
|
+
throw BadRequest('OTP logins require challengeId and otp');
|
|
40
|
+
}
|
|
41
|
+
if (tokenType === 'token' && !temporaryToken) {
|
|
42
|
+
throw BadRequest('Token logins require token');
|
|
43
|
+
}
|
|
44
|
+
const uid = tokenType === 'otp'
|
|
45
|
+
? await context.verifyOtp(String(challengeId), String(otp), null, false)
|
|
46
|
+
: await context.verifyTemporaryToken(String(temporaryToken), null, false);
|
|
40
47
|
const user = await context.getUserById(uid);
|
|
41
|
-
|
|
48
|
+
if (tokenType === 'otp') {
|
|
49
|
+
await context.verifyOtp(String(challengeId), String(otp), user);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await context.verifyTemporaryToken(String(temporaryToken), user);
|
|
53
|
+
}
|
|
42
54
|
const accessToken = await context.login(req, res, user);
|
|
43
55
|
logger.info('passwordless login', { tokenType, uid: user.id });
|
|
44
56
|
if (req.accepts('html')) {
|
|
@@ -62,13 +74,13 @@ export function PasswordlessAuthProvider(options) {
|
|
|
62
74
|
const user = await getUserOrNull(email);
|
|
63
75
|
if (!user) {
|
|
64
76
|
logger.warn('passwordless: user not found', { email });
|
|
65
|
-
return 202;
|
|
77
|
+
return [202, { challengeId: crypto.randomUUID() }];
|
|
66
78
|
}
|
|
67
79
|
const token = await context.createTemporaryToken(user);
|
|
68
|
-
const otp = await context.createOtp(user);
|
|
69
|
-
await options.onLoginRequest(user, token, otp);
|
|
80
|
+
const { challengeId, otp } = await context.createOtp(user);
|
|
81
|
+
await options.onLoginRequest(user, challengeId, token, otp);
|
|
70
82
|
logger.info('passwordless login requested', { email });
|
|
71
|
-
return 202;
|
|
83
|
+
return [202, { challengeId }];
|
|
72
84
|
});
|
|
73
85
|
},
|
|
74
86
|
};
|
package/dist/auth/token.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { config } from '@devmoods/config';
|
|
2
3
|
import {} from 'express';
|
|
3
4
|
import { SignJWT, jwtVerify } from 'jose';
|
|
@@ -27,7 +28,7 @@ export function createTokenResponse(accessToken) {
|
|
|
27
28
|
export async function createAccessToken(payload, version = '1', secret = config.AUTH_JWT_SECRET) {
|
|
28
29
|
const expiresIn = config.AUTH_TOKEN_EXPIRES;
|
|
29
30
|
const secretKey = new TextEncoder().encode(secret);
|
|
30
|
-
const token = await new SignJWT({ v: version, ...payload })
|
|
31
|
+
const token = await new SignJWT({ jti: randomUUID(), v: version, ...payload })
|
|
31
32
|
.setProtectedHeader({ alg: 'HS256' })
|
|
32
33
|
.setExpirationTime(`${expiresIn}s`)
|
|
33
34
|
.setIssuedAt(new Date())
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export interface DecodedTemporaryToken {
|
|
|
31
31
|
};
|
|
32
32
|
signature: string;
|
|
33
33
|
}
|
|
34
|
+
export interface OtpChallenge {
|
|
35
|
+
challengeId: string;
|
|
36
|
+
otp: string;
|
|
37
|
+
}
|
|
34
38
|
export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
35
39
|
filterUser: AuthOptions<UserModel>['filterUser'];
|
|
36
40
|
tokenStorage: AuthOptions<UserModel>['tokenStorage'];
|
|
@@ -38,8 +42,8 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
38
42
|
createTemporaryToken(user: UserModel | null): Promise<string>;
|
|
39
43
|
verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
40
44
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
41
|
-
createOtp(user: UserModel): Promise<
|
|
42
|
-
verifyOtp(otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
45
|
+
createOtp(user: UserModel): Promise<OtpChallenge>;
|
|
46
|
+
verifyOtp(challengeId: string, otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
43
47
|
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
44
48
|
login: (req: Request, res: Response, user: UserModel, isRefresh?: boolean) => Promise<AccessToken>;
|
|
45
49
|
getUserById(id: string): Promise<UserModel>;
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RequestHandler, type Response, type Request, type Router } from 'express';
|
|
2
|
-
import { type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
|
|
2
|
+
import { type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken, type OtpChallenge } from './auth/types.js';
|
|
3
3
|
export * from './auth/types.js';
|
|
4
4
|
export * from './auth/emailAndPassword.js';
|
|
5
5
|
export * from './auth/passwordless.js';
|
|
@@ -15,8 +15,8 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
15
15
|
createTemporaryToken(user: UserModel | null): Promise<string>;
|
|
16
16
|
verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
17
17
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
18
|
-
createOtp(user: UserModel): Promise<
|
|
19
|
-
verifyOtp(otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
18
|
+
createOtp(user: UserModel): Promise<OtpChallenge>;
|
|
19
|
+
verifyOtp(challengeId: string, otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
20
20
|
logout: (req: Request, res: Response) => Promise<void>;
|
|
21
21
|
setSessionTokenVersion: (version: string) => void;
|
|
22
22
|
}
|
package/dist/auth.js
CHANGED
|
@@ -17,14 +17,27 @@ export * from './auth/token.js';
|
|
|
17
17
|
export * from './auth/google.js';
|
|
18
18
|
export * from './auth/tokenStorage.js';
|
|
19
19
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
20
|
+
const hashEquals = (left, right) => {
|
|
21
|
+
const leftBuffer = Buffer.from(left, 'hex');
|
|
22
|
+
const rightBuffer = Buffer.from(right, 'hex');
|
|
23
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return crypto.timingSafeEqual(leftBuffer, rightBuffer);
|
|
27
|
+
};
|
|
20
28
|
export function createAuth({ getUserById, filterUser, tokenStorage, providers, loginUrl = '/', onUserLoggedIn, sessionTokenVersion: initialSessionTokenVersion = '1', }) {
|
|
21
29
|
let sessionTokenVersion = initialSessionTokenVersion;
|
|
22
30
|
const useCurrentUser = () => useRequestContext('currentUser');
|
|
23
31
|
const setCurrentUser = (user) => setRequestContext('currentUser', user);
|
|
24
32
|
const login = async (req, res, user, isRefresh = false) => {
|
|
33
|
+
const previousToken = isRefresh ? extractAccessTokenFromRequest(req) : null;
|
|
34
|
+
const previousSessionId = previousToken ? sha256(previousToken) : null;
|
|
25
35
|
const accessToken = await createAccessToken({ uid: user.id }, sessionTokenVersion);
|
|
26
36
|
const sessionId = sha256(accessToken.token);
|
|
27
37
|
await tokenStorage.set(sessionId, String(user.id), accessToken.expiresIn);
|
|
38
|
+
if (previousSessionId && previousSessionId !== sessionId) {
|
|
39
|
+
await tokenStorage.invalidate(previousSessionId);
|
|
40
|
+
}
|
|
28
41
|
setAccessTokenCookie(res, accessToken.token);
|
|
29
42
|
setCurrentUser(user);
|
|
30
43
|
if (!isRefresh) {
|
|
@@ -161,7 +174,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
161
174
|
if (req.accepts('html')) {
|
|
162
175
|
return res.redirect(loginUrl);
|
|
163
176
|
}
|
|
164
|
-
return next(Unauthorized(
|
|
177
|
+
return next(Unauthorized('Invalid token', { cause: error }));
|
|
165
178
|
}
|
|
166
179
|
/* v8 ignore next -- @preserve */
|
|
167
180
|
next(error);
|
|
@@ -171,6 +184,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
171
184
|
function protect(isAuthorized) {
|
|
172
185
|
return async function authorizationMiddleware(req, res, next) {
|
|
173
186
|
try {
|
|
187
|
+
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
174
188
|
const user = useCurrentUser();
|
|
175
189
|
if (!user) {
|
|
176
190
|
throw new InvalidToken('User not logged in');
|
|
@@ -202,22 +216,48 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
202
216
|
}
|
|
203
217
|
async function createOtp(user) {
|
|
204
218
|
const otp = crypto.randomInt(100000, 999999).toString();
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
const challengeId = crypto.randomUUID();
|
|
220
|
+
const otpHash = sha256(otp);
|
|
221
|
+
await tokenStorage.set(`otp:${challengeId}`, JSON.stringify({
|
|
222
|
+
uid: String(user.id),
|
|
223
|
+
otpHash,
|
|
224
|
+
attempts: 0,
|
|
225
|
+
maxAttempts: config.AUTH_OTP_MAX_ATTEMPTS,
|
|
226
|
+
}), config.AUTH_OTP_EXPIRES);
|
|
227
|
+
return { challengeId, otp };
|
|
207
228
|
}
|
|
208
|
-
async function verifyOtp(otp, user, consume = true) {
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
229
|
+
async function verifyOtp(challengeId, otp, user, consume = true) {
|
|
230
|
+
const storedChallenge = await tokenStorage.get(`otp:${challengeId}`);
|
|
231
|
+
if (!storedChallenge) {
|
|
211
232
|
throw new InvalidToken('Invalid OTP');
|
|
212
233
|
}
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
let parsedChallenge;
|
|
235
|
+
try {
|
|
236
|
+
parsedChallenge = JSON.parse(storedChallenge);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
await tokenStorage.invalidate(`otp:${challengeId}`);
|
|
240
|
+
throw new InvalidToken('Invalid OTP', { cause: error });
|
|
241
|
+
}
|
|
242
|
+
if (user && parsedChallenge.uid !== String(user.id)) {
|
|
243
|
+
throw new InvalidToken('Invalid OTP');
|
|
244
|
+
}
|
|
245
|
+
const attemptedOtpHash = sha256(otp);
|
|
246
|
+
const matches = hashEquals(parsedChallenge.otpHash, attemptedOtpHash);
|
|
247
|
+
if (!matches) {
|
|
248
|
+
const attempts = parsedChallenge.attempts + 1;
|
|
249
|
+
if (attempts >= parsedChallenge.maxAttempts) {
|
|
250
|
+
await tokenStorage.invalidate(`otp:${challengeId}`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
await tokenStorage.set(`otp:${challengeId}`, JSON.stringify({ ...parsedChallenge, attempts }), config.AUTH_OTP_EXPIRES);
|
|
254
|
+
}
|
|
215
255
|
throw new InvalidToken('Invalid OTP');
|
|
216
256
|
}
|
|
217
257
|
if (consume) {
|
|
218
|
-
await tokenStorage.invalidate(otp);
|
|
258
|
+
await tokenStorage.invalidate(`otp:${challengeId}`);
|
|
219
259
|
}
|
|
220
|
-
return
|
|
260
|
+
return parsedChallenge.uid;
|
|
221
261
|
}
|
|
222
262
|
/**
|
|
223
263
|
* Create a short-lived token tied to a user that can be
|
package/dist/cache.d.ts
CHANGED
|
@@ -3,16 +3,37 @@ export interface CacheOptions {
|
|
|
3
3
|
key?: string;
|
|
4
4
|
expiresMs?: number | null;
|
|
5
5
|
}
|
|
6
|
-
export
|
|
7
|
-
get(key: string): Promise<any>;
|
|
8
|
-
set(key: string, value: any, expires?: number | null | undefined): Promise<void>;
|
|
9
|
-
clear(key?: string): Promise<void>;
|
|
6
|
+
export declare abstract class CacheAdapter {
|
|
7
|
+
abstract get(key: string): Promise<any>;
|
|
8
|
+
abstract set(key: string, value: any, expires?: number | null | undefined): Promise<void>;
|
|
9
|
+
abstract clear(key?: string): Promise<void>;
|
|
10
10
|
}
|
|
11
11
|
export interface RedisCacheAdapterOptions {
|
|
12
12
|
keyPrefix?: string;
|
|
13
13
|
}
|
|
14
|
+
export interface MemoryCacheAdapterOptions {
|
|
15
|
+
maxSize?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class RedisCacheAdapter extends CacheAdapter {
|
|
18
|
+
redis: RedisClient;
|
|
19
|
+
keyPrefix: string;
|
|
20
|
+
constructor(redis: RedisClient, { keyPrefix }?: RedisCacheAdapterOptions);
|
|
21
|
+
get(key: string): Promise<any>;
|
|
22
|
+
set(key: string, value: unknown, expires?: number | null): Promise<void>;
|
|
23
|
+
clear(key?: string): Promise<void>;
|
|
24
|
+
private withKeyPrefix;
|
|
25
|
+
}
|
|
14
26
|
export declare function createRedisCacheAdapter(redis: RedisClient, options?: RedisCacheAdapterOptions): CacheAdapter;
|
|
15
|
-
export declare
|
|
27
|
+
export declare class MemoryCacheAdapter extends CacheAdapter {
|
|
28
|
+
#private;
|
|
29
|
+
maxSize?: number;
|
|
30
|
+
constructor({ maxSize }?: MemoryCacheAdapterOptions);
|
|
31
|
+
get(key: string): Promise<unknown>;
|
|
32
|
+
set(key: string, value: unknown, expires?: number): Promise<void>;
|
|
33
|
+
clear(key?: string): Promise<void>;
|
|
34
|
+
private evictLeastRecentlyUsedEntries;
|
|
35
|
+
}
|
|
36
|
+
export declare function createMemoryCacheAdapter(options?: MemoryCacheAdapterOptions): CacheAdapter;
|
|
16
37
|
export type Cache = ReturnType<typeof createCache>;
|
|
17
38
|
export declare function createCache(adapter: CacheAdapter): {
|
|
18
39
|
set: (key: string, value: any, expires?: number | null | undefined) => Promise<void>;
|
package/dist/cache.js
CHANGED
|
@@ -1,62 +1,116 @@
|
|
|
1
1
|
import { config } from '@devmoods/config';
|
|
2
2
|
import {} from './redis.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
const MAX_CACHE_KEY_SIZE_BYTES = 256;
|
|
4
|
+
export class CacheAdapter {
|
|
5
|
+
}
|
|
6
|
+
export class RedisCacheAdapter extends CacheAdapter {
|
|
7
|
+
redis;
|
|
8
|
+
keyPrefix;
|
|
9
|
+
constructor(redis, { keyPrefix = `${config.REDIS_KEY_PREFIX}cache:`, } = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.redis = redis;
|
|
12
|
+
this.keyPrefix = keyPrefix;
|
|
13
|
+
}
|
|
14
|
+
async get(key) {
|
|
15
|
+
const value = await this.redis.get(this.withKeyPrefix(key));
|
|
16
|
+
if (value == null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(value);
|
|
20
|
+
}
|
|
21
|
+
async set(key, value, expires) {
|
|
22
|
+
const serializedValue = JSON.stringify(value);
|
|
23
|
+
if (expires == null) {
|
|
24
|
+
await this.redis.set(this.withKeyPrefix(key), serializedValue);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await this.redis.set(this.withKeyPrefix(key), serializedValue, {
|
|
28
|
+
PX: expires,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async clear(key) {
|
|
32
|
+
if (key) {
|
|
33
|
+
await this.redis.del(this.withKeyPrefix(key));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let cursor = '0';
|
|
37
|
+
const pattern = `${this.keyPrefix}*`;
|
|
38
|
+
do {
|
|
39
|
+
const result = await this.redis.scan(cursor, {
|
|
40
|
+
MATCH: pattern,
|
|
41
|
+
COUNT: 100,
|
|
17
42
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
await redis.
|
|
22
|
-
}
|
|
23
|
-
const keys = await redis.keys(`${keyPrefix}*`);
|
|
24
|
-
for (const existingKey of keys) {
|
|
25
|
-
await redis.del(existingKey);
|
|
43
|
+
cursor = result.cursor;
|
|
44
|
+
/* v8 ignore next -- @preserve */
|
|
45
|
+
if (result.keys.length > 0) {
|
|
46
|
+
await this.redis.unlink(result.keys);
|
|
26
47
|
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
48
|
+
} while (cursor !== '0');
|
|
49
|
+
}
|
|
50
|
+
withKeyPrefix(key) {
|
|
51
|
+
return `${this.keyPrefix}${key}`;
|
|
52
|
+
}
|
|
29
53
|
}
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
export function createRedisCacheAdapter(redis, options = {}) {
|
|
55
|
+
return new RedisCacheAdapter(redis, options);
|
|
56
|
+
}
|
|
57
|
+
export class MemoryCacheAdapter extends CacheAdapter {
|
|
58
|
+
maxSize;
|
|
59
|
+
#cache = new Map();
|
|
60
|
+
constructor({ maxSize } = {}) {
|
|
61
|
+
super();
|
|
62
|
+
this.maxSize = maxSize;
|
|
63
|
+
}
|
|
64
|
+
async get(key) {
|
|
65
|
+
const entry = this.#cache.get(key);
|
|
66
|
+
if (entry == null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const [value, expires] = entry;
|
|
70
|
+
if (expires != null && Date.now() >= expires) {
|
|
71
|
+
this.#cache.delete(key);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// Mark as recently used by deleting the entry and adding it back to the cache
|
|
75
|
+
this.#cache.delete(key);
|
|
76
|
+
this.#cache.set(key, entry);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
async set(key, value, expires) {
|
|
80
|
+
this.#cache.delete(key);
|
|
81
|
+
this.#cache.set(key, [
|
|
82
|
+
value,
|
|
83
|
+
expires == null ? null : Date.now() + expires,
|
|
84
|
+
]);
|
|
85
|
+
this.evictLeastRecentlyUsedEntries();
|
|
86
|
+
}
|
|
87
|
+
async clear(key) {
|
|
88
|
+
if (key) {
|
|
89
|
+
this.#cache.delete(key);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.#cache.clear();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
evictLeastRecentlyUsedEntries() {
|
|
96
|
+
if (this.maxSize == null) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
while (this.#cache.size > this.maxSize) {
|
|
100
|
+
const oldestKey = this.#cache.keys().next().value;
|
|
101
|
+
if (oldestKey == null) {
|
|
102
|
+
return;
|
|
54
103
|
}
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
this.#cache.delete(oldestKey);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function createMemoryCacheAdapter(options = {}) {
|
|
109
|
+
return new MemoryCacheAdapter(options);
|
|
57
110
|
}
|
|
58
111
|
export function createCache(adapter) {
|
|
59
112
|
async function cached(key, options, fn) {
|
|
113
|
+
assertCacheKeySize(key);
|
|
60
114
|
const cachedValue = await adapter.get(key);
|
|
61
115
|
if (cachedValue != null) {
|
|
62
116
|
return cachedValue;
|
|
@@ -73,7 +127,9 @@ export function createCache(adapter) {
|
|
|
73
127
|
const baseKey = typeof options === 'object' && options != null
|
|
74
128
|
? options.key
|
|
75
129
|
: undefined;
|
|
76
|
-
|
|
130
|
+
const cacheKey = `${baseKey ?? Buffer.from(fn.toString()).toString('hex')}-${hashKey(args)}`;
|
|
131
|
+
assertCacheKeySize(cacheKey);
|
|
132
|
+
return cacheKey;
|
|
77
133
|
};
|
|
78
134
|
const wrapped = async (...args) => {
|
|
79
135
|
const cacheKey = getCacheKey(...args);
|
|
@@ -101,21 +157,28 @@ export function createCache(adapter) {
|
|
|
101
157
|
};
|
|
102
158
|
}
|
|
103
159
|
function hashKey(obj) {
|
|
104
|
-
return JSON.stringify(obj, (
|
|
105
|
-
if (value
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
if (typeof value === 'object') {
|
|
109
|
-
if (Array.isArray(value)) {
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
return Object.keys(value)
|
|
113
|
-
.sort()
|
|
114
|
-
.reduce((result, item) => {
|
|
115
|
-
result[item] = value[item];
|
|
116
|
-
return result;
|
|
117
|
-
}, {});
|
|
160
|
+
return JSON.stringify(obj, (_, value) => {
|
|
161
|
+
if (!isPlainObject(value)) {
|
|
162
|
+
return value;
|
|
118
163
|
}
|
|
119
|
-
return value
|
|
164
|
+
return Object.keys(value)
|
|
165
|
+
.sort()
|
|
166
|
+
.reduce((result, key) => {
|
|
167
|
+
result[key] = value[key];
|
|
168
|
+
return result;
|
|
169
|
+
}, {});
|
|
120
170
|
});
|
|
121
171
|
}
|
|
172
|
+
function assertCacheKeySize(key) {
|
|
173
|
+
if (Buffer.byteLength(key, 'utf8') <= MAX_CACHE_KEY_SIZE_BYTES) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`Cache key exceeds ${MAX_CACHE_KEY_SIZE_BYTES} bytes. Provide a shorter explicit key or reduce the size of the cache arguments.`);
|
|
177
|
+
}
|
|
178
|
+
function isPlainObject(value) {
|
|
179
|
+
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const prototype = Object.getPrototypeOf(value);
|
|
183
|
+
return prototype === null || prototype === Object.prototype;
|
|
184
|
+
}
|
|
@@ -20,14 +20,14 @@ function generate(handlers) {
|
|
|
20
20
|
const stack = handlers.flat();
|
|
21
21
|
return function middleware(err, req, res, done) {
|
|
22
22
|
let index = -1;
|
|
23
|
-
//
|
|
23
|
+
// oxlint-disable-next-line @typescript-eslint/no-shadow
|
|
24
24
|
function dispatch(pos, err) {
|
|
25
25
|
const handler = stack[pos];
|
|
26
26
|
index = pos;
|
|
27
27
|
if (index === stack.length) {
|
|
28
28
|
return done(err);
|
|
29
29
|
}
|
|
30
|
-
//
|
|
30
|
+
// oxlint-disable-next-line @typescript-eslint/no-shadow
|
|
31
31
|
function next(err) {
|
|
32
32
|
/* v8 ignore next -- @preserve */
|
|
33
33
|
if (pos < index) {
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ declare const app: Defs<{
|
|
|
9
9
|
AUTH_TOKEN_EXPIRES: number;
|
|
10
10
|
AUTH_TEMP_TOKEN_EXPIRES: number;
|
|
11
11
|
AUTH_OTP_EXPIRES: number;
|
|
12
|
+
AUTH_OTP_MAX_ATTEMPTS: number;
|
|
12
13
|
AUTH_JWT_SECRET: string;
|
|
13
14
|
AUTH_COOKIE_SAME_SITE: string;
|
|
14
15
|
REDIS_URL: string;
|
|
@@ -41,6 +42,7 @@ export declare const config: import("@devmoods/config").Defs<{
|
|
|
41
42
|
AUTH_TOKEN_EXPIRES: number;
|
|
42
43
|
AUTH_TEMP_TOKEN_EXPIRES: number;
|
|
43
44
|
AUTH_OTP_EXPIRES: number;
|
|
45
|
+
AUTH_OTP_MAX_ATTEMPTS: number;
|
|
44
46
|
AUTH_JWT_SECRET: string;
|
|
45
47
|
AUTH_COOKIE_SAME_SITE: string;
|
|
46
48
|
REDIS_URL: string;
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ const app = define({
|
|
|
7
7
|
AUTH_TOKEN_EXPIRES: t.number(3600 * 24 * 30),
|
|
8
8
|
AUTH_TEMP_TOKEN_EXPIRES: t.number(3600),
|
|
9
9
|
AUTH_OTP_EXPIRES: t.number(5 * 60),
|
|
10
|
+
AUTH_OTP_MAX_ATTEMPTS: t.number(5),
|
|
10
11
|
AUTH_JWT_SECRET: t.string(),
|
|
11
12
|
AUTH_COOKIE_SAME_SITE: t.enum(['strict', 'lax', 'none'], 'strict'),
|
|
12
13
|
REDIS_URL: t.string(),
|
package/dist/csrf.js
CHANGED
|
@@ -43,6 +43,7 @@ export function useCsrfOptions() {
|
|
|
43
43
|
}
|
|
44
44
|
const generateTokenAndHash = (req, { overwrite = false } = {}) => {
|
|
45
45
|
const secret = getCsrfSecret();
|
|
46
|
+
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
46
47
|
const { sessionIdentifier, cookieName } = useCsrfOptions();
|
|
47
48
|
const csrfCookie = req.cookies[cookieName];
|
|
48
49
|
if (typeof csrfCookie === 'string' && !overwrite) {
|
|
@@ -66,6 +67,7 @@ const generateTokenAndHash = (req, { overwrite = false } = {}) => {
|
|
|
66
67
|
export const generateCsrfToken = (req, res, options = {}) => {
|
|
67
68
|
const { csrfToken, csrfTokenHash } = generateTokenAndHash(req, options);
|
|
68
69
|
const cookieContent = `${csrfToken}|${csrfTokenHash}`;
|
|
70
|
+
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
69
71
|
const { cookieName } = useCsrfOptions();
|
|
70
72
|
res.cookie(cookieName, cookieContent, {
|
|
71
73
|
sameSite: 'lax',
|
|
@@ -85,6 +87,7 @@ const validateTokenHash = ({ csrfToken, csrfTokenHash, secret, sessionIdentifier
|
|
|
85
87
|
return csrfTokenHash === expectedHash;
|
|
86
88
|
};
|
|
87
89
|
const validateRequest = (req) => {
|
|
90
|
+
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
88
91
|
const { cookieName, sessionIdentifier } = useCsrfOptions();
|
|
89
92
|
const csrfCookie = req.cookies[cookieName];
|
|
90
93
|
if (typeof csrfCookie !== 'string') {
|
|
@@ -112,6 +115,7 @@ const validateRequest = (req) => {
|
|
|
112
115
|
}
|
|
113
116
|
};
|
|
114
117
|
export function doubleCsrfProtection(req) {
|
|
118
|
+
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
115
119
|
return async () => {
|
|
116
120
|
const { exempt } = useCsrfOptions();
|
|
117
121
|
if (exempt) {
|
package/dist/listen.js
CHANGED
|
@@ -8,7 +8,6 @@ import express, {} from 'express';
|
|
|
8
8
|
import { BadRequest } from './errors.js';
|
|
9
9
|
import { getLogger } from './logger.js';
|
|
10
10
|
import { before, route } from './middleware.js';
|
|
11
|
-
import { envPort } from './utils.js';
|
|
12
11
|
const logger = getLogger('http');
|
|
13
12
|
const appTimer = startTimer();
|
|
14
13
|
export function stoppable(server, gracePeriodMs = Infinity) {
|
|
@@ -175,7 +174,7 @@ export function createExpressServer(app, options = {}) {
|
|
|
175
174
|
configurable: true,
|
|
176
175
|
});
|
|
177
176
|
enhanced.start = async function start() {
|
|
178
|
-
const port =
|
|
177
|
+
const port = config.PORT;
|
|
179
178
|
await options.onStart?.();
|
|
180
179
|
await ctl.start(port);
|
|
181
180
|
logger.info('app listening', {
|
package/dist/logger.js
CHANGED
|
@@ -7,7 +7,7 @@ export class JsonStreamAdapter {
|
|
|
7
7
|
constructor(stream) {
|
|
8
8
|
this.#stream = stream;
|
|
9
9
|
}
|
|
10
|
-
//
|
|
10
|
+
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
|
|
11
11
|
captureError(error, attributes) { }
|
|
12
12
|
write(log) {
|
|
13
13
|
const chunk = `${JSON.stringify(log)}\n`;
|
|
@@ -72,7 +72,7 @@ export function createLogger({ name, getContext, level = config.LOG_LEVEL, outpu
|
|
|
72
72
|
function getFinalContext(context) {
|
|
73
73
|
return Object.assign(baseContext(), getContext?.(), context);
|
|
74
74
|
}
|
|
75
|
-
//
|
|
75
|
+
// oxlint-disable-next-line @typescript-eslint/no-shadow
|
|
76
76
|
function log(level, message, context) {
|
|
77
77
|
if (level < logLevel) {
|
|
78
78
|
return;
|
package/dist/manage.js
CHANGED
|
@@ -33,13 +33,13 @@ export function registerDefaultCommands(app, program = defaultProgram) {
|
|
|
33
33
|
}
|
|
34
34
|
if (options.openapi) {
|
|
35
35
|
const routes = toOpenAPISpec(app);
|
|
36
|
-
//
|
|
36
|
+
// oxlint-disable-next-line no-console
|
|
37
37
|
console.log(JSON.stringify(routes, null, 2));
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
const routes = parseRoutes(app);
|
|
41
41
|
if (options.json) {
|
|
42
|
-
//
|
|
42
|
+
// oxlint-disable-next-line no-console
|
|
43
43
|
console.log(JSON.stringify(routes, null, 2));
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
@@ -60,7 +60,7 @@ export function registerDefaultCommands(app, program = defaultProgram) {
|
|
|
60
60
|
.option('--json', 'Output JSON')
|
|
61
61
|
.action(cli.wrap(async (options) => {
|
|
62
62
|
if (options.json) {
|
|
63
|
-
//
|
|
63
|
+
// oxlint-disable-next-line no-console
|
|
64
64
|
console.log(JSON.stringify(config, null, 2));
|
|
65
65
|
return;
|
|
66
66
|
}
|
package/dist/middleware.js
CHANGED
|
@@ -225,14 +225,41 @@ export function withTransaction(wrapper) {
|
|
|
225
225
|
try {
|
|
226
226
|
await wrapper.transaction(async () => {
|
|
227
227
|
await new Promise((resolve, reject) => {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
const cleanup = () => {
|
|
229
|
+
res.off('finish', onFinish);
|
|
230
|
+
res.off('close', onClose);
|
|
231
|
+
res.off('error', onError);
|
|
232
|
+
};
|
|
233
|
+
const onFinish = () => {
|
|
234
|
+
cleanup();
|
|
235
|
+
if (res.statusCode >= 400) {
|
|
236
|
+
reject(new Error(`Request finished with status ${res.statusCode}; rolling back transaction.`));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
resolve();
|
|
240
|
+
};
|
|
241
|
+
const onClose = () => {
|
|
242
|
+
cleanup();
|
|
243
|
+
if (!res.writableFinished) {
|
|
244
|
+
reject(new Error('Request closed before the response finished.'));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const onError = (error) => {
|
|
248
|
+
cleanup();
|
|
249
|
+
reject(error);
|
|
250
|
+
};
|
|
251
|
+
res.once('finish', onFinish);
|
|
252
|
+
res.once('close', onClose);
|
|
253
|
+
res.once('error', onError);
|
|
230
254
|
next();
|
|
231
255
|
});
|
|
232
256
|
});
|
|
233
257
|
}
|
|
234
258
|
catch (error) {
|
|
235
259
|
/* v8 ignore next -- @preserve */
|
|
260
|
+
if (res.headersSent || res.writableEnded) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
236
263
|
next(error);
|
|
237
264
|
}
|
|
238
265
|
};
|
package/dist/utils.d.ts
CHANGED
package/dist/utils.js
CHANGED
package/dist/vite/ssr.js
CHANGED
|
@@ -11,8 +11,8 @@ import * as path from 'node:path';
|
|
|
11
11
|
import { config } from '@devmoods/config';
|
|
12
12
|
import express, {} from 'express';
|
|
13
13
|
import {} from 'vite';
|
|
14
|
-
import {} from '../composeMiddleware.js';
|
|
15
14
|
import {} from './types.js';
|
|
15
|
+
import {} from '../composeMiddleware.js';
|
|
16
16
|
import { generateCsrfToken } from '../csrf.js';
|
|
17
17
|
import { composeMiddleware, route, useCspNonce } from '../middleware.js';
|
|
18
18
|
import { createFetchRequest } from '../webapi.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"clean": "rm -rf dist",
|
|
48
|
-
"lint": "
|
|
48
|
+
"lint": "oxlint .",
|
|
49
|
+
"format": "oxfmt .",
|
|
49
50
|
"test": "NODE_ENV=test DOTENV_CONFIG_PATH=.env.test vitest",
|
|
50
51
|
"test:coverage": "pnpm run test --coverage",
|
|
51
52
|
"build": "pnpm run clean && tsgo",
|
|
@@ -68,10 +69,10 @@
|
|
|
68
69
|
"vite": "^8.0.8"
|
|
69
70
|
},
|
|
70
71
|
"dependencies": {
|
|
71
|
-
"@devmoods/config": "0.2.
|
|
72
|
+
"@devmoods/config": "0.2.4",
|
|
72
73
|
"@devmoods/fetch": "^4.3.0",
|
|
73
|
-
"@devmoods/observability": "0.
|
|
74
|
-
"@devmoods/postgres": "0.
|
|
74
|
+
"@devmoods/observability": "0.3.0",
|
|
75
|
+
"@devmoods/postgres": "0.6.0",
|
|
75
76
|
"@hey-api/openapi-ts": "^0.96.0",
|
|
76
77
|
"@rolldown/plugin-babel": "^0.2.3",
|
|
77
78
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -94,7 +95,7 @@
|
|
|
94
95
|
"helmet": "^8.1.0",
|
|
95
96
|
"jose": "^6.2.2",
|
|
96
97
|
"nodemailer": "^8.0.5",
|
|
97
|
-
"redis": "^5.
|
|
98
|
+
"redis": "^5.12.1",
|
|
98
99
|
"sonda": "^0.11.1",
|
|
99
100
|
"tough-cookie": "^6.0.1",
|
|
100
101
|
"uuid": "^13.0.0",
|
|
@@ -110,5 +111,5 @@
|
|
|
110
111
|
"engines": {
|
|
111
112
|
"node": ">=20 <26"
|
|
112
113
|
},
|
|
113
|
-
"gitHead": "
|
|
114
|
+
"gitHead": "2c4100d80823bcf16bd962b4b8a26bd078522dfc"
|
|
114
115
|
}
|