@devmoods/express-extras 0.65.0 → 0.66.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.
Files changed (54) hide show
  1. package/dist/auth/emailAndPassword.js +4 -4
  2. package/dist/auth/errors.d.ts +2 -2
  3. package/dist/auth/errors.js +4 -4
  4. package/dist/auth/google.js +2 -2
  5. package/dist/auth/passwordless.js +1 -1
  6. package/dist/auth/token.js +3 -1
  7. package/dist/auth/tokenStorage.d.ts +16 -0
  8. package/dist/auth/tokenStorage.js +44 -0
  9. package/dist/auth/types.d.ts +1 -3
  10. package/dist/auth.d.ts +7 -15
  11. package/dist/auth.js +29 -41
  12. package/dist/channels.d.ts +19 -1
  13. package/dist/channels.js +23 -34
  14. package/dist/composeMiddleware.d.ts +15 -0
  15. package/dist/composeMiddleware.js +63 -0
  16. package/dist/config.d.ts +6 -0
  17. package/dist/config.js +3 -0
  18. package/dist/context.d.ts +4 -0
  19. package/dist/context.js +8 -0
  20. package/dist/csrf.d.ts +1 -0
  21. package/dist/csrf.js +6 -2
  22. package/dist/errors.d.ts +7 -7
  23. package/dist/errors.js +11 -11
  24. package/dist/faktory.d.ts +13 -11
  25. package/dist/faktory.js +6 -6
  26. package/dist/listen.d.ts +18 -9
  27. package/dist/listen.js +64 -38
  28. package/dist/logger.d.ts +23 -4
  29. package/dist/logger.js +65 -45
  30. package/dist/manage.d.ts +2 -2
  31. package/dist/manage.js +12 -12
  32. package/dist/middleware.d.ts +4 -2
  33. package/dist/middleware.js +10 -3
  34. package/dist/rate-limit.d.ts +14 -4
  35. package/dist/rate-limit.js +10 -5
  36. package/dist/redis.d.ts +0 -3
  37. package/dist/redis.js +0 -23
  38. package/dist/router.js +1 -1
  39. package/dist/routes.d.ts +5 -4
  40. package/dist/routes.js +34 -23
  41. package/dist/sentry.js +5 -3
  42. package/dist/streams.d.ts +7 -0
  43. package/dist/streams.js +29 -0
  44. package/dist/utils.d.ts +0 -2
  45. package/dist/utils.js +0 -8
  46. package/dist/validate.d.ts +33 -12
  47. package/dist/validate.js +6 -1
  48. package/dist/vite/defineViteConfig.js +1 -0
  49. package/dist/vite/ssr.d.ts +20 -1
  50. package/dist/vite/ssr.js +6 -1
  51. package/dist/vitest/matchers.d.ts +4 -0
  52. package/dist/vitest/matchers.js +17 -2
  53. package/dist/vitest/testClient.d.ts +1 -0
  54. package/package.json +18 -20
@@ -36,7 +36,7 @@ export function EmailAndPasswordAuthProvider(options) {
36
36
  catch (error) {
37
37
  logger.error(error);
38
38
  await context.jitter(timer.getDuration());
39
- throw Unauthorized(error.message);
39
+ throw Unauthorized(error.message, { cause: error });
40
40
  }
41
41
  });
42
42
  router.post('/change-password', {
@@ -52,8 +52,8 @@ export function EmailAndPasswordAuthProvider(options) {
52
52
  try {
53
53
  await context.verifyTemporaryToken(token, user);
54
54
  }
55
- catch {
56
- throw Unauthorized('Invalid token');
55
+ catch (error) {
56
+ throw Unauthorized('Invalid token', { cause: error });
57
57
  }
58
58
  context.setCurrentUser(user);
59
59
  await options.onPasswordChanged?.(user, await hashPassword(password), {
@@ -97,7 +97,7 @@ export function EmailAndPasswordAuthProvider(options) {
97
97
  user: context.filterUser(user),
98
98
  error: error.message,
99
99
  });
100
- throw Unauthorized('Invalid token');
100
+ throw Unauthorized('Invalid token', { cause: error });
101
101
  }
102
102
  context.setCurrentUser(user);
103
103
  await options.onEmailVerified?.(user);
@@ -1,6 +1,6 @@
1
1
  export declare class InvalidToken extends Error {
2
- constructor(message: string);
2
+ constructor(message: string, options?: ErrorOptions);
3
3
  }
4
4
  export declare class PermissionDenied extends Error {
5
- constructor(message: string);
5
+ constructor(message: string, options?: ErrorOptions);
6
6
  }
@@ -1,12 +1,12 @@
1
1
  export class InvalidToken extends Error {
2
- constructor(message) {
3
- super(message);
2
+ constructor(message, options) {
3
+ super(message, options);
4
4
  this.name = 'Invalid Token';
5
5
  }
6
6
  }
7
7
  export class PermissionDenied extends Error {
8
- constructor(message) {
9
- super(message);
8
+ constructor(message, options) {
9
+ super(message, options);
10
10
  this.name = 'Permission Denied';
11
11
  }
12
12
  }
@@ -20,7 +20,7 @@ export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, cl
20
20
  }
21
21
  catch (e) {
22
22
  logger.error(e);
23
- throw Unauthorized('Invalid authorization code');
23
+ throw Unauthorized('Invalid authorization code', { cause: e });
24
24
  }
25
25
  };
26
26
  const verifyIdToken = async (token) => {
@@ -73,7 +73,7 @@ export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, cl
73
73
  }
74
74
  catch (e) {
75
75
  logger.error(e);
76
- throw Unauthorized('Invalid state');
76
+ throw Unauthorized('Invalid state', { cause: e });
77
77
  }
78
78
  const { idToken } = await verifyAuthorizationCode(code);
79
79
  if (!idToken) {
@@ -35,7 +35,7 @@ export function PasswordlessAuthProvider(options) {
35
35
  const verify = tokenType === 'otp'
36
36
  ? context.verifyOtp
37
37
  : context.verifyTemporaryToken;
38
- const tokenValue = tokenType === 'otp' ? otp : temporaryToken;
38
+ const tokenValue = String(tokenType === 'otp' ? otp : temporaryToken);
39
39
  const uid = await verify(tokenValue, null, false);
40
40
  const user = await context.getUserById(uid);
41
41
  await verify(tokenValue, user);
@@ -44,7 +44,9 @@ export function decodeAccessToken(token, secret = config.AUTH_JWT_SECRET) {
44
44
  return { ...decoded };
45
45
  }
46
46
  catch (error) {
47
- throw new InvalidToken(`Failed to decode auth token: ${error.message}`);
47
+ throw new InvalidToken(`Failed to decode auth token: ${error.message}`, {
48
+ cause: error,
49
+ });
48
50
  }
49
51
  }
50
52
  /**
@@ -0,0 +1,16 @@
1
+ import { type RedisClient } from '../redis.js';
2
+ import { TokenStorage } from './types.js';
3
+ export declare class RedisTokenStorage extends TokenStorage {
4
+ redis: RedisClient;
5
+ keyPrefix: string;
6
+ constructor(redis: RedisClient, keyPrefix?: string);
7
+ set(token: string, userId: string, expiresIn: number): Promise<void>;
8
+ get(token: string): Promise<string | null>;
9
+ invalidate(token: string): Promise<void>;
10
+ }
11
+ export declare class MemoryTokenStorage extends TokenStorage {
12
+ #private;
13
+ set(token: string, userId: string, expiresIn: number): Promise<void>;
14
+ get(token: string): Promise<string | null>;
15
+ invalidate(token: string): Promise<void>;
16
+ }
@@ -0,0 +1,44 @@
1
+ import { config } from '@devmoods/config';
2
+ import {} from '../redis.js';
3
+ import { TokenStorage } from './types.js';
4
+ export class RedisTokenStorage extends TokenStorage {
5
+ redis;
6
+ keyPrefix;
7
+ constructor(redis, keyPrefix = `${config.REDIS_KEY_PREFIX}auth:`) {
8
+ super();
9
+ this.redis = redis;
10
+ this.keyPrefix = keyPrefix;
11
+ }
12
+ async set(token, userId, expiresIn) {
13
+ await this.redis.set(`${this.keyPrefix}${token}`, userId, {
14
+ EX: expiresIn,
15
+ });
16
+ }
17
+ async get(token) {
18
+ return this.redis.get(`${this.keyPrefix}${token}`);
19
+ }
20
+ async invalidate(token) {
21
+ await this.redis.del(`${this.keyPrefix}${token}`);
22
+ }
23
+ }
24
+ export class MemoryTokenStorage extends TokenStorage {
25
+ #tokens = new Map();
26
+ async set(token, userId, expiresIn) {
27
+ this.#tokens.set(token, [userId, Date.now() + expiresIn]);
28
+ }
29
+ async get(token) {
30
+ const entry = this.#tokens.get(token);
31
+ if (entry == null) {
32
+ return null;
33
+ }
34
+ const [value, expires] = entry;
35
+ if (expires != null && Date.now() >= expires) {
36
+ this.#tokens.delete(token);
37
+ return null;
38
+ }
39
+ return value;
40
+ }
41
+ async invalidate(token) {
42
+ this.#tokens.delete(token);
43
+ }
44
+ }
@@ -49,6 +49,4 @@ export interface AuthProvider<UserModel extends AnyUser> {
49
49
  setupRoutes(router: TypedRouter, context: AuthProviderContext<UserModel>): void;
50
50
  }
51
51
  export type AuthIsAllowedFn<UserModel extends AnyUser> = (user: UserModel) => Promise<boolean> | boolean;
52
- export type AuthMiddlewareOptions<UserModel extends AnyUser> = false | {
53
- isAuthorized?: AuthIsAllowedFn<UserModel>;
54
- };
52
+ export type AuthMiddlewareOptions<UserModel extends AnyUser> = false | {};
package/dist/auth.d.ts CHANGED
@@ -1,31 +1,23 @@
1
1
  import { type RequestHandler, type Response, type Request, type Router } from 'express';
2
- import { TokenStorage, type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
3
- import { type RedisClient } from './redis.js';
2
+ import { type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
4
3
  export * from './auth/types.js';
5
4
  export * from './auth/emailAndPassword.js';
6
5
  export * from './auth/passwordless.js';
7
6
  export * from './auth/token.js';
8
7
  export * from './auth/google.js';
8
+ export * from './auth/tokenStorage.js';
9
9
  export interface CreateAuth<UserModel extends AnyUser> {
10
10
  createRouter: () => Router;
11
11
  useCurrentUser: () => UserModel | undefined;
12
12
  middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
13
13
  before: () => RequestHandler;
14
- allow: (fn?: AuthIsAllowedFn<UserModel>) => RequestHandler;
15
- isAuthorized: (fn?: (user: UserModel) => boolean) => RequestHandler;
14
+ protect: (fn?: AuthIsAllowedFn<UserModel>) => RequestHandler;
16
15
  createTemporaryToken(user: UserModel | null): Promise<string>;
17
- verifyTemporaryToken(token: string, user: UserModel | null): Promise<string>;
16
+ verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
18
17
  decodeTemporaryToken(token: string): DecodedTemporaryToken;
19
18
  createOtp(user: UserModel): Promise<string>;
20
- verifyOtp(otp: string, user: UserModel | null): Promise<string>;
19
+ verifyOtp(otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
21
20
  logout: (req: Request, res: Response) => Promise<void>;
21
+ setSessionTokenVersion: (version: string) => void;
22
22
  }
23
- export declare class RedisTokenStorage extends TokenStorage {
24
- redis: RedisClient;
25
- keyPrefix: string;
26
- constructor(redis: RedisClient, keyPrefix?: string);
27
- set(token: string, userId: string, expiresIn: number): Promise<void>;
28
- get(token: string): Promise<string | null>;
29
- invalidate(token: string): Promise<void>;
30
- }
31
- export declare function createAuth<UserModel extends AnyUser>({ getUserById, filterUser, tokenStorage, providers, loginUrl, onUserLoggedIn, sessionTokenVersion, }: 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/auth.js CHANGED
@@ -3,11 +3,10 @@ import { config } from '@devmoods/config';
3
3
  import {} from 'express';
4
4
  import { InvalidToken, PermissionDenied } from './auth/errors.js';
5
5
  import { createAccessToken, decodeAccessToken, extractAccessTokenFromRequest, setAccessTokenCookie, tokenSchema, } from './auth/token.js';
6
- import { TokenStorage, } from './auth/types.js';
6
+ import {} from './auth/types.js';
7
7
  import { setRequestContext, useRequestContext } from './context.js';
8
8
  import { Forbidden, NotFound, Unauthorized } from './errors.js';
9
9
  import { getLogger } from './logger.js';
10
- import {} from './redis.js';
11
10
  import { createRouter } from './router.js';
12
11
  import { jsonSchema } from './validate.js';
13
12
  const logger = getLogger('auth');
@@ -16,28 +15,10 @@ export * from './auth/emailAndPassword.js';
16
15
  export * from './auth/passwordless.js';
17
16
  export * from './auth/token.js';
18
17
  export * from './auth/google.js';
19
- export class RedisTokenStorage extends TokenStorage {
20
- redis;
21
- keyPrefix;
22
- constructor(redis, keyPrefix = `${config.REDIS_KEY_PREFIX}auth:`) {
23
- super();
24
- this.redis = redis;
25
- this.keyPrefix = keyPrefix;
26
- }
27
- async set(token, userId, expiresIn) {
28
- await this.redis.set(`${this.keyPrefix}${token}`, userId, {
29
- EX: expiresIn,
30
- });
31
- }
32
- async get(token) {
33
- return this.redis.get(`${this.keyPrefix}${token}`);
34
- }
35
- async invalidate(token) {
36
- await this.redis.del(`${this.keyPrefix}${token}`);
37
- }
38
- }
18
+ export * from './auth/tokenStorage.js';
39
19
  const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
40
- export function createAuth({ getUserById, filterUser, tokenStorage, providers, loginUrl = '/', onUserLoggedIn, sessionTokenVersion = '1', }) {
20
+ export function createAuth({ getUserById, filterUser, tokenStorage, providers, loginUrl = '/', onUserLoggedIn, sessionTokenVersion: initialSessionTokenVersion = '1', }) {
21
+ let sessionTokenVersion = initialSessionTokenVersion;
41
22
  const useCurrentUser = () => useRequestContext('currentUser');
42
23
  const setCurrentUser = (user) => setRequestContext('currentUser', user);
43
24
  const login = async (req, res, user, isRefresh = false) => {
@@ -48,6 +29,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
48
29
  setCurrentUser(user);
49
30
  if (!isRefresh) {
50
31
  await onUserLoggedIn?.(user, {
32
+ /* c8 ignore next 2 */
51
33
  ip: req.ip ?? '',
52
34
  userAgent: req.headers['user-agent'] ?? '',
53
35
  sessionId,
@@ -64,10 +46,12 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
64
46
  };
65
47
  // Make timing attacks harder
66
48
  const jitter = (duration = 0) => {
49
+ /* c8 ignore start */
67
50
  if (process.env.NODE_ENV === 'test') {
68
51
  return Promise.resolve();
69
52
  }
70
53
  return new Promise((resolve) => void setTimeout(resolve, Math.max((0.5 - duration) * 1000, 200)));
54
+ /* c8 ignore end */
71
55
  };
72
56
  /**
73
57
  * API for auth providers to use
@@ -162,17 +146,18 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
162
146
  let user;
163
147
  try {
164
148
  user = await getUserById(decoded.uid);
149
+ if (!user) {
150
+ throw new Error('getUserById returned an empty result');
151
+ }
165
152
  }
166
- catch {
153
+ catch (error) {
167
154
  await logout(req, res);
168
- throw new InvalidToken('User not found');
155
+ throw new InvalidToken('User not found', { cause: error });
169
156
  }
170
157
  const tokenLifetimeMs = decoded.exp * 1000 - decoded.iat * 1000;
171
158
  const remainingMs = decoded.exp * 1000 - Date.now();
172
159
  const shouldRefresh = remainingMs < tokenLifetimeMs * 0.25;
173
- if (options.isAuthorized && !(await options.isAuthorized(user))) {
174
- throw new PermissionDenied('Permission denied');
175
- }
160
+ /* c8 ignore next 3 */
176
161
  if (shouldRefresh) {
177
162
  await providerContext.login(req, res, user, true);
178
163
  }
@@ -181,23 +166,18 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
181
166
  }
182
167
  catch (error) {
183
168
  logger.error(error);
184
- if (error instanceof PermissionDenied) {
185
- if (req.accepts('html')) {
186
- return res.redirect(loginUrl);
187
- }
188
- return next(Forbidden('Permission denied'));
189
- }
190
169
  if (error instanceof InvalidToken) {
191
170
  if (req.accepts('html')) {
192
171
  return res.redirect(loginUrl);
193
172
  }
194
- return next(Unauthorized(`Invalid token: ${error.message}`));
173
+ return next(Unauthorized(`Invalid token: ${error.message}`, { cause: error }));
195
174
  }
175
+ /* c8 ignore next 3 */
196
176
  next(error);
197
177
  }
198
178
  };
199
179
  }
200
- function allow(isAuthorized = () => true) {
180
+ function protect(isAuthorized = () => true) {
201
181
  return async function authorizationMiddleware(req, res, next) {
202
182
  try {
203
183
  const user = useCurrentUser();
@@ -215,14 +195,15 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
215
195
  if (req.accepts('html')) {
216
196
  return res.redirect(loginUrl);
217
197
  }
218
- return next(Forbidden('Permission denied'));
198
+ return next(Forbidden('Permission denied', { cause: error }));
219
199
  }
220
200
  if (error instanceof InvalidToken) {
221
201
  if (req.accepts('html')) {
222
202
  return res.redirect(loginUrl);
223
203
  }
224
- return next(Unauthorized(`Invalid token: ${error.message}`));
204
+ return next(Unauthorized(`Invalid token: ${error.message}`, { cause: error }));
225
205
  }
206
+ /* c8 ignore next 3 */
226
207
  next(error);
227
208
  }
228
209
  };
@@ -273,7 +254,12 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
273
254
  }
274
255
  }
275
256
  function decodeTemporaryToken(token) {
276
- return JSON.parse(Buffer.from(token, 'base64url').toString('utf-8'));
257
+ try {
258
+ return JSON.parse(Buffer.from(token, 'base64url').toString('utf-8'));
259
+ }
260
+ catch (error) {
261
+ throw new InvalidToken(`Invalid token not created by createTemporaryToken`, { cause: error });
262
+ }
277
263
  }
278
264
  async function verifyTemporaryToken(token, user, consume = true) {
279
265
  const decoded = decodeTemporaryToken(token);
@@ -302,9 +288,8 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
302
288
  return storedUid;
303
289
  }
304
290
  return {
305
- allow,
291
+ protect,
306
292
  before: () => middleware(false),
307
- isAuthorized: (fn) => middleware({ isAuthorized: fn || (() => true) }),
308
293
  createRouter: () => createAuthRouter().getRouter(),
309
294
  useCurrentUser,
310
295
  middleware,
@@ -314,5 +299,8 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
314
299
  createOtp,
315
300
  verifyOtp,
316
301
  logout,
302
+ setSessionTokenVersion: (version) => {
303
+ sessionTokenVersion = version;
304
+ },
317
305
  };
318
306
  }
@@ -6,7 +6,25 @@ export interface EventStreamSseOptions<Schema extends StandardSchemaV1> {
6
6
  channels: (req: Request) => string[] | Promise<string[]>;
7
7
  getIdFromMessage?: (message: StandardSchemaV1.InferOutput<Schema>) => string | undefined;
8
8
  }
9
- export declare function createEventStream<Channels extends string, Schema extends StandardSchemaV1>(redis: RedisClient, schema: Schema): {
9
+ export interface EventStreamSubscriber {
10
+ subscribe: (channels: string[], listener: (message: string) => void) => Promise<void>;
11
+ close: () => void;
12
+ }
13
+ export interface EventStreamBackend {
14
+ publish: (channel: string, message: string) => Promise<number>;
15
+ createSubscriber: () => Promise<EventStreamSubscriber>;
16
+ }
17
+ export declare class RedisEventStreamBackend implements EventStreamBackend {
18
+ redis: RedisClient;
19
+ constructor(redis: RedisClient);
20
+ publish(channel: string, message: string): Promise<number>;
21
+ createSubscriber(): Promise<RedisClient>;
22
+ }
23
+ export interface EventStreamOptions<Schema extends StandardSchemaV1> {
24
+ schema: Schema;
25
+ heartbeatInterval?: number;
26
+ }
27
+ export declare function createEventStream<Channels extends string, Schema extends StandardSchemaV1>(backend: EventStreamBackend, options: EventStreamOptions<Schema>): {
10
28
  sse: <const Path extends string>({ channels, getIdFromMessage, }: EventStreamSseOptions<Schema>) => TypedRequestHandler<ExtractParams<Path>, any, any, any>;
11
29
  publish: (channel: Channels, message: StandardSchemaV1.InferOutput<Schema>) => Promise<number>;
12
30
  close: () => Promise<void>;
package/dist/channels.js CHANGED
@@ -5,65 +5,53 @@ import { requestContext } from './context.js';
5
5
  import { getLogger } from './logger.js';
6
6
  import {} from './redis.js';
7
7
  import {} from './router.js';
8
+ import { writeChunk } from './streams.js';
8
9
  const logger = getLogger();
9
10
  const defaultGetIdFromMessage = (message) => {
11
+ /* c8 ignore next 5 */
10
12
  if (typeof message === 'object' && message !== null && 'id' in message) {
11
13
  return `${message.id}`;
12
14
  }
13
15
  return undefined;
14
16
  };
15
- function writeChunk(res, chunk, state) {
16
- // If we're already draining, just enqueue.
17
- if (state.draining) {
18
- if (state.queue.length >= state.maxQueue) {
19
- // Drop the oldest chunk to keep memory bounded.
20
- state.queue.shift();
21
- }
22
- state.queue.push(chunk);
23
- return;
17
+ export class RedisEventStreamBackend {
18
+ redis;
19
+ constructor(redis) {
20
+ this.redis = redis;
24
21
  }
25
- if (res.write(chunk)) {
26
- return;
22
+ publish(channel, message) {
23
+ return this.redis.publish(channel, message);
24
+ }
25
+ async createSubscriber() {
26
+ const subscriber = this.redis.duplicate();
27
+ /* c8 ignore next 3 */
28
+ subscriber.on('error', (error) => {
29
+ logger.error(error);
30
+ });
31
+ await subscriber.connect();
32
+ return subscriber;
27
33
  }
28
- // Buffer is full — start draining
29
- state.draining = true;
30
- const flush = () => {
31
- while (state.queue.length) {
32
- const next = state.queue.shift();
33
- if (!res.write(next)) {
34
- state.draining = true;
35
- res.once('drain', flush);
36
- return;
37
- }
38
- }
39
- state.draining = false;
40
- };
41
- res.once('drain', flush);
42
34
  }
43
- export function createEventStream(redis, schema) {
35
+ export function createEventStream(backend, options) {
44
36
  const keyPrefix = `${config.REDIS_KEY_PREFIX}events:`;
45
37
  const withKeyPrefix = (key) => `${keyPrefix}${key}`;
46
38
  const subscribers = new Set();
47
39
  const sse = ({ channels, getIdFromMessage = defaultGetIdFromMessage, }) => {
48
40
  return async (req, res, next) => {
49
41
  try {
50
- const subscriber = redis.duplicate();
51
- /* c8 ignore next 3 */
52
- subscriber.on('error', (error) => {
53
- logger.error(error);
54
- });
55
- await subscriber.connect();
42
+ const subscriber = await backend.createSubscriber();
56
43
  const heartbeat = setInterval(() => {
57
44
  try {
58
45
  if (!res.writableEnded) {
59
46
  writeChunk(res, 'event: heartbeat\n', state);
60
47
  writeChunk(res, 'data: ping\n\n', state);
61
48
  }
49
+ /* c8 ignore next 3 */
62
50
  }
63
51
  catch (error) {
64
52
  logger.error(error);
65
53
  }
66
- }, 15000);
54
+ }, options.heartbeatInterval ?? 15000);
67
55
  const state = {
68
56
  draining: false,
69
57
  queue: [],
@@ -124,6 +112,7 @@ export function createEventStream(redis, schema) {
124
112
  });
125
113
  };
126
114
  const gracefulClose = () => {
115
+ /* c8 ignore next 3 */
127
116
  if (closed) {
128
117
  return;
129
118
  }
@@ -151,7 +140,7 @@ export function createEventStream(redis, schema) {
151
140
  };
152
141
  };
153
142
  const publish = (channel, message) => {
154
- return redis.publish(withKeyPrefix(channel), JSON.stringify(message));
143
+ return backend.publish(withKeyPrefix(channel), JSON.stringify(message));
155
144
  };
156
145
  const close = async () => {
157
146
  for (const closeSubscriber of subscribers) {
@@ -0,0 +1,15 @@
1
+ export type Next<T = void> = (err?: Error | null) => T;
2
+ export type RequestHandler<T, U, V = void> = (req: T, res: U, next: Next<V>) => V;
3
+ export type ErrorHandler<T, U, V = void> = (err: Error, req: T, res: U, next: Next<V>) => V;
4
+ export type Middleware<T, U, V = void> = RequestHandler<T, U, V> | ErrorHandler<T, U, V>;
5
+ export type Handler<T, U, V = void> = Middleware<T, U, V> | NestedMiddleware<T, U, V>;
6
+ export interface NestedMiddleware<T, U, V = void> extends ReadonlyArray<Handler<T, U, V>> {
7
+ }
8
+ /**
9
+ * Compose an array of middleware handlers into a single handler.
10
+ */
11
+ export declare function compose<T, U, V = void>(...handlers: Handler<T, U, V>[]): RequestHandler<T, U, V>;
12
+ /**
13
+ * Wrap middleware handlers.
14
+ */
15
+ export declare function errors<T, U, V = void>(...handlers: Handler<T, U, V>[]): ErrorHandler<T, U, V>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Compose an array of middleware handlers into a single handler.
3
+ */
4
+ export function compose(...handlers) {
5
+ const middleware = generate(handlers);
6
+ return function requestMiddleware(req, res, done) {
7
+ return middleware(null, req, res, done);
8
+ };
9
+ }
10
+ /**
11
+ * Wrap middleware handlers.
12
+ */
13
+ export function errors(...handlers) {
14
+ return generate(handlers);
15
+ }
16
+ /**
17
+ * Generate a composed middleware function.
18
+ */
19
+ function generate(handlers) {
20
+ const stack = handlers.flat();
21
+ return function middleware(err, req, res, done) {
22
+ let index = -1;
23
+ // eslint-disable-next-line @typescript-eslint/no-shadow
24
+ function dispatch(pos, err) {
25
+ const handler = stack[pos];
26
+ index = pos;
27
+ if (index === stack.length) {
28
+ return done(err);
29
+ }
30
+ // eslint-disable-next-line @typescript-eslint/no-shadow
31
+ function next(err) {
32
+ /* c8 ignore start */
33
+ if (pos < index) {
34
+ throw new TypeError('`next()` called multiple times');
35
+ }
36
+ /* c8 ignore end */
37
+ return dispatch(pos + 1, err);
38
+ }
39
+ try {
40
+ if (handler.length === 4) {
41
+ if (err) {
42
+ return handler(err, req, res, next);
43
+ }
44
+ }
45
+ else {
46
+ if (!err) {
47
+ return handler(req, res, next);
48
+ }
49
+ }
50
+ }
51
+ catch (e) {
52
+ /* c8 ignore start */
53
+ if (index > pos) {
54
+ throw e;
55
+ }
56
+ return next(e);
57
+ /* c8 ignore end */
58
+ }
59
+ return next(err);
60
+ }
61
+ return dispatch(0, err);
62
+ };
63
+ }
package/dist/config.d.ts CHANGED
@@ -14,6 +14,9 @@ declare const app: import("@devmoods/config").Defs<{
14
14
  REDIS_URL: string;
15
15
  REDIS_KEY_PREFIX: string;
16
16
  SENTRY_DSN: string;
17
+ SENTRY_TRACES_SAMPLE_RATE: number;
18
+ SENTRY_PROFILES_SAMPLE_RATE: number;
19
+ SENTRY_EVENT_LOOP_THRESHOLD: number;
17
20
  LOG_LEVEL: string;
18
21
  LOG_SQL: boolean;
19
22
  NODE_ENV: string;
@@ -43,6 +46,9 @@ export declare const config: import("@devmoods/config").Defs<{
43
46
  REDIS_URL: string;
44
47
  REDIS_KEY_PREFIX: string;
45
48
  SENTRY_DSN: string;
49
+ SENTRY_TRACES_SAMPLE_RATE: number;
50
+ SENTRY_PROFILES_SAMPLE_RATE: number;
51
+ SENTRY_EVENT_LOOP_THRESHOLD: number;
46
52
  LOG_LEVEL: string;
47
53
  LOG_SQL: boolean;
48
54
  NODE_ENV: string;
package/dist/config.js CHANGED
@@ -12,6 +12,9 @@ const app = define({
12
12
  REDIS_URL: t.string(),
13
13
  REDIS_KEY_PREFIX: t.string(),
14
14
  SENTRY_DSN: t.string(),
15
+ SENTRY_TRACES_SAMPLE_RATE: t.number(0.3),
16
+ SENTRY_PROFILES_SAMPLE_RATE: t.number(0.3),
17
+ SENTRY_EVENT_LOOP_THRESHOLD: t.number(300),
15
18
  LOG_LEVEL: t.string(),
16
19
  LOG_SQL: t.boolean(),
17
20
  NODE_ENV: t.string(),
package/dist/context.d.ts CHANGED
@@ -18,6 +18,10 @@ export declare function useRequestContext<T>(key: string): T | undefined;
18
18
  * Set a value on the request context
19
19
  */
20
20
  export declare function setRequestContext<T>(key: string, value: T): void;
21
+ export declare function useRequestCounter(key: string): {
22
+ value: number;
23
+ increase: () => void;
24
+ };
21
25
  /**
22
26
  * Obtain the requestId from the current async request context.
23
27
  */
package/dist/context.js CHANGED
@@ -34,6 +34,14 @@ export function setRequestContext(key, value) {
34
34
  store.set(key, value);
35
35
  }
36
36
  }
37
+ export function useRequestCounter(key) {
38
+ const value = useRequestContext(key) || 0;
39
+ const increase = () => setRequestContext(key, value + 1);
40
+ return {
41
+ value,
42
+ increase,
43
+ };
44
+ }
37
45
  /**
38
46
  * Obtain the requestId from the current async request context.
39
47
  */
package/dist/csrf.d.ts CHANGED
@@ -12,6 +12,7 @@ interface CsrfOptions {
12
12
  }
13
13
  export declare function csrf({ exempt, sessionIdentifier, cookieName, }?: CsrfMiddlewareOptions): RequestHandler;
14
14
  export declare function setCsrfOptions(options: CsrfOptions): void;
15
+ export declare function useCsrfOptions(): CsrfOptions;
15
16
  interface TokenOptions {
16
17
  overwrite?: boolean;
17
18
  }