@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.
@@ -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
+ }
@@ -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 {};
@@ -1,20 +1,20 @@
1
1
  import { config } from '@devmoods/config';
2
2
  import { type } from 'arktype';
3
- import { OAuth2Client } from 'google-auth-library';
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 { tokens } = await client.getToken(inputCode);
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
- const ticket = await client.verifyIdToken({
27
- idToken: token,
28
- audience: clientId,
29
- });
30
- const payload = ticket.getPayload();
31
- if (!payload) {
32
- throw Unauthorized('Token from Google contains invalid payload');
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 (!payload.sub) {
85
+ if (typeof payload.sub !== 'string' || payload.sub.length === 0) {
83
86
  throw Unauthorized('Token from Google is missing subject');
84
87
  }
85
- if (!payload.email || payload.email_verified !== true) {
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 && payload.hd !== 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 });
@@ -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': 'no-store';
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: 'Bearer';
15
+ tokenType: "Bearer";
16
16
  expiresIn: number;
17
17
  }>;
18
18
  /**
@@ -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, v4(), {
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: (...args: Arguments) => Promise<void>;
45
+ clear(...args: Arguments): Promise<void>;
46
46
  };
47
47
  hashKey: typeof hashKey;
48
48
  };
package/dist/config.d.ts CHANGED
@@ -2,7 +2,7 @@ declare module '@devmoods/config' {
2
2
  interface Config extends ConfigFromDefs<typeof app> {
3
3
  }
4
4
  }
5
- declare const app: Defs<{
5
+ declare const app: import("@devmoods/config").Defs<{
6
6
  CI: boolean;
7
7
  PORT: number;
8
8
  PUBLIC_URL: string;
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) || uuid();
12
+ const requestId = getRequestId(req) || randomUUID();
13
13
  requestContext.run(new Map(), () => {
14
14
  setRequestContext(REQUEST_ID_CONTEXT_KEY, requestId);
15
15
  next();
@@ -1,3 +1,3 @@
1
- export declare function cookieConsent({ onConsent }: {
1
+ export declare function cookieConsent({ onConsent, }: {
2
2
  onConsent: (consents: string[]) => Promise<void> | void;
3
3
  }): import("express-serve-static-core").Router;
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;
@@ -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
  *
@@ -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
- if (FILTERED_PATHS.some((p) => req.path.startsWith(p))) {
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
  };
@@ -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 createClient>;
7
- export declare function createRedis({ url, onError }?: RedisOptions): RedisClient;
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;
@@ -1,5 +1,5 @@
1
1
  import { type SsrRenderOptions } from '../../../types.js';
2
2
  export declare function render(req: Request, options: SsrRenderOptions): Promise<{
3
3
  html: string;
4
- context: any;
4
+ context: undefined;
5
5
  }>;
@@ -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: helmetContext.helmet,
12
+ context: undefined,
14
13
  };
15
14
  }
@@ -1,5 +1,5 @@
1
1
  import { type UserConfig } from 'vite';
2
- export declare function defineViteConfig({ base, ssr, analyze }: {
2
+ export declare function defineViteConfig({ base, ssr, analyze, }: {
3
3
  base?: string;
4
4
  reactCompiler?: boolean;
5
5
  ssr?: boolean;
@@ -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.74.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 && tsgo",
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.1",
65
- "arktype": "^2.2.0",
64
+ "@vitejs/plugin-react": "^6.0.3",
65
+ "arktype": "^2.2.2",
66
66
  "faktory-worker": "^4.7.1",
67
- "react": "^19.2.5",
68
- "react-dom": "^19.2.5",
69
- "vite": "^8.0.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.2.4",
73
- "@devmoods/fetch": "^4.3.0",
74
- "@devmoods/observability": "0.3.0",
75
- "@devmoods/postgres": "0.6.0",
76
- "@hey-api/openapi-ts": "^0.96.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.0",
84
- "@types/qs": "^6.15.0",
85
- "ajv": "^8.18.0",
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": "^14.0.3",
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
- "google-auth-library": "^10.6.2",
95
- "helmet": "^8.1.0",
96
- "jose": "^6.2.2",
97
- "nodemailer": "^8.0.5",
98
- "redis": "^5.12.1",
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": "2c4100d80823bcf16bd962b4b8a26bd078522dfc"
112
+ "gitHead": "52519bed307a1eedf13ab334f11c1ad9c7c8db91"
115
113
  }