@devmoods/express-extras 0.66.1 → 0.68.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.
@@ -1,7 +1,6 @@
1
1
  import { startTimer } from '@devmoods/observability';
2
2
  import argon2 from 'argon2';
3
3
  import bcrypt from 'bcrypt';
4
- import {} from 'express';
5
4
  import { createTokenResponse, tokenSchema } from './token.js';
6
5
  import {} from './types.js';
7
6
  import { Unauthorized } from '../errors.js';
@@ -1,6 +1,5 @@
1
1
  import { config } from '@devmoods/config';
2
2
  import { type } from 'arktype';
3
- import {} from 'express';
4
3
  import { OAuth2Client } from 'google-auth-library';
5
4
  import { BadRequest, Unauthorized } from '../errors.js';
6
5
  import { getLogger } from '../logger.js';
@@ -1,5 +1,4 @@
1
1
  import { DoesNotExistError } from '@devmoods/postgres';
2
- import {} from 'express';
3
2
  import { createTokenResponse } from './token.js';
4
3
  import {} from './types.js';
5
4
  import { getLogger } from '../logger.js';
@@ -6,28 +6,28 @@ export declare function createTokenResponse(accessToken: AccessToken): readonly
6
6
  }, {
7
7
  readonly 'Cache-Control': "no-store";
8
8
  }];
9
- export type AccessToken = ReturnType<typeof createAccessToken>;
9
+ export type AccessToken = Awaited<ReturnType<typeof createAccessToken>>;
10
10
  /**
11
11
  * Returns a signed JWT token.
12
12
  */
13
- export declare function createAccessToken(payload: any, version?: string, secret?: string): {
13
+ export declare function createAccessToken(payload: any, version?: string, secret?: string): Promise<{
14
14
  token: string;
15
15
  tokenType: "Bearer";
16
16
  expiresIn: number;
17
- };
17
+ }>;
18
18
  /**
19
19
  * Decode the given JWT token.
20
20
  */
21
- export declare function decodeAccessToken(token: string, secret?: string): {
22
- [key: string]: any;
23
- iss?: string | undefined;
24
- sub?: string | undefined;
25
- aud?: string | string[] | undefined;
26
- exp?: number | undefined;
27
- nbf?: number | undefined;
28
- iat?: number | undefined;
29
- jti?: string | undefined;
30
- };
21
+ export declare function decodeAccessToken(token: string, secret?: string): Promise<{
22
+ [propName: string]: unknown;
23
+ iss?: string;
24
+ sub?: string;
25
+ aud?: string | string[];
26
+ jti?: string;
27
+ nbf?: number;
28
+ exp?: number;
29
+ iat?: number;
30
+ }>;
31
31
  /**
32
32
  * Extract a Bearer token from the HTTP Authorization header
33
33
  * of `req`.
@@ -1,6 +1,6 @@
1
1
  import { config } from '@devmoods/config';
2
2
  import {} from 'express';
3
- import jwt from 'jsonwebtoken';
3
+ import { SignJWT, jwtVerify } from 'jose';
4
4
  import { v4 } from 'uuid';
5
5
  import { InvalidToken } from './errors.js';
6
6
  import {} from '../validate.js';
@@ -24,24 +24,24 @@ export function createTokenResponse(accessToken) {
24
24
  /**
25
25
  * Returns a signed JWT token.
26
26
  */
27
- export function createAccessToken(payload, version = '1', secret = config.AUTH_JWT_SECRET) {
27
+ export async function createAccessToken(payload, version = '1', secret = config.AUTH_JWT_SECRET) {
28
28
  const expiresIn = config.AUTH_TOKEN_EXPIRES;
29
- const token = jwt.sign({ v: version, ...payload }, secret, {
30
- expiresIn,
31
- });
29
+ const secretKey = new TextEncoder().encode(secret);
30
+ const token = await new SignJWT({ v: version, ...payload })
31
+ .setProtectedHeader({ alg: 'HS256' })
32
+ .setExpirationTime(`${expiresIn}s`)
33
+ .setIssuedAt(new Date())
34
+ .sign(secretKey);
32
35
  return { token, tokenType: 'Bearer', expiresIn };
33
36
  }
34
37
  /**
35
38
  * Decode the given JWT token.
36
39
  */
37
- export function decodeAccessToken(token, secret = config.AUTH_JWT_SECRET) {
40
+ export async function decodeAccessToken(token, secret = config.AUTH_JWT_SECRET) {
38
41
  try {
39
- const decoded = jwt.verify(token.trim(), secret);
40
- /* c8 ignore next 3 */
41
- if (typeof decoded === 'string') {
42
- throw new Error('decoded value not an object');
43
- }
44
- return { ...decoded };
42
+ const secretKey = new TextEncoder().encode(secret);
43
+ const { payload } = await jwtVerify(token.trim(), secretKey);
44
+ return { ...payload };
45
45
  }
46
46
  catch (error) {
47
47
  throw new InvalidToken(`Failed to decode auth token: ${error.message}`, {
package/dist/auth.js CHANGED
@@ -22,7 +22,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
22
22
  const useCurrentUser = () => useRequestContext('currentUser');
23
23
  const setCurrentUser = (user) => setRequestContext('currentUser', user);
24
24
  const login = async (req, res, user, isRefresh = false) => {
25
- const accessToken = createAccessToken({ uid: user.id }, sessionTokenVersion);
25
+ const accessToken = await createAccessToken({ uid: user.id }, sessionTokenVersion);
26
26
  const sessionId = sha256(accessToken.token);
27
27
  await tokenStorage.set(sessionId, String(user.id), accessToken.expiresIn);
28
28
  setAccessTokenCookie(res, accessToken.token);
@@ -138,7 +138,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
138
138
  await logout(req, res);
139
139
  throw new InvalidToken('session token expired');
140
140
  }
141
- const decoded = decodeAccessToken(token);
141
+ const decoded = await decodeAccessToken(token);
142
142
  if (decoded.v !== sessionTokenVersion) {
143
143
  await logout(req, res);
144
144
  throw new InvalidToken('Invalid token version');
package/dist/faktory.js CHANGED
@@ -65,6 +65,14 @@ export async function createFaktoryWorker({ queues, jobs, faktory: createFactory
65
65
  return job.execute(...args, jobContext);
66
66
  });
67
67
  }
68
+ /* c8 ignore start */
69
+ const onSignal = async (signal) => {
70
+ logger.info('faktory worker received signal', { signal });
71
+ };
72
+ for (const signal of ['SIGTERM', 'SIGINT']) {
73
+ process.once(signal, onSignal);
74
+ }
75
+ /* c8 ignore end */
68
76
  return {
69
77
  async start(workerOptions = {}) {
70
78
  try {
@@ -1,5 +1,5 @@
1
1
  import { type ServerResponse, type IncomingMessage } from 'node:http';
2
- import { type NextFunction, type RequestHandler, type Request, type Response, json as jsonParser, urlencoded as urlencodedParser } from 'express';
2
+ import { type NextFunction, type RequestHandler, type Request, type Response, json as jsonParser, urlencoded as urlencodedParser, static as staticMiddleware } from 'express';
3
3
  import { type createAuth } from './auth.js';
4
4
  import { compose } from './composeMiddleware.js';
5
5
  import { type CsrfMiddlewareOptions } from './csrf.js';
@@ -82,3 +82,4 @@ export declare function asyncMiddleware<RequestBody, RequestQuery extends Record
82
82
  export declare function withTransaction(wrapper: {
83
83
  transaction: (fn: () => Promise<void>) => Promise<void>;
84
84
  }): RequestHandler;
85
+ export { staticMiddleware };
@@ -3,7 +3,7 @@ import {} from 'node:http';
3
3
  import { config } from '@devmoods/config';
4
4
  import { startTimer } from '@devmoods/observability';
5
5
  import cookieParser from 'cookie-parser';
6
- import { json as jsonParser, urlencoded as urlencodedParser, } from 'express';
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';
@@ -236,3 +236,4 @@ export function withTransaction(wrapper) {
236
236
  }
237
237
  };
238
238
  }
239
+ export { staticMiddleware };
package/dist/router.d.ts CHANGED
@@ -12,7 +12,7 @@ export type ExtractParams<Path extends string> = Prettify<Path extends `${string
12
12
  } & ExtractParams<`/${Rest}`> : Path extends `${string}:${infer Param}` ? {
13
13
  [K in Param extends `${infer P}?` ? P : Param]: string;
14
14
  } : {}>;
15
- export type TypedRequestHandler<Params, ResponseBodySchema extends StandardSchemaV1 | undefined, RequestBodySchema extends StandardSchemaV1 | undefined, RequestQuery extends StandardSchemaV1 | undefined, RequestParamsSchema extends StandardSchemaV1 | undefined = undefined> = (req: Request<Params, never, RequestBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<RequestBodySchema> : unknown, RequestQuery extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<RequestQuery> : Record<string, string | string[]>>, res: Response<ResponseBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>, next: NextFunction) => ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown> | Promise<ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>>;
15
+ export type TypedRequestHandler<Params, ResponseBodySchema extends StandardSchemaV1 | undefined, RequestBodySchema extends StandardSchemaV1 | undefined, RequestQuery extends StandardSchemaV1 | undefined, RequestParamsSchema extends StandardSchemaV1 | undefined = undefined> = (req: Request<Params, any, RequestBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<RequestBodySchema> : unknown, RequestQuery extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<RequestQuery> : Record<string, string | string[]>>, res: Response<ResponseBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>, next: NextFunction) => ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown> | Promise<ResponseWrapper<ResponseBodySchema extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<ResponseBodySchema> : unknown>>;
16
16
  export declare class TypedRouter {
17
17
  #private;
18
18
  router: import("express-serve-static-core").Router;
package/dist/vite/ssr.js CHANGED
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { config } from '@devmoods/config';
4
4
  import express, {} from 'express';
5
- import { createServer as createViteServer } from 'vite';
5
+ import {} from 'vite';
6
6
  import {} from '../composeMiddleware.js';
7
7
  import {} from './types.js';
8
8
  import { generateCsrfToken } from '../csrf.js';
@@ -16,30 +16,28 @@ import { createFetchRequest } from '../webapi.js';
16
16
  export async function createSsrMiddleware({ root = process.cwd(), getInitialData, useDevServer = config.VITE_USE_DEV_SERVER, } = {}) {
17
17
  const resolve = (p) => path.resolve(root, p);
18
18
  let vite;
19
+ let render;
20
+ let assetLinks = { preload: '', css: '', js: '' };
19
21
  const middlewares = [];
20
22
  if (useDevServer) {
21
- vite = await createViteServer({
23
+ vite = await import('vite').then((mod) => mod.createServer({
22
24
  root,
23
25
  server: { middlewareMode: true },
24
26
  appType: 'custom',
25
- });
27
+ }));
28
+ render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render;
26
29
  middlewares.push(vite.middlewares);
27
30
  }
28
31
  else {
32
+ const result = await import(path.join(root, 'dist/server/entry-server.js'));
33
+ render = result.render;
34
+ assetLinks = await getAssetLinks(resolve('dist/client/.vite/manifest.json'), 'src/entry-client.tsx');
29
35
  middlewares.push(express.static(resolve('dist/client'), { index: false }));
30
36
  }
31
37
  middlewares.push(route(async (req, res) => {
32
38
  try {
33
- let render;
34
- if (useDevServer) {
35
- render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render;
36
- }
37
- else {
38
- const result = await import(path.join(root, 'dist/server/entry-server.js'));
39
- render = result.render;
40
- }
41
39
  const nonce = useCspNonce();
42
- const rendered = await render(createFetchRequest(req), {
40
+ const rendered = await render(createFetchRequest(req, res), {
43
41
  useDevServer,
44
42
  getInitialData: getInitialData || (async () => ({})),
45
43
  placeholders: `
@@ -61,13 +59,6 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
61
59
  publicUrl: new URL(config.PUBLIC_URL),
62
60
  csrfToken: generateCsrfToken(req, res),
63
61
  });
64
- const readManifest = async () => {
65
- if (useDevServer) {
66
- return { preload: '', css: '', js: '' };
67
- }
68
- const manifestPath = path.resolve(root, 'dist/client/.vite/manifest.json');
69
- return getAssetLinks(manifestPath, 'src/entry-client.tsx');
70
- };
71
62
  let html = rendered.html;
72
63
  const context = Object.assign({
73
64
  fouc: `
@@ -76,7 +67,7 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
76
67
  setTimeout(() => { document.getElementById('fouc').remove(); }, 2000);
77
68
  </script>
78
69
  `,
79
- }, rendered.context, await readManifest());
70
+ }, rendered.context, assetLinks);
80
71
  const ssrPlaceholders = Object.fromEntries(Object.entries(context).map(([k, v]) => [`ssr-${k}`, v.toString()]));
81
72
  // Add nonce to all bare script tags
82
73
  html = html.replace(/<script>/g, `<script nonce="${nonce}">`);
package/dist/webapi.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { type Request as ExpressRequest } from 'express';
1
+ import { type Request as ExpressRequest, type Response as ExpressResponse } from 'express';
2
2
  export declare function createFetchHeaders(requestHeaders: ExpressRequest['headers']): Headers;
3
- export declare function createFetchRequest(req: ExpressRequest): Request;
3
+ export declare function createFetchRequest(req: ExpressRequest, res: ExpressResponse): Request;
package/dist/webapi.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import {} from 'express';
2
- import { BadRequest } from './errors.js';
3
2
  export function createFetchHeaders(requestHeaders) {
4
3
  const headers = new Headers();
5
4
  for (const [key, values] of Object.entries(requestHeaders)) {
@@ -17,32 +16,25 @@ export function createFetchHeaders(requestHeaders) {
17
16
  }
18
17
  return headers;
19
18
  }
20
- export function createFetchRequest(req) {
21
- try {
22
- const origin = `${req.protocol}://${req.get('host')}`;
23
- // Note: This had to take originalUrl into account for presumably vite's proxying
24
- /* c8 ignore next 1 */
25
- const url = new URL(req.originalUrl || req.url, origin);
26
- const controller = new AbortController();
27
- /* c8 ignore next 3 */
28
- req.on('close', () => {
29
- controller.abort();
30
- });
31
- const init = {
32
- method: req.method,
33
- headers: createFetchHeaders(req.headers),
34
- signal: controller.signal,
35
- };
36
- if (req.method !== 'GET' && req.method !== 'HEAD') {
37
- init.body = req.body;
38
- }
39
- return new Request(url.href, init);
40
- /* c8 ignore next 7 */
41
- }
42
- catch (error) {
43
- if (error instanceof TypeError) {
44
- throw BadRequest('Invalid request');
45
- }
46
- throw error;
19
+ export function createFetchRequest(req, res) {
20
+ const origin = `${req.protocol}://${req.get('host')}`;
21
+ /* c8 ignore next 1 */
22
+ const url = new URL(req.originalUrl || req.url, origin);
23
+ let controller = new AbortController();
24
+ const init = {
25
+ method: req.method,
26
+ headers: createFetchHeaders(req.headers),
27
+ signal: controller.signal,
28
+ };
29
+ /* c8 ignore next 6 */
30
+ res.on('finish', () => {
31
+ controller = null;
32
+ });
33
+ res.on('close', () => {
34
+ controller?.abort();
35
+ });
36
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
37
+ init.body = req.body;
47
38
  }
39
+ return new Request(url.href, init);
48
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.66.1",
3
+ "version": "0.68.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -61,26 +61,25 @@
61
61
  "license": "ISC",
62
62
  "devDependencies": {
63
63
  "@vitejs/plugin-react": "^5.0.4",
64
- "arktype": "^2.1.22",
64
+ "arktype": "^2.1.23",
65
65
  "faktory-worker": "^4.7.1",
66
66
  "react": "^19.2.0",
67
67
  "react-dom": "^19.2.0",
68
- "vite": "^7.1.9"
68
+ "vite": "^7.1.11"
69
69
  },
70
70
  "dependencies": {
71
71
  "@devmoods/config": "0.2.0",
72
72
  "@devmoods/fetch": "^4.2.0",
73
73
  "@devmoods/observability": "0.1.1",
74
- "@devmoods/postgres": "0.2.0",
75
- "@hey-api/openapi-ts": "^0.85.1",
76
- "@sentry/node": "^10.19.0",
77
- "@sentry/node-native": "^10.19.0",
78
- "@sentry/profiling-node": "^10.19.0",
74
+ "@devmoods/postgres": "0.2.1",
75
+ "@hey-api/openapi-ts": "^0.86.1",
76
+ "@sentry/node": "^10.21.0",
77
+ "@sentry/node-native": "^10.21.0",
78
+ "@sentry/profiling-node": "^10.21.0",
79
79
  "@standard-schema/spec": "^1.0.0",
80
80
  "@types/bcrypt": "^6.0.0",
81
81
  "@types/cookie-parser": "^1.4.9",
82
82
  "@types/express": "^5.0.3",
83
- "@types/jsonwebtoken": "^9.0.10",
84
83
  "@types/koa-compose": "^3.2.8",
85
84
  "@types/nodemailer": "^7.0.2",
86
85
  "@types/qs": "^6.14.0",
@@ -92,9 +91,9 @@
92
91
  "cookie-parser": "^1.4.7",
93
92
  "dotenv": "^17.2.3",
94
93
  "express": "^4.21.2",
95
- "google-auth-library": "^10.4.0",
94
+ "google-auth-library": "^10.4.1",
96
95
  "helmet": "^8.1.0",
97
- "jsonwebtoken": "^9.0.2",
96
+ "jose": "^6.1.0",
98
97
  "nodemailer": "^7.0.9",
99
98
  "redis": "^5.8.3",
100
99
  "sonda": "^0.9.0",
@@ -112,5 +111,5 @@
112
111
  "engines": {
113
112
  "node": ">=20 <25"
114
113
  },
115
- "gitHead": "cc2eac4d3a68e9d2530ccffddaff0eee190381d0"
114
+ "gitHead": "721a709c087904a5cc1332d28a62857a82295fea"
116
115
  }