@devmoods/express-extras 0.19.0 → 0.20.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.
package/README.md CHANGED
@@ -77,7 +77,7 @@ app.get(
77
77
  route(async () => {
78
78
  const posts = await postgres.query('SELECT * FROM posts LIMIT 10');
79
79
  return posts;
80
- })
80
+ }),
81
81
  );
82
82
 
83
83
  app.post(
@@ -100,13 +100,13 @@ app.post(
100
100
  const post = { title, content };
101
101
 
102
102
  await postgres.insert(
103
- sql`INSERT INTO posts (title, content) VALUES (${title}, ${content})`
103
+ sql`INSERT INTO posts (title, content) VALUES (${title}, ${content})`,
104
104
  );
105
105
 
106
106
  slack.postMessage(`New post created with title ${title}`);
107
107
 
108
108
  return [201, post];
109
- })
109
+ }),
110
110
  );
111
111
 
112
112
  app.use((req, res, next) => {
@@ -118,7 +118,7 @@ app.use(
118
118
  shouldHandleError(error) {
119
119
  return true;
120
120
  },
121
- })
121
+ }),
122
122
  );
123
123
 
124
124
  app.use(
@@ -130,7 +130,7 @@ app.use(
130
130
  logger.error(error);
131
131
  }
132
132
  },
133
- })
133
+ }),
134
134
  );
135
135
 
136
136
  listen(server, {
package/dist/auth.d.ts CHANGED
@@ -24,6 +24,14 @@ export declare function createAuth<UserModel extends Record<string, any>>({ getU
24
24
  * of `req`.
25
25
  */
26
26
  export declare function parseAuthorizationHeader(req: Request): string | null;
27
+ /**
28
+ * Extract an access token from the `Request`-object.
29
+ *
30
+ * Checks multiple places and uses the first match. Priority:
31
+ * 1. Request header `Authorization: `
32
+ * 2. Cookie named `access_token`
33
+ */
34
+ export declare function extractAccessTokenFromRequest(req: Request): string | null;
27
35
  export declare function setAccessTokenCookie(res: Response, token: string | null): void;
28
36
  export declare function checkIfPasswordsMatch(realPassword: string, attemptedPassword: string): Promise<boolean>;
29
37
  export declare function hashPassword(password: string): Promise<string>;
package/dist/auth.js CHANGED
@@ -16,6 +16,7 @@ export function createAuthToken(payload, secret = process.env.APP_JWT_SECRET) {
16
16
  }
17
17
  export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, onResetRequested, onPasswordChanged, }) {
18
18
  const useCurrentUser = () => useContext('currentUser');
19
+ const setCurrentUser = (user) => setContext('currentUser', user);
19
20
  const createRouter = () => {
20
21
  const router = Router();
21
22
  router.get('/me', middleware(), route(() => {
@@ -36,6 +37,8 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
36
37
  const token = createAuthToken(filterUser(user));
37
38
  await redis.set(token, '1', { EX: tokenExpiry });
38
39
  setAccessTokenCookie(res, token);
40
+ setCurrentUser(user);
41
+ logger.info('access token created');
39
42
  return [201, { token }];
40
43
  }
41
44
  catch (error) {
@@ -75,12 +78,14 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
75
78
  const { token } = req.params;
76
79
  const email = await redis.get(token);
77
80
  if (!email) {
81
+ logger.info('invalid reset token', { token });
78
82
  throw BadRequest('invalid reset token');
79
83
  }
80
84
  const user = await emailAndPassword(email, skipPasswordCheck);
85
+ setCurrentUser(user);
81
86
  await onPasswordChanged?.(user, await hashPassword(password));
82
87
  await redis.del(token);
83
- logger.info('user changed password', { email });
88
+ logger.info('user changed password');
84
89
  return 200;
85
90
  }));
86
91
  }
@@ -88,6 +93,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
88
93
  const token = req.params.token;
89
94
  const email = await redis.get(token);
90
95
  if (!email) {
96
+ logger.info('invalid reset token', { token });
91
97
  throw NotFound();
92
98
  }
93
99
  return 200;
@@ -110,6 +116,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
110
116
  EX: TEMP_TOKEN_EXIRES_IN,
111
117
  });
112
118
  await onResetRequested?.(user, temporaryToken);
119
+ logger.info('password reset request', { email });
113
120
  }
114
121
  catch (error) {
115
122
  logger.error(error, 'auth reset failed');
@@ -125,6 +132,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
125
132
  const token = createAuthToken(rest);
126
133
  await redis.set(token, '1', { EX: tokenExpiry });
127
134
  setAccessTokenCookie(res, token);
135
+ logger.info('refreshed token', { user });
128
136
  return { token };
129
137
  }));
130
138
  return router;
@@ -142,6 +150,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
142
150
  const valid = await redis.get(token);
143
151
  if (!valid) {
144
152
  if (!authorize) {
153
+ logger.warn('request with invalid access token');
145
154
  return next();
146
155
  }
147
156
  throw new InvalidToken('token expired');
@@ -151,7 +160,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
151
160
  throw new Error('Failed to decode token');
152
161
  }
153
162
  const user = await getUserByToken({ ...decoded });
154
- setContext('currentUser', user);
163
+ setCurrentUser(user);
155
164
  next();
156
165
  }
157
166
  catch (error) {
@@ -190,7 +199,14 @@ export function parseAuthorizationHeader(req) {
190
199
  }
191
200
  return token;
192
201
  }
193
- function extractAccessTokenFromRequest(req) {
202
+ /**
203
+ * Extract an access token from the `Request`-object.
204
+ *
205
+ * Checks multiple places and uses the first match. Priority:
206
+ * 1. Request header `Authorization: `
207
+ * 2. Cookie named `access_token`
208
+ */
209
+ export function extractAccessTokenFromRequest(req) {
194
210
  const authorizationHeader = parseAuthorizationHeader(req);
195
211
  if (authorizationHeader) {
196
212
  return authorizationHeader;
@@ -226,7 +242,7 @@ export async function checkIfPasswordsMatch(realPassword, attemptedPassword) {
226
242
  }
227
243
  const validPassword = await bcrypt.compare(attemptedPassword, realPassword);
228
244
  if (!validPassword) {
229
- throw new Error(`Passwords does not match`);
245
+ throw new Error(`Passwords do not match`);
230
246
  }
231
247
  return true;
232
248
  }
package/dist/context.d.ts CHANGED
@@ -10,16 +10,8 @@ export declare function withRequestContext({ getRequestId, }?: RequestContextOpt
10
10
  * Return a value from the request context
11
11
  */
12
12
  export declare function useContext<T>(key: string): T | undefined;
13
- /**
14
- * @deprecated
15
- */
16
- export declare const getContext: typeof useContext;
17
13
  /**
18
14
  * Set a value on the request context
19
15
  */
20
- export declare function setContext<T>(key: string, value: T): any;
16
+ export declare function setContext<T>(key: string, value: T): Map<string, any> | undefined;
21
17
  export declare function useRequestId(): string | undefined;
22
- /**
23
- * @deprecated
24
- */
25
- export declare const getRequestId: typeof useRequestId;
package/dist/context.js CHANGED
@@ -1,14 +1,18 @@
1
- import { createNamespace } from 'cls-hooked';
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { v4 as uuid } from 'uuid';
3
- const requestContext = createNamespace('@requestContext');
3
+ const requestContext = new AsyncLocalStorage();
4
+ const REQUEST_ID = 'requestId';
4
5
  /**
5
6
  * Middleware for setting a `requestId` on the request context.
6
7
  */
7
8
  export function withRequestContext({ getRequestId = (req) => req.headers['X-Request-ID'], } = {}) {
8
9
  return (req, res, next) => {
9
10
  const requestId = getRequestId(req) || uuid();
10
- requestContext.run(() => {
11
- requestContext.set('requestId', requestId);
11
+ requestContext.run(new Map(), () => {
12
+ const store = requestContext.getStore();
13
+ if (store) {
14
+ store.set(REQUEST_ID, requestId);
15
+ }
12
16
  next();
13
17
  });
14
18
  };
@@ -17,26 +21,20 @@ export function withRequestContext({ getRequestId = (req) => req.headers['X-Requ
17
21
  * Return a value from the request context
18
22
  */
19
23
  export function useContext(key) {
20
- if (requestContext && requestContext.active) {
21
- return requestContext.get(key);
24
+ const store = requestContext.getStore();
25
+ if (store) {
26
+ return store.get(key);
22
27
  }
23
28
  }
24
- /**
25
- * @deprecated
26
- */
27
- export const getContext = useContext;
28
29
  /**
29
30
  * Set a value on the request context
30
31
  */
31
32
  export function setContext(key, value) {
32
- if (requestContext && requestContext.active) {
33
- return requestContext.set(key, value);
33
+ const store = requestContext.getStore();
34
+ if (store) {
35
+ return store.set(key, value);
34
36
  }
35
37
  }
36
38
  export function useRequestId() {
37
- return useContext('requestId');
39
+ return useContext(REQUEST_ID);
38
40
  }
39
- /**
40
- * @deprecated
41
- */
42
- export const getRequestId = useRequestId;
package/dist/email.js CHANGED
@@ -15,7 +15,7 @@ export function createEmail({ host, port, user, pass, senderName, senderAddress,
15
15
  text: message,
16
16
  ...rest,
17
17
  });
18
- getLogger('core').info('email-sent', { email: result });
18
+ getLogger('email').info('email-sent', { email: result });
19
19
  };
20
20
  return { send, transporter };
21
21
  }
package/dist/errors.js CHANGED
@@ -44,8 +44,7 @@ function convertToAPIError(error) {
44
44
  return error;
45
45
  }
46
46
  function _errorHandler({ onError }) {
47
- return (err, req, res, next // eslint-disable-line
48
- ) => {
47
+ return (err, req, res, next) => {
49
48
  if (!err) {
50
49
  err = makeError(500);
51
50
  }
package/dist/listen.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createTerminus } from '@godaddy/terminus';
2
2
  import { getLogger } from './logger.js';
3
- const logger = getLogger();
3
+ const logger = getLogger('core');
4
4
  const READINESS_PERIOD_MS = parseInt(process.env.DM_READINESS_PERIOD_MS || '', 10) || 0;
5
5
  const FORCE_EXIT_TIMEOUT = parseInt(process.env.DM_FORCE_EXIT_TIMEOUT || '', 10) || 1000;
6
6
  /**
package/dist/logger.d.ts CHANGED
@@ -18,29 +18,17 @@ export declare function attachSentryClientToLoggers(sentryClient: CreateSentry['
18
18
  export declare function setGlobalLoggerContext(getContext: () => LoggerContext): void;
19
19
  export type Logger = ReturnType<typeof createLogger>;
20
20
  export declare function createLogger({ name, getContext, silent, }: LoggerOptions): {
21
- debug(message: string, context?: {
22
- [key: string]: any;
23
- }): void;
24
- info(message: string, context?: {
25
- [key: string]: any;
26
- }): void;
27
- warn(message: string, context?: {
28
- [key: string]: any;
29
- }): void;
21
+ debug(message: string, context?: LoggerContext): void;
22
+ info(message: string, context?: LoggerContext): void;
23
+ warn(message: string, context?: LoggerContext): void;
30
24
  error(error: Error | null, message?: string): void;
31
25
  fatal(error: Error | null, message?: string): void;
32
26
  };
33
27
  export declare function getLogger(name?: string): Logger;
34
28
  export declare function getLoggers(): Map<string, {
35
- debug(message: string, context?: {
36
- [key: string]: any;
37
- }): void;
38
- info(message: string, context?: {
39
- [key: string]: any;
40
- }): void;
41
- warn(message: string, context?: {
42
- [key: string]: any;
43
- }): void;
29
+ debug(message: string, context?: LoggerContext): void;
30
+ info(message: string, context?: LoggerContext): void;
31
+ warn(message: string, context?: LoggerContext): void;
44
32
  error(error: Error | null, message?: string | undefined): void;
45
33
  fatal(error: Error | null, message?: string | undefined): void;
46
34
  }>;
package/dist/logger.js CHANGED
@@ -1,5 +1,14 @@
1
- import bunyan from 'bunyan';
1
+ import * as os from 'node:os';
2
2
  import { useRequestId } from './context.js';
3
+ const LEVELS = {
4
+ TRACE: 10,
5
+ DEBUG: 20,
6
+ INFO: 30,
7
+ WARN: 40,
8
+ ERROR: 50,
9
+ FATAL: 60,
10
+ };
11
+ const LEVELS_TO_NAME = Object.fromEntries(Object.entries(LEVELS).map(([name, level]) => [level, name.toLowerCase()]));
3
12
  const _loggers = new Map();
4
13
  let _sentryClient;
5
14
  export function attachSentryClientToLoggers(sentryClient) {
@@ -19,51 +28,61 @@ export function createLogger({ name, getContext, silent = () => process.env.NODE
19
28
  if (_loggers.has(name)) {
20
29
  throw new Error(`A logger named ${name} already exists`);
21
30
  }
22
- const logger = bunyan.createLogger({
23
- name,
24
- serializers: bunyan.stdSerializers,
25
- });
26
- _loggers.set(name, logger);
27
- if (silent()) {
28
- logger.level(bunyan.FATAL + 1);
29
- }
30
- function getFinalContext() {
31
+ const hostname = os.hostname();
32
+ const pid = process.pid;
33
+ const logLevel = silent() ? LEVELS.FATAL + 1 : LEVELS.INFO;
34
+ function getFinalContext(context) {
31
35
  return {
32
36
  ...defaultGetContext(),
33
37
  ...getContext?.(),
38
+ ...context,
34
39
  };
35
40
  }
36
- return {
41
+ function log(level, message, context) {
42
+ const payload = JSON.stringify({
43
+ level: LEVELS_TO_NAME[level],
44
+ hostname,
45
+ pid,
46
+ message,
47
+ ...context,
48
+ time: new Date().toISOString(),
49
+ v: 1,
50
+ });
51
+ if (level >= logLevel) {
52
+ console.log(payload);
53
+ }
54
+ }
55
+ const api = {
37
56
  debug(message, context = {}) {
38
- return logger.child(getFinalContext(), true).debug(context, message);
57
+ return log(LEVELS.DEBUG, message, getFinalContext(context));
39
58
  },
40
59
  info(message, context = {}) {
41
- return logger.child(getFinalContext(), true).info(context, message);
60
+ return log(LEVELS.INFO, message, getFinalContext(context));
42
61
  },
43
62
  warn(message, context = {}) {
44
- return logger.child(getFinalContext(), true).warn(context, message);
63
+ return log(LEVELS.WARN, message, getFinalContext(context));
45
64
  },
46
65
  error(error, message) {
47
- const context = getFinalContext();
66
+ const context = getFinalContext({ err: error?.stack });
48
67
  if (_sentryClient != null) {
49
68
  _sentryClient.captureException(error, {
50
69
  tags: context,
51
70
  });
52
71
  }
53
- return logger
54
- .child(context, true)
55
- .error(error, message || error?.message);
72
+ return log(LEVELS.ERROR, message || error?.message || '', context);
56
73
  },
57
74
  fatal(error, message) {
58
- const context = getFinalContext();
75
+ const context = getFinalContext({ err: error });
59
76
  if (_sentryClient != null) {
60
77
  _sentryClient.captureException(error, {
61
78
  tags: context,
62
79
  });
63
80
  }
64
- return logger.child(context, true).fatal(error, message);
81
+ return log(LEVELS.FATAL, message || error?.message || '', context);
65
82
  },
66
83
  };
84
+ _loggers.set(name, api);
85
+ return api;
67
86
  }
68
87
  export function getLogger(name = 'default') {
69
88
  const logger = _loggers.get(name);
@@ -48,9 +48,9 @@ export function before({ cookies = false, helmet: useHelmet = true, json: useJso
48
48
  function logRequestDuration() {
49
49
  const getDurationInMilliseconds = (start) => {
50
50
  const NS_PER_SEC = 1e9;
51
- const NS_TO_MS = 1e6;
51
+ const NS_TO_S = 1e9;
52
52
  const diff = process.hrtime(start);
53
- return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS;
53
+ return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_S;
54
54
  };
55
55
  return (req, res, next) => {
56
56
  const start = process.hrtime();
@@ -58,6 +58,7 @@ function logRequestDuration() {
58
58
  method: req.method,
59
59
  url: req.originalUrl,
60
60
  path: req.path,
61
+ ip: req.ip,
61
62
  });
62
63
  res.on('finish', () => {
63
64
  const duration = getDurationInMilliseconds(start);
@@ -66,6 +67,7 @@ function logRequestDuration() {
66
67
  url: req.originalUrl,
67
68
  path: req.path,
68
69
  duration,
70
+ status: res.statusCode,
69
71
  });
70
72
  });
71
73
  next();
package/dist/slack.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createFetch } from '@devmoods/fetch';
2
2
  import { getLogger } from './logger.js';
3
- const logger = getLogger();
3
+ const logger = getLogger('slack');
4
4
  /**
5
5
  * Send messages to Slack using Incoming Webhooks
6
6
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,6 +8,17 @@
8
8
  "bin": {
9
9
  "migrate": "bin/migrate"
10
10
  },
11
+ "scripts": {
12
+ "start": "ts-node --esm example/index.ts",
13
+ "clean": "rm -rf dist",
14
+ "lint": "eslint src",
15
+ "test": "NODE_ENV=test vitest",
16
+ "build": "pnpm run clean && tsc --project tsconfig.build.json",
17
+ "prepublishOnly": "pnpm run clean && pnpm run build",
18
+ "prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
19
+ "import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
20
+ "format": "pnpm run import-sort && pnpm run prettier"
21
+ },
11
22
  "author": {
12
23
  "name": "hanse",
13
24
  "email": "hanse@koren.im"
@@ -51,8 +62,6 @@
51
62
  "@sentry/node": "^7.60.1",
52
63
  "@types/bcrypt": "^5.0.0",
53
64
  "@types/body-parser": "^1.19.2",
54
- "@types/bunyan": "^1.8.8",
55
- "@types/cls-hooked": "^4.3.4",
56
65
  "@types/cookie-parser": "^1.4.3",
57
66
  "@types/express": "^4.17.17",
58
67
  "@types/jsonwebtoken": "^9.0.2",
@@ -62,8 +71,6 @@
62
71
  "ajv": "^8.12.0",
63
72
  "bcrypt": "^5.1.0",
64
73
  "body-parser": "^1.20.2",
65
- "bunyan": "^1.8.15",
66
- "cls-hooked": "^4.2.2",
67
74
  "compose-middleware": "^5.0.1",
68
75
  "cookie-parser": "^1.4.6",
69
76
  "dotenv": "^16.3.1",
@@ -92,15 +99,5 @@
92
99
  "@typescript-eslint/no-shadow": "off",
93
100
  "react-hooks/rules-of-hooks": "off"
94
101
  }
95
- },
96
- "scripts": {
97
- "start": "ts-node --esm example/index.ts",
98
- "clean": "rm -rf dist",
99
- "lint": "eslint src",
100
- "test": "NODE_ENV=test vitest",
101
- "build": "pnpm run clean && tsc --project tsconfig.build.json",
102
- "prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
103
- "import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
104
- "format": "pnpm run import-sort && pnpm run prettier"
105
102
  }
106
- }
103
+ }