@devmoods/express-extras 0.74.0 → 0.76.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/google-client.d.ts +12 -0
- package/dist/auth/google-client.js +27 -0
- package/dist/auth/google.d.ts +1 -1
- package/dist/auth/google.js +24 -19
- package/dist/auth/token.d.ts +2 -2
- package/dist/auth/token.js +1 -2
- package/dist/auth.d.ts +1 -1
- package/dist/cache.d.ts +2 -2
- package/dist/config.d.ts +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/context.js +2 -2
- package/dist/cookie-consent.d.ts +1 -1
- package/dist/csrf.d.ts +1 -1
- package/dist/csrf.js +1 -1
- package/dist/email.d.ts +1 -1
- package/dist/faktory.d.ts +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +52 -13
- package/dist/rate-limit.d.ts +1 -1
- package/dist/redis.d.ts +2 -3
- package/dist/routes.d.ts +2 -2
- package/dist/slack.d.ts +1 -1
- package/dist/utils.d.ts +0 -2
- package/dist/vite/__tests__/fixtures/src/entry-server.d.ts +1 -1
- package/dist/vite/__tests__/fixtures/src/entry-server.js +1 -2
- package/dist/vite/defineViteConfig.d.ts +1 -1
- package/dist/vite/ssr.d.ts +1 -1
- package/dist/vitest/testClient.d.ts +5 -5
- package/package.json +22 -24
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function exchangeGoogleAuthorizationCode({ code, clientId, clientSecret, redirectUri, }: {
|
|
2
|
+
code: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
redirectUri: string;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
id_token?: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function verifyGoogleIdToken({ token, clientId, }: {
|
|
10
|
+
token: string;
|
|
11
|
+
clientId: string;
|
|
12
|
+
}): Promise<import("jose").JWTPayload>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createFetch } from '@devmoods/fetch';
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
3
|
+
const fetch = createFetch();
|
|
4
|
+
const googleJwks = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'));
|
|
5
|
+
export async function exchangeGoogleAuthorizationCode({ code, clientId, clientSecret, redirectUri, }) {
|
|
6
|
+
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
10
|
+
},
|
|
11
|
+
body: new URLSearchParams({
|
|
12
|
+
code,
|
|
13
|
+
client_id: clientId,
|
|
14
|
+
client_secret: clientSecret,
|
|
15
|
+
redirect_uri: redirectUri,
|
|
16
|
+
grant_type: 'authorization_code',
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
return response.jsonData;
|
|
20
|
+
}
|
|
21
|
+
export async function verifyGoogleIdToken({ token, clientId, }) {
|
|
22
|
+
const { payload } = await jwtVerify(token, googleJwks, {
|
|
23
|
+
audience: clientId,
|
|
24
|
+
issuer: ['https://accounts.google.com', 'accounts.google.com'],
|
|
25
|
+
});
|
|
26
|
+
return payload;
|
|
27
|
+
}
|
package/dist/auth/google.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ interface GoogleAuthProviderOptions<UserModel = AnyUser> {
|
|
|
10
10
|
hostedDomain?: string;
|
|
11
11
|
getOrCreateUserFromUserIdentifier(userIdentifier: string, profile: GoogleProfile): Promise<UserModel>;
|
|
12
12
|
}
|
|
13
|
-
export declare function GoogleAuthProvider<UserModel extends AnyUser>({ clientId, clientSecret, redirectUri, hostedDomain, getOrCreateUserFromUserIdentifier }: GoogleAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
13
|
+
export declare function GoogleAuthProvider<UserModel extends AnyUser>({ clientId, clientSecret, redirectUri, hostedDomain, getOrCreateUserFromUserIdentifier, }: GoogleAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
14
14
|
export {};
|
package/dist/auth/google.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { config } from '@devmoods/config';
|
|
2
2
|
import { type } from 'arktype';
|
|
3
|
-
import {
|
|
3
|
+
import { exchangeGoogleAuthorizationCode, verifyGoogleIdToken, } from './google-client.js';
|
|
4
4
|
import { createTokenResponse } from './token.js';
|
|
5
5
|
import {} from './types.js';
|
|
6
6
|
import { BadRequest, Unauthorized } from '../errors.js';
|
|
7
7
|
import { getLogger } from '../logger.js';
|
|
8
8
|
const logger = getLogger('auth');
|
|
9
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
|
-
const client = new OAuth2Client({
|
|
11
|
-
clientId,
|
|
12
|
-
clientSecret,
|
|
13
|
-
redirectUri,
|
|
14
|
-
});
|
|
15
10
|
const verifyAuthorizationCode = async (inputCode) => {
|
|
16
11
|
try {
|
|
17
|
-
const
|
|
12
|
+
const tokens = await exchangeGoogleAuthorizationCode({
|
|
13
|
+
code: inputCode,
|
|
14
|
+
clientId,
|
|
15
|
+
clientSecret,
|
|
16
|
+
redirectUri,
|
|
17
|
+
});
|
|
18
18
|
return { idToken: tokens.id_token };
|
|
19
19
|
}
|
|
20
20
|
catch (e) {
|
|
@@ -23,15 +23,18 @@ export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, cl
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
const verifyIdToken = async (token) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
try {
|
|
27
|
+
return await verifyGoogleIdToken({
|
|
28
|
+
token,
|
|
29
|
+
clientId,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
logger.error(e);
|
|
34
|
+
throw Unauthorized('Token from Google contains invalid payload', {
|
|
35
|
+
cause: e,
|
|
36
|
+
});
|
|
33
37
|
}
|
|
34
|
-
return payload;
|
|
35
38
|
};
|
|
36
39
|
const requiredScopes = ['openid', 'email', 'profile'];
|
|
37
40
|
return {
|
|
@@ -79,18 +82,20 @@ export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, cl
|
|
|
79
82
|
throw Unauthorized('Invalid id token');
|
|
80
83
|
}
|
|
81
84
|
const payload = await verifyIdToken(idToken);
|
|
82
|
-
if (
|
|
85
|
+
if (typeof payload.sub !== 'string' || payload.sub.length === 0) {
|
|
83
86
|
throw Unauthorized('Token from Google is missing subject');
|
|
84
87
|
}
|
|
85
|
-
if (
|
|
88
|
+
if (typeof payload.email !== 'string' ||
|
|
89
|
+
payload.email_verified !== true) {
|
|
86
90
|
throw Unauthorized('Token from Google contains unverified email');
|
|
87
91
|
}
|
|
88
|
-
if (hostedDomain &&
|
|
92
|
+
if (hostedDomain &&
|
|
93
|
+
(typeof payload.hd !== 'string' || payload.hd !== hostedDomain)) {
|
|
89
94
|
throw Unauthorized('Token from Google contains invalid hosted domain');
|
|
90
95
|
}
|
|
91
96
|
const user = await getOrCreateUserFromUserIdentifier(payload.sub, {
|
|
92
97
|
email: payload.email,
|
|
93
|
-
fullName: payload.name
|
|
98
|
+
fullName: typeof payload.name === 'string' ? payload.name : '',
|
|
94
99
|
});
|
|
95
100
|
const accessToken = await context.login(req, res, user);
|
|
96
101
|
logger.info('google-login', { email: user.email });
|
package/dist/auth/token.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const tokenSchema: ValidationSchema;
|
|
|
4
4
|
export declare function createTokenResponse(accessToken: AccessToken): readonly [201, {
|
|
5
5
|
readonly token: string;
|
|
6
6
|
}, {
|
|
7
|
-
readonly 'Cache-Control':
|
|
7
|
+
readonly 'Cache-Control': "no-store";
|
|
8
8
|
}];
|
|
9
9
|
export type AccessToken = Awaited<ReturnType<typeof createAccessToken>>;
|
|
10
10
|
/**
|
|
@@ -12,7 +12,7 @@ export type AccessToken = Awaited<ReturnType<typeof createAccessToken>>;
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function createAccessToken(payload: any, version?: string, secret?: string): Promise<{
|
|
14
14
|
token: string;
|
|
15
|
-
tokenType:
|
|
15
|
+
tokenType: "Bearer";
|
|
16
16
|
expiresIn: number;
|
|
17
17
|
}>;
|
|
18
18
|
/**
|
package/dist/auth/token.js
CHANGED
|
@@ -2,7 +2,6 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { config } from '@devmoods/config';
|
|
3
3
|
import {} from 'express';
|
|
4
4
|
import { SignJWT, jwtVerify } from 'jose';
|
|
5
|
-
import { v4 } from 'uuid';
|
|
6
5
|
import { InvalidToken } from './errors.js';
|
|
7
6
|
import {} from '../validate.js';
|
|
8
7
|
const ACCESS_TOKEN_COOKIE_NAME = 'access_token';
|
|
@@ -101,7 +100,7 @@ export function extractAccessTokenFromRequest(req) {
|
|
|
101
100
|
export function setAccessTokenCookie(res, token) {
|
|
102
101
|
const sameSite = config.AUTH_COOKIE_SAME_SITE;
|
|
103
102
|
if (token == null) {
|
|
104
|
-
res.cookie(ACCESS_TOKEN_COOKIE_NAME,
|
|
103
|
+
res.cookie(ACCESS_TOKEN_COOKIE_NAME, randomUUID(), {
|
|
105
104
|
expires: new Date(1970, 0, 1),
|
|
106
105
|
sameSite,
|
|
107
106
|
secure: process.env.NODE_ENV === 'production',
|
package/dist/auth.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
20
20
|
logout: (req: Request, res: Response) => Promise<void>;
|
|
21
21
|
setSessionTokenVersion: (version: string) => void;
|
|
22
22
|
}
|
|
23
|
-
export declare function createAuth<UserModel extends AnyUser>({ getUserById, filterUser, tokenStorage, providers, loginUrl, onUserLoggedIn, sessionTokenVersion: initialSessionTokenVersion }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
|
23
|
+
export declare function createAuth<UserModel extends AnyUser>({ getUserById, filterUser, tokenStorage, providers, loginUrl, onUserLoggedIn, sessionTokenVersion: initialSessionTokenVersion, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
package/dist/cache.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface MemoryCacheAdapterOptions {
|
|
|
17
17
|
export declare class RedisCacheAdapter extends CacheAdapter {
|
|
18
18
|
redis: RedisClient;
|
|
19
19
|
keyPrefix: string;
|
|
20
|
-
constructor(redis: RedisClient, { keyPrefix }?: RedisCacheAdapterOptions);
|
|
20
|
+
constructor(redis: RedisClient, { keyPrefix, }?: RedisCacheAdapterOptions);
|
|
21
21
|
get(key: string): Promise<any>;
|
|
22
22
|
set(key: string, value: unknown, expires?: number | null): Promise<void>;
|
|
23
23
|
clear(key?: string): Promise<void>;
|
|
@@ -42,7 +42,7 @@ export declare function createCache(adapter: CacheAdapter): {
|
|
|
42
42
|
wrap: <T>(key: string, options: CacheOptions | number | null, fn: () => Promise<T>) => Promise<T>;
|
|
43
43
|
decorate: <T, Arguments extends any[]>(options: CacheOptions | number | null, fn: (...args: Arguments) => T | Promise<T>) => {
|
|
44
44
|
(...args: Arguments): Promise<T>;
|
|
45
|
-
clear
|
|
45
|
+
clear(...args: Arguments): Promise<void>;
|
|
46
46
|
};
|
|
47
47
|
hashKey: typeof hashKey;
|
|
48
48
|
};
|
package/dist/config.d.ts
CHANGED
package/dist/context.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface RequestContextOptions {
|
|
|
9
9
|
/**
|
|
10
10
|
* Middleware for setting a `requestId` on the request context.
|
|
11
11
|
*/
|
|
12
|
-
export declare function withRequestContext({ getRequestId }?: RequestContextOptions): RequestHandler;
|
|
12
|
+
export declare function withRequestContext({ getRequestId, }?: RequestContextOptions): RequestHandler;
|
|
13
13
|
/**
|
|
14
14
|
* Return a value from the request context
|
|
15
15
|
*/
|
package/dist/context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import {} from 'express';
|
|
3
|
-
import { v4 as uuid } from 'uuid';
|
|
4
4
|
export const requestContext = new AsyncLocalStorage();
|
|
5
5
|
export const REQUEST_ID_CONTEXT_KEY = 'requestId';
|
|
6
6
|
export const JOB_ID_CONTEXT_KEY = 'jobId';
|
|
@@ -9,7 +9,7 @@ export const JOB_ID_CONTEXT_KEY = 'jobId';
|
|
|
9
9
|
*/
|
|
10
10
|
export function withRequestContext({ getRequestId = (req) => req.headers['X-Request-ID'], } = {}) {
|
|
11
11
|
return (req, res, next) => {
|
|
12
|
-
const requestId = getRequestId(req) ||
|
|
12
|
+
const requestId = getRequestId(req) || randomUUID();
|
|
13
13
|
requestContext.run(new Map(), () => {
|
|
14
14
|
setRequestContext(REQUEST_ID_CONTEXT_KEY, requestId);
|
|
15
15
|
next();
|
package/dist/cookie-consent.d.ts
CHANGED
package/dist/csrf.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ interface CsrfOptions {
|
|
|
10
10
|
cookieName: string;
|
|
11
11
|
sessionIdentifier: string;
|
|
12
12
|
}
|
|
13
|
-
export declare function csrf({ exempt, sessionIdentifier, cookieName }?: CsrfMiddlewareOptions): RequestHandler;
|
|
13
|
+
export declare function csrf({ exempt, sessionIdentifier, cookieName, }?: CsrfMiddlewareOptions): RequestHandler;
|
|
14
14
|
export declare function setCsrfOptions(options: CsrfOptions): void;
|
|
15
15
|
export declare function useCsrfOptions(): CsrfOptions;
|
|
16
16
|
interface TokenOptions {
|
package/dist/csrf.js
CHANGED
|
@@ -115,8 +115,8 @@ const validateRequest = (req) => {
|
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
117
|
export function doubleCsrfProtection(req) {
|
|
118
|
-
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
119
118
|
return async () => {
|
|
119
|
+
// oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks
|
|
120
120
|
const { exempt } = useCsrfOptions();
|
|
121
121
|
if (exempt) {
|
|
122
122
|
return;
|
package/dist/email.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface EmailOptions {
|
|
|
11
11
|
export interface SendEmailOptions extends nodemailer.SendMailOptions {
|
|
12
12
|
message: string;
|
|
13
13
|
}
|
|
14
|
-
export declare function createEmail({ host, port, user, pass, senderName, senderAddress, onEmailSent }?: EmailOptions): {
|
|
14
|
+
export declare function createEmail({ host, port, user, pass, senderName, senderAddress, onEmailSent, }?: EmailOptions): {
|
|
15
15
|
send: ({ to, subject, message, ...rest }: SendEmailOptions) => Promise<void>;
|
|
16
16
|
transporter: nodemailer.Transporter<import("nodemailer/lib/smtp-transport/index.js").SentMessageInfo, import("nodemailer/lib/smtp-transport/index.js").Options>;
|
|
17
17
|
};
|
package/dist/faktory.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface FaktoryWorkerOptions<GlobalContext> {
|
|
|
29
29
|
jobContext?: GlobalContext;
|
|
30
30
|
}
|
|
31
31
|
export { createFaktoryWorker as createJobWorker };
|
|
32
|
-
export declare function createFaktoryWorker<GlobalContext>({ queues, jobs, faktory: createFactory, jobContext }: FaktoryWorkerOptions<GlobalContext>): Promise<{
|
|
32
|
+
export declare function createFaktoryWorker<GlobalContext>({ queues, jobs, faktory: createFactory, jobContext, }: FaktoryWorkerOptions<GlobalContext>): Promise<{
|
|
33
33
|
start(workerOptions?: WorkerOptions): Promise<void>;
|
|
34
34
|
stop(): Promise<void>;
|
|
35
35
|
}>;
|
package/dist/logger.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare function clearGlobalLoggerContext(): void;
|
|
|
30
30
|
export type Logger = ReturnType<typeof createLogger>;
|
|
31
31
|
export declare function addDefaultLoggerOutput(output: LoggerAdapter): void;
|
|
32
32
|
export declare function clearDefaultLoggerOutputs(): void;
|
|
33
|
-
export declare function createLogger({ name, getContext, level, outputs }: LoggerOptions): {
|
|
33
|
+
export declare function createLogger({ name, getContext, level, outputs, }: LoggerOptions): {
|
|
34
34
|
debug(message: string, context?: LoggerContext): void;
|
|
35
35
|
info(message: string, context?: LoggerContext): void;
|
|
36
36
|
warn(message: string, context?: LoggerContext): void;
|
package/dist/middleware.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface BeforeMiddleware extends RequestHandler {
|
|
|
55
55
|
/**
|
|
56
56
|
* Register middleware that must come before the app routes.
|
|
57
57
|
*/
|
|
58
|
-
export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, auth }?: BeforeMiddlewareOptions): BeforeMiddleware;
|
|
58
|
+
export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, auth, }?: BeforeMiddlewareOptions): BeforeMiddleware;
|
|
59
59
|
/**
|
|
60
60
|
* Middleware that sends a 404
|
|
61
61
|
*
|
package/dist/middleware.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import {} from 'node:http';
|
|
3
3
|
import { config } from '@devmoods/config';
|
|
4
|
-
import { startTimer } from '@devmoods/observability';
|
|
4
|
+
import { metrics, startTimer } from '@devmoods/observability';
|
|
5
5
|
import cookieParser from 'cookie-parser';
|
|
6
6
|
import { json as jsonParser, urlencoded as urlencodedParser, static as staticMiddleware, } from 'express';
|
|
7
7
|
import helmet from 'helmet';
|
|
8
8
|
import {} from './auth.js';
|
|
9
9
|
import { compose } from './composeMiddleware.js';
|
|
10
|
-
import { ANONYMOUS_SESSION_KEY, setRequestContext, useRequestContext, withRequestContext, } from './context.js';
|
|
10
|
+
import { ANONYMOUS_SESSION_KEY, requestContext, setRequestContext, useRequestContext, withRequestContext, } from './context.js';
|
|
11
11
|
import { csrf, doubleCsrfProtection, } from './csrf.js';
|
|
12
12
|
import { NotFound } from './errors.js';
|
|
13
13
|
import { getLogger } from './logger.js';
|
|
@@ -26,6 +26,7 @@ export function useCspNonce() {
|
|
|
26
26
|
export function before({ cookies = true, helmet: useHelmet = true, json: useJson = true, csrf: useCsrf = false, urlencoded: useUrlencoded = false, allowUnsafeInlineScripts = false, cspDirectives = (x) => x, auth, } = {}) {
|
|
27
27
|
const middleware = compose([
|
|
28
28
|
withRequestContext(),
|
|
29
|
+
startRequestTimer(),
|
|
29
30
|
(cookies || useCsrf) && cookieParser(),
|
|
30
31
|
cookies && setAnonymousCookie(),
|
|
31
32
|
typeof useCsrf === 'object' && useCsrf != null
|
|
@@ -76,21 +77,52 @@ export function before({ cookies = true, helmet: useHelmet = true, json: useJson
|
|
|
76
77
|
});
|
|
77
78
|
return middleware;
|
|
78
79
|
}
|
|
80
|
+
const REQUEST_TIMER_KEY = 'request-timer';
|
|
81
|
+
const LOG_REQUEST_FILTERED_PATHS = [
|
|
82
|
+
'/src',
|
|
83
|
+
'/node_modules',
|
|
84
|
+
'/@fs',
|
|
85
|
+
'/@vite',
|
|
86
|
+
'/@react-refresh',
|
|
87
|
+
'/@id',
|
|
88
|
+
'/analytics.js',
|
|
89
|
+
];
|
|
90
|
+
function isLogFilteredPath(req) {
|
|
91
|
+
return LOG_REQUEST_FILTERED_PATHS.some((p) => req.path.startsWith(p));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Derive a low-cardinality route label (e.g. `/users/:id`) for metrics.
|
|
95
|
+
* Falls back to `unknown` when no route matched (e.g. 404).
|
|
96
|
+
*/
|
|
97
|
+
function getRouteLabel(req) {
|
|
98
|
+
const routePath = req.route?.path;
|
|
99
|
+
if (!routePath) {
|
|
100
|
+
return 'unknown';
|
|
101
|
+
}
|
|
102
|
+
/* c8 ignore next 1 */
|
|
103
|
+
const pattern = Array.isArray(routePath) ? routePath.join('|') : routePath;
|
|
104
|
+
return `${req.baseUrl}${pattern}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Start a timer as early as possible in the middleware chain so the
|
|
108
|
+
* duration reported by `logRequestDuration` includes time spent in
|
|
109
|
+
* upstream middlewares (cookie parsing, CSRF, auth, etc.).
|
|
110
|
+
*/
|
|
111
|
+
function startRequestTimer() {
|
|
112
|
+
return function startRequestTimerMiddleware(req, res, next) {
|
|
113
|
+
if (isLogFilteredPath(req)) {
|
|
114
|
+
return next();
|
|
115
|
+
}
|
|
116
|
+
setRequestContext(REQUEST_TIMER_KEY, startTimer());
|
|
117
|
+
next();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
79
120
|
function logRequestDuration() {
|
|
80
|
-
const FILTERED_PATHS = [
|
|
81
|
-
'/src',
|
|
82
|
-
'/node_modules',
|
|
83
|
-
'/@fs',
|
|
84
|
-
'/@vite',
|
|
85
|
-
'/@react-refresh',
|
|
86
|
-
'/@id',
|
|
87
|
-
'/analytics.js',
|
|
88
|
-
];
|
|
89
121
|
return function logRequestDurationMiddleware(req, res, next) {
|
|
90
|
-
|
|
122
|
+
const timer = requestContext.getStore()?.get(REQUEST_TIMER_KEY);
|
|
123
|
+
if (!timer) {
|
|
91
124
|
return next();
|
|
92
125
|
}
|
|
93
|
-
const timer = startTimer();
|
|
94
126
|
logger.info('http request', {
|
|
95
127
|
method: req.method,
|
|
96
128
|
url: req.originalUrl,
|
|
@@ -107,13 +139,20 @@ function logRequestDuration() {
|
|
|
107
139
|
});
|
|
108
140
|
res.on('finish', () => {
|
|
109
141
|
const duration = timer.getDuration();
|
|
142
|
+
const route = getRouteLabel(req);
|
|
110
143
|
logger.info('http response', {
|
|
111
144
|
method: req.method,
|
|
112
145
|
url: req.originalUrl,
|
|
113
146
|
path: req.path,
|
|
147
|
+
route,
|
|
114
148
|
duration,
|
|
115
149
|
status: res.statusCode,
|
|
116
150
|
});
|
|
151
|
+
metrics.distribution('http_request_duration_seconds', duration, {
|
|
152
|
+
method: req.method,
|
|
153
|
+
route,
|
|
154
|
+
status: res.statusCode >= 400 ? 'error' : 'success',
|
|
155
|
+
});
|
|
117
156
|
});
|
|
118
157
|
next();
|
|
119
158
|
};
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -30,4 +30,4 @@ export interface RateLimitOptions {
|
|
|
30
30
|
/**
|
|
31
31
|
* Returns a middleware for rate limiting requests.
|
|
32
32
|
*/
|
|
33
|
-
export declare function rateLimit(redis: RedisClient, { max, windowSizeInSeconds, getKey }?: RateLimitOptions): RateLimitMiddleware;
|
|
33
|
+
export declare function rateLimit(redis: RedisClient, { max, windowSizeInSeconds, getKey, }?: RateLimitOptions): RateLimitMiddleware;
|
package/dist/redis.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { createClient } from 'redis';
|
|
2
1
|
export interface RedisOptions {
|
|
3
2
|
url?: string;
|
|
4
3
|
onError?: (error: Error) => void;
|
|
5
4
|
}
|
|
6
|
-
export type RedisClient = ReturnType<typeof
|
|
7
|
-
export declare function createRedis({ url, onError }?: RedisOptions):
|
|
5
|
+
export type RedisClient = ReturnType<typeof createRedis>;
|
|
6
|
+
export declare function createRedis({ url, onError, }?: RedisOptions): import("redis").RedisClientType<{}, {}, {}, 3, {}>;
|
package/dist/routes.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { type Application } from 'express';
|
|
|
2
2
|
export declare function createDocsRouter(app: Application, extras?: Record<string, any>): import("express").RequestHandler<Record<string, string>, {
|
|
3
3
|
openapi: string;
|
|
4
4
|
info: {
|
|
5
|
+
description?: string | undefined;
|
|
5
6
|
title: string;
|
|
6
7
|
version: string;
|
|
7
|
-
description?: string | undefined;
|
|
8
8
|
};
|
|
9
9
|
tags: {
|
|
10
10
|
name: string;
|
|
@@ -42,9 +42,9 @@ export declare function toOpenAPISpec(app: Application, info?: {
|
|
|
42
42
|
}): {
|
|
43
43
|
openapi: string;
|
|
44
44
|
info: {
|
|
45
|
+
description?: string | undefined;
|
|
45
46
|
title: string;
|
|
46
47
|
version: string;
|
|
47
|
-
description?: string | undefined;
|
|
48
48
|
};
|
|
49
49
|
tags: {
|
|
50
50
|
name: string;
|
package/dist/slack.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ type Blocks = unknown[];
|
|
|
17
17
|
*
|
|
18
18
|
* See https://api.slack.com/messaging/webhooks
|
|
19
19
|
*/
|
|
20
|
-
export declare function createSlack({ webhookUrl, dryRun }?: SlackOptions): {
|
|
20
|
+
export declare function createSlack({ webhookUrl, dryRun, }?: SlackOptions): {
|
|
21
21
|
postMessage: (message: Message, blocks?: Blocks) => Promise<unknown>;
|
|
22
22
|
};
|
|
23
23
|
export {};
|
package/dist/utils.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ export declare function readEnv(key: string): string | undefined;
|
|
|
4
4
|
export declare function readEnv(key: string, defaultValue: string): string;
|
|
5
5
|
export declare namespace readEnv {
|
|
6
6
|
var number: typeof readEnvNumber;
|
|
7
|
-
}
|
|
8
|
-
export declare namespace readEnv {
|
|
9
7
|
var boolean: typeof readEnvBoolean;
|
|
10
8
|
}
|
|
11
9
|
declare function readEnvNumber(key: string, defaultValue: number): number;
|
|
@@ -2,7 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { renderToString } from 'react-dom/server';
|
|
3
3
|
import {} from '../../../types.js';
|
|
4
4
|
export async function render(req, options) {
|
|
5
|
-
const helmetContext = {};
|
|
6
5
|
const initialData = await options.getInitialData(req);
|
|
7
6
|
if (initialData.shouldRedirect) {
|
|
8
7
|
throw new Response('redirect', { status: 302 });
|
|
@@ -10,6 +9,6 @@ export async function render(req, options) {
|
|
|
10
9
|
const html = renderToString(_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [options.useDevServer && (_jsx("script", { type: "module", nonce: options.nonce, children: options.preamble })), _jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1, shrink-to-fit=no" }), _jsx("meta", { name: "theme-color", content: "#FFFFFF" }), _jsx("meta", { name: "apple-mobile-web-app-capable", content: "yes" }), _jsx("meta", { name: "apple-mobile-web-app-title", content: "express-extras" }), _jsx("link", { rel: "apple-touch-icon", href: "/apple-touch-icon.png" }), options.placeholders, _jsx("meta", { name: "csrf-token", content: options.csrfToken })] }), _jsxs("body", { children: [_jsx("div", { id: "root", children: _jsx("h1", { children: "Hello World!" }) }), options.useDevServer && (_jsx("script", { type: "module", src: "/src/entry-client.tsx" })), _jsx("script", { defer: true, "data-domain": options.publicUrl.hostname, src: "/analytics.js" }), _jsx("script", { nonce: options.nonce, children: `window.store = ${JSON.stringify(initialData)};` })] })] }));
|
|
11
10
|
return {
|
|
12
11
|
html,
|
|
13
|
-
context:
|
|
12
|
+
context: undefined,
|
|
14
13
|
};
|
|
15
14
|
}
|
package/dist/vite/ssr.d.ts
CHANGED
|
@@ -22,5 +22,5 @@ export interface SsrMiddleware extends RequestHandler {
|
|
|
22
22
|
*
|
|
23
23
|
* See more: https://vite.dev/guide/ssr
|
|
24
24
|
*/
|
|
25
|
-
export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer }?: SsrMiddlewareOptions<InitialData>): Promise<SsrMiddleware>;
|
|
25
|
+
export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer, }?: SsrMiddlewareOptions<InitialData>): Promise<SsrMiddleware>;
|
|
26
26
|
export declare function replaceSsrPlaceholders(template: string, values: Record<string, unknown>): string;
|
|
@@ -7,23 +7,23 @@ export declare function createTestClient(server: Server | Application, options?:
|
|
|
7
7
|
duplicate(): Promise</*elided*/ any>;
|
|
8
8
|
cookieJar: tough.CookieJar;
|
|
9
9
|
fetch: {
|
|
10
|
-
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec").StandardSchemaV1.InferOutput<TSchema>>>;
|
|
10
|
+
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
|
|
11
11
|
intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
|
|
12
12
|
};
|
|
13
13
|
post: {
|
|
14
|
-
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec").StandardSchemaV1.InferOutput<TSchema>>>;
|
|
14
|
+
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
|
|
15
15
|
intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
|
|
16
16
|
};
|
|
17
17
|
get: {
|
|
18
|
-
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec").StandardSchemaV1.InferOutput<TSchema>>>;
|
|
18
|
+
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
|
|
19
19
|
intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
|
|
20
20
|
};
|
|
21
21
|
put: {
|
|
22
|
-
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec").StandardSchemaV1.InferOutput<TSchema>>>;
|
|
22
|
+
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
|
|
23
23
|
intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
|
|
24
24
|
};
|
|
25
25
|
delete: {
|
|
26
|
-
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec").StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec").StandardSchemaV1.InferOutput<TSchema>>>;
|
|
26
|
+
<TResponseData, TRequestData = any, TTransformedResponseData = TResponseData, TTransformedRequestData = TRequestData, TSchema extends import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, any> = import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1<TTransformedResponseData, TTransformedResponseData>>(url: string, options?: import("@devmoods/fetch").RequestOptions<TRequestData, TTransformedRequestData, TResponseData, TTransformedResponseData, TSchema>): Promise<import("@devmoods/fetch").HttpResponse<import("@standard-schema/spec", { with: { "resolution-mode": "require" } }).StandardSchemaV1.InferOutput<TSchema>>>;
|
|
27
27
|
intercept(interceptor: import("@devmoods/fetch").FetchInterceptor): () => void;
|
|
28
28
|
};
|
|
29
29
|
close: () => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"format": "oxfmt .",
|
|
50
50
|
"test": "NODE_ENV=test DOTENV_CONFIG_PATH=.env.test vitest",
|
|
51
51
|
"test:coverage": "pnpm run test --coverage",
|
|
52
|
-
"build": "pnpm run clean &&
|
|
52
|
+
"build": "pnpm run clean && tsc",
|
|
53
53
|
"prepublishOnly": "pnpm run clean && pnpm run build"
|
|
54
54
|
},
|
|
55
55
|
"repository": {
|
|
@@ -61,44 +61,42 @@
|
|
|
61
61
|
],
|
|
62
62
|
"license": "ISC",
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@vitejs/plugin-react": "^6.0.
|
|
65
|
-
"arktype": "^2.2.
|
|
64
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
65
|
+
"arktype": "^2.2.2",
|
|
66
66
|
"faktory-worker": "^4.7.1",
|
|
67
|
-
"react": "^19.2.
|
|
68
|
-
"react-dom": "^19.2.
|
|
69
|
-
"vite": "^8.
|
|
67
|
+
"react": "^19.2.7",
|
|
68
|
+
"react-dom": "^19.2.7",
|
|
69
|
+
"vite": "^8.1.3"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@devmoods/config": "0.
|
|
73
|
-
"@devmoods/fetch": "^4.3.
|
|
74
|
-
"@devmoods/observability": "0.
|
|
75
|
-
"@devmoods/postgres": "0.
|
|
76
|
-
"@hey-api/openapi-ts": "^0.
|
|
72
|
+
"@devmoods/config": "0.3.0",
|
|
73
|
+
"@devmoods/fetch": "^4.3.1",
|
|
74
|
+
"@devmoods/observability": "0.4.0",
|
|
75
|
+
"@devmoods/postgres": "0.8.0",
|
|
76
|
+
"@hey-api/openapi-ts": "^0.99.0",
|
|
77
77
|
"@rolldown/plugin-babel": "^0.2.3",
|
|
78
78
|
"@standard-schema/spec": "^1.1.0",
|
|
79
79
|
"@types/bcrypt": "^6.0.0",
|
|
80
80
|
"@types/cookie-parser": "^1.4.10",
|
|
81
81
|
"@types/express": "^5.0.6",
|
|
82
82
|
"@types/koa-compose": "^3.2.9",
|
|
83
|
-
"@types/nodemailer": "^8.0.
|
|
84
|
-
"@types/qs": "^6.15.
|
|
85
|
-
"ajv": "^8.
|
|
83
|
+
"@types/nodemailer": "^8.0.1",
|
|
84
|
+
"@types/qs": "^6.15.1",
|
|
85
|
+
"ajv": "^8.20.0",
|
|
86
86
|
"argon2": "^0.44.0",
|
|
87
87
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
88
88
|
"bcrypt": "^6.0.0",
|
|
89
89
|
"chalk": "^5.6.2",
|
|
90
|
-
"commander": "^
|
|
90
|
+
"commander": "^15.0.0",
|
|
91
91
|
"cookie-parser": "^1.4.7",
|
|
92
92
|
"dotenv": "^17.4.2",
|
|
93
93
|
"express": "^4.21.2",
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"sonda": "^0.11.1",
|
|
94
|
+
"helmet": "^8.2.0",
|
|
95
|
+
"jose": "^6.2.3",
|
|
96
|
+
"nodemailer": "^9.0.3",
|
|
97
|
+
"redis": "^6.1.0",
|
|
98
|
+
"sonda": "^0.13.1",
|
|
100
99
|
"tough-cookie": "^6.0.1",
|
|
101
|
-
"uuid": "^13.0.0",
|
|
102
100
|
"vitest-fail-on-console": "^0.10.1"
|
|
103
101
|
},
|
|
104
102
|
"peerDependencies": {
|
|
@@ -111,5 +109,5 @@
|
|
|
111
109
|
"engines": {
|
|
112
110
|
"node": ">=20 <26"
|
|
113
111
|
},
|
|
114
|
-
"gitHead": "
|
|
112
|
+
"gitHead": "52519bed307a1eedf13ab334f11c1ad9c7c8db91"
|
|
115
113
|
}
|