@devmoods/express-extras 0.62.0 → 0.63.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,7 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
48
48
  export interface AuthProvider<UserModel extends AnyUser> {
49
49
  setupRoutes(router: TypedRouter, context: AuthProviderContext<UserModel>): void;
50
50
  }
51
+ export type AuthIsAllowedFn<UserModel extends AnyUser> = (user: UserModel) => Promise<boolean> | boolean;
51
52
  export type AuthMiddlewareOptions<UserModel extends AnyUser> = false | {
52
- isAuthorized?: (user: UserModel) => Promise<boolean> | boolean;
53
+ isAuthorized?: AuthIsAllowedFn<UserModel>;
53
54
  };
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 { TokenStorage, type AnyUser, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
2
+ import { TokenStorage, type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
3
3
  import { type RedisClient } from './redis.js';
4
4
  export * from './auth/types.js';
5
5
  export * from './auth/emailAndPassword.js';
@@ -11,6 +11,7 @@ export interface CreateAuth<UserModel extends AnyUser> {
11
11
  useCurrentUser: () => UserModel | undefined;
12
12
  middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
13
13
  before: () => RequestHandler;
14
+ allow: (fn?: AuthIsAllowedFn<UserModel>) => RequestHandler;
14
15
  isAuthorized: (fn?: (user: UserModel) => boolean) => RequestHandler;
15
16
  createTemporaryToken(user: UserModel): Promise<string>;
16
17
  verifyTemporaryToken(token: string, user: UserModel | null): Promise<string>;
package/dist/auth.js CHANGED
@@ -130,7 +130,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
130
130
  authenticate = options;
131
131
  options = {};
132
132
  }
133
- return async (req, res, next) => {
133
+ return async function authenticationMiddleware(req, res, next) {
134
134
  try {
135
135
  const token = extractAccessTokenFromRequest(req);
136
136
  if (!token) {
@@ -192,6 +192,36 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
192
192
  }
193
193
  };
194
194
  }
195
+ function allow(isAuthorized = () => true) {
196
+ return async function authorizationMiddleware(req, res, next) {
197
+ try {
198
+ const user = useCurrentUser();
199
+ if (!user) {
200
+ throw new InvalidToken('User not logged in');
201
+ }
202
+ if (isAuthorized && !(await isAuthorized(user))) {
203
+ throw new PermissionDenied('Permission denied');
204
+ }
205
+ next();
206
+ }
207
+ catch (error) {
208
+ logger.error(error);
209
+ if (error instanceof PermissionDenied) {
210
+ if (req.accepts('html')) {
211
+ return res.redirect(loginUrl);
212
+ }
213
+ return next(Forbidden('Permission denied'));
214
+ }
215
+ if (error instanceof InvalidToken) {
216
+ if (req.accepts('html')) {
217
+ return res.redirect(loginUrl);
218
+ }
219
+ return next(Unauthorized(`Invalid token: ${error.message}`));
220
+ }
221
+ next(error);
222
+ }
223
+ };
224
+ }
195
225
  async function createOtp(user) {
196
226
  const otp = crypto.randomInt(100000, 999999).toString();
197
227
  await tokenStorage.set(otp, String(user.id), config.value.AUTH_OTP_EXPIRES_IN);
@@ -265,6 +295,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
265
295
  return storedUid;
266
296
  }
267
297
  return {
298
+ allow,
268
299
  before: () => middleware(false),
269
300
  isAuthorized: (fn) => middleware({ isAuthorized: fn || (() => true) }),
270
301
  createRouter: () => createAuthRouter().getRouter(),
package/dist/context.d.ts CHANGED
@@ -20,3 +20,5 @@ export declare function setContext<T>(key: string, value: T): void;
20
20
  * Obtain the requestId from the current async request context.
21
21
  */
22
22
  export declare function useRequestId(): string | undefined;
23
+ export declare const ANONYMOUS_SESSION_KEY = "anonymous-session";
24
+ export declare function useAnonymousSession(): string | undefined;
package/dist/context.js CHANGED
@@ -42,3 +42,7 @@ export function setContext(key, value) {
42
42
  export function useRequestId() {
43
43
  return useContext(REQUEST_ID);
44
44
  }
45
+ export const ANONYMOUS_SESSION_KEY = 'anonymous-session';
46
+ export function useAnonymousSession() {
47
+ return useContext(ANONYMOUS_SESSION_KEY);
48
+ }
package/dist/faktory.js CHANGED
@@ -82,6 +82,10 @@ export async function createFaktoryWorker({ queueName, jobs, faktory: createFact
82
82
  faktory.use(async (ctx, next) => {
83
83
  const jobId = ctx.job.jid;
84
84
  const timer = startTimer();
85
+ logger.info('job started', {
86
+ jobName: ctx.job.jobtype,
87
+ jobId,
88
+ });
85
89
  await context.run(new Map(), async () => {
86
90
  const store = context.getStore();
87
91
  if (store) {
package/dist/logger.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as os from 'node:os';
2
2
  import * as Sentry from '@sentry/node';
3
3
  import { config } from './config.js';
4
- import { useRequestId } from './context.js';
4
+ import { useAnonymousSession, useRequestId } from './context.js';
5
5
  const LEVELS = {
6
6
  TRACE: 10,
7
7
  DEBUG: 20,
@@ -21,6 +21,7 @@ function defaultGetContext() {
21
21
  return {
22
22
  app: nameArgIndex > -1 ? process.argv.at(nameArgIndex + 1) : undefined,
23
23
  requestId: useRequestId(),
24
+ sessionId: useAnonymousSession(),
24
25
  ..._globalGetContext?.(),
25
26
  };
26
27
  }
@@ -1,6 +1,7 @@
1
1
  import { type ServerResponse, type IncomingMessage } from 'node:http';
2
2
  import { compose } from 'compose-middleware';
3
3
  import { type NextFunction, type RequestHandler, type Request, type Response, json as jsonParser, urlencoded as urlencodedParser } from 'express';
4
+ import { type createAuth } from './auth.js';
4
5
  import { type CsrfMiddlewareOptions } from './csrf.js';
5
6
  export { urlencodedParser, jsonParser };
6
7
  export { asyncMiddleware as route };
@@ -43,12 +44,16 @@ export interface BeforeMiddlewareOptions {
43
44
  * Custom CSP directives
44
45
  */
45
46
  cspDirectives?: (defaults: BeforeMiddlewareCSPDirectives) => BeforeMiddlewareCSPDirectives;
47
+ /**
48
+ * Auth middleware needs to be available early before request logging happens.
49
+ */
50
+ auth?: ReturnType<typeof createAuth>;
46
51
  }
47
52
  export declare function useCspNonce(): string;
48
53
  /**
49
54
  * Register middleware that must come before the app routes.
50
55
  */
51
- export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<any, any, void>;
56
+ export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, auth, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<any, any, void>;
52
57
  /**
53
58
  * Middleware that sends a 404
54
59
  *
@@ -56,6 +61,10 @@ export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf
56
61
  * unknown routes.
57
62
  */
58
63
  export declare function notFound(): RequestHandler;
64
+ /**
65
+ * Set a session cookie for the request
66
+ */
67
+ export declare function setAnonymousCookie(): RequestHandler;
59
68
  /**
60
69
  * Register middlewares that must come after the app routes.
61
70
  */
@@ -4,7 +4,9 @@ import { compose } from 'compose-middleware';
4
4
  import cookieParser from 'cookie-parser';
5
5
  import { json as jsonParser, urlencoded as urlencodedParser, } from 'express';
6
6
  import helmet from 'helmet';
7
- import { setContext, useContext, withRequestContext } from './context.js';
7
+ import {} from './auth.js';
8
+ import { config } from './config.js';
9
+ import { ANONYMOUS_SESSION_KEY, setContext, useContext, withRequestContext, } from './context.js';
8
10
  import { csrf, doubleCsrfProtection, } from './csrf.js';
9
11
  import { NotFound } from './errors.js';
10
12
  import { getLogger } from './logger.js';
@@ -20,14 +22,17 @@ export function useCspNonce() {
20
22
  /**
21
23
  * Register middleware that must come before the app routes.
22
24
  */
23
- export function before({ cookies = false, helmet: useHelmet = true, json: useJson = true, csrf: useCsrf = false, urlencoded: useUrlencoded = false, allowUnsafeInlineScripts = false, cspDirectives = (x) => x, } = {}) {
24
- return compose([
25
+ export function before({ cookies = true, helmet: useHelmet = true, json: useJson = true, csrf: useCsrf = false, urlencoded: useUrlencoded = false, allowUnsafeInlineScripts = false, cspDirectives = (x) => x, auth, } = {}) {
26
+ const middleware = compose([
25
27
  withRequestContext(),
28
+ (cookies || useCsrf) && cookieParser(),
29
+ cookies && setAnonymousCookie(),
26
30
  typeof useCsrf === 'object' && useCsrf != null
27
31
  ? csrf(useCsrf)
28
32
  : useCsrf
29
33
  ? csrf()
30
34
  : undefined,
35
+ auth && auth.before(),
31
36
  logRequestDuration(),
32
37
  setCspNonce(),
33
38
  useHelmet &&
@@ -63,9 +68,12 @@ export function before({ cookies = false, helmet: useHelmet = true, json: useJso
63
68
  }),
64
69
  useJson && jsonParser(),
65
70
  useUrlencoded && urlencodedParser({ extended: true }),
66
- (cookies || useCsrf) && cookieParser(),
67
71
  analyticsProxy(),
68
72
  ].filter(Boolean));
73
+ Object.defineProperty(middleware, 'name', {
74
+ value: before.name,
75
+ });
76
+ return middleware;
69
77
  }
70
78
  function logRequestDuration() {
71
79
  const FILTERED_PATHS = [
@@ -77,7 +85,7 @@ function logRequestDuration() {
77
85
  '/@id',
78
86
  '/analytics.js',
79
87
  ];
80
- return (req, res, next) => {
88
+ return function logRequestDurationMiddleware(req, res, next) {
81
89
  if (FILTERED_PATHS.some((p) => req.path.startsWith(p))) {
82
90
  return next();
83
91
  }
@@ -86,7 +94,13 @@ function logRequestDuration() {
86
94
  method: req.method,
87
95
  url: req.originalUrl,
88
96
  path: req.path,
97
+ query: req.query,
89
98
  ip: req.ip,
99
+ userAgent: req.headers['user-agent'] ?? 'unknown',
100
+ accept: req.headers['accept'] ?? 'unknown',
101
+ host: req.headers['host'] ?? 'unknown',
102
+ referer: req.headers['referer'] ?? 'unknown',
103
+ origin: req.headers['origin'] ?? 'unknown',
90
104
  });
91
105
  res.on('finish', () => {
92
106
  const duration = timer.getDuration();
@@ -108,7 +122,7 @@ function logRequestDuration() {
108
122
  * unknown routes.
109
123
  */
110
124
  export function notFound() {
111
- return (req, res, next) => {
125
+ return function notFoundMiddleware(req, res, next) {
112
126
  next(NotFound(`Could not ${req.method} ${req.path}`));
113
127
  };
114
128
  }
@@ -116,18 +130,41 @@ export function notFound() {
116
130
  * Set a CSP nonce for the request
117
131
  */
118
132
  function setCspNonce() {
119
- return (req, res, next) => {
133
+ return function setCspNonceMiddleware(req, res, next) {
120
134
  setContext(CSP_NONCE_KEY, crypto.randomBytes(16).toString('base64'));
121
135
  next();
122
136
  };
123
137
  }
138
+ /**
139
+ * Set a session cookie for the request
140
+ */
141
+ export function setAnonymousCookie() {
142
+ return function setAnonymousCookieMiddleware(req, res, next) {
143
+ const cookieName = 'aid';
144
+ let sessionId = req.cookies?.[cookieName];
145
+ // Generate new sessionId if none exists
146
+ if (!sessionId) {
147
+ sessionId = crypto.randomUUID();
148
+ // Set the cookie
149
+ res.cookie(cookieName, sessionId, {
150
+ maxAge: 365 * 24 * 60 * 60 * 1000,
151
+ httpOnly: true,
152
+ secure: process.env.NODE_ENV === 'production',
153
+ sameSite: config.value.AUTH_COOKIE_SAME_SITE,
154
+ });
155
+ }
156
+ // Store sessionId in request context
157
+ setContext(ANONYMOUS_SESSION_KEY, sessionId);
158
+ next();
159
+ };
160
+ }
124
161
  /**
125
162
  * In production, we could directly proxy the url
126
163
  * to an analytics service from nginx, but in dev
127
164
  * we serve a dummy script to avoid 404.
128
165
  */
129
166
  function analyticsProxy() {
130
- return (req, res, next) => {
167
+ return function analyticsProxyMiddleware(req, res, next) {
131
168
  if (req.originalUrl === '/analytics.js') {
132
169
  res.setHeader('Content-type', 'text/javascript');
133
170
  res.status(200).end('// no-op analytics proxy');
package/dist/postgres.js CHANGED
@@ -244,7 +244,7 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
244
244
  };
245
245
  }
246
246
  export function withTransaction(postgres) {
247
- return async (req, res, next) => {
247
+ return async function withTransactionMiddleware(req, res, next) {
248
248
  try {
249
249
  await postgres.transaction(async () => {
250
250
  await new Promise((resolve, reject) => {
package/dist/validate.js CHANGED
@@ -80,6 +80,9 @@ export function validateStandardSchema(schemas) {
80
80
  metadata.responseSchema = toJsonSchema(schemas.response);
81
81
  }
82
82
  middleware.metadata = metadata;
83
+ Object.defineProperty(middleware, 'name', {
84
+ value: validateStandardSchema.name,
85
+ });
83
86
  return middleware;
84
87
  }
85
88
  function toJsonSchema(standardSchema) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.62.0",
3
+ "version": "0.63.1",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -59,13 +59,13 @@
59
59
  "ts-node": "10.9.2",
60
60
  "tsx": "^4.20.5",
61
61
  "typescript": "5.9.2",
62
- "vite": "^7.1.4",
62
+ "vite": "^7.1.5",
63
63
  "vitest": "^3.2.4",
64
64
  "vitest-fail-on-console": "^0.10.1"
65
65
  },
66
66
  "dependencies": {
67
67
  "@devmoods/fetch": "^4.2.0",
68
- "@hey-api/openapi-ts": "^0.82.4",
68
+ "@hey-api/openapi-ts": "^0.82.5",
69
69
  "@sentry/node": "^10.10.0",
70
70
  "@sentry/profiling-node": "^10.10.0",
71
71
  "@standard-schema/spec": "^1.0.0",
@@ -81,7 +81,7 @@
81
81
  "ajv": "^8.17.1",
82
82
  "argon2": "^0.44.0",
83
83
  "bcrypt": "^6.0.0",
84
- "chalk": "^5.6.0",
84
+ "chalk": "^5.6.2",
85
85
  "commander": "^14.0.0",
86
86
  "compose-middleware": "^5.0.1",
87
87
  "cookie-parser": "^1.4.7",