@devmoods/express-extras 0.65.1 → 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.
- package/dist/auth/emailAndPassword.js +4 -4
- package/dist/auth/errors.d.ts +2 -2
- package/dist/auth/errors.js +4 -4
- package/dist/auth/google.js +2 -2
- package/dist/auth/passwordless.js +1 -1
- package/dist/auth/token.js +3 -1
- package/dist/auth/tokenStorage.d.ts +16 -0
- package/dist/auth/tokenStorage.js +44 -0
- package/dist/auth/types.d.ts +1 -3
- package/dist/auth.d.ts +7 -15
- package/dist/auth.js +29 -41
- package/dist/channels.d.ts +19 -1
- package/dist/channels.js +23 -34
- package/dist/composeMiddleware.d.ts +15 -0
- package/dist/composeMiddleware.js +63 -0
- package/dist/csrf.d.ts +1 -0
- package/dist/csrf.js +6 -2
- package/dist/errors.d.ts +7 -7
- package/dist/errors.js +11 -11
- package/dist/faktory.d.ts +13 -11
- package/dist/faktory.js +6 -6
- package/dist/listen.d.ts +18 -9
- package/dist/listen.js +64 -38
- package/dist/logger.d.ts +23 -4
- package/dist/logger.js +65 -45
- package/dist/manage.d.ts +2 -2
- package/dist/manage.js +12 -12
- package/dist/middleware.d.ts +4 -2
- package/dist/middleware.js +10 -3
- package/dist/rate-limit.d.ts +14 -4
- package/dist/rate-limit.js +10 -5
- package/dist/redis.d.ts +0 -3
- package/dist/redis.js +0 -23
- package/dist/router.js +1 -1
- package/dist/routes.d.ts +5 -4
- package/dist/routes.js +34 -23
- package/dist/streams.d.ts +7 -0
- package/dist/streams.js +29 -0
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -8
- package/dist/validate.d.ts +33 -12
- package/dist/validate.js +6 -1
- package/dist/vite/defineViteConfig.js +1 -0
- package/dist/vite/ssr.d.ts +20 -1
- package/dist/vite/ssr.js +6 -1
- package/dist/vitest/matchers.d.ts +4 -0
- package/dist/vitest/matchers.js +17 -2
- package/dist/vitest/testClient.d.ts +1 -0
- 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);
|
package/dist/auth/errors.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/auth/errors.js
CHANGED
|
@@ -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
|
}
|
package/dist/auth/google.js
CHANGED
|
@@ -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);
|
package/dist/auth/token.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/channels.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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/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
|
}
|
package/dist/csrf.js
CHANGED
|
@@ -11,6 +11,7 @@ const defaultCookieName = () => {
|
|
|
11
11
|
};
|
|
12
12
|
export const getCsrfSecret = () => {
|
|
13
13
|
const secret = config.CSRF_SECRET;
|
|
14
|
+
/* c8 ignore next 3 */
|
|
14
15
|
if (!secret) {
|
|
15
16
|
throw new Error('CSRF_SECRET is not set');
|
|
16
17
|
}
|
|
@@ -20,7 +21,7 @@ export function csrf({ exempt = false, sessionIdentifier, cookieName, } = {}) {
|
|
|
20
21
|
return (req, res, next) => {
|
|
21
22
|
const existingOptions = useCsrfOptions();
|
|
22
23
|
setCsrfOptions({
|
|
23
|
-
cookieName: cookieName ?? existingOptions.cookieName
|
|
24
|
+
cookieName: cookieName ?? existingOptions.cookieName,
|
|
24
25
|
sessionIdentifier: sessionIdentifier
|
|
25
26
|
? sessionIdentifier(req)
|
|
26
27
|
: existingOptions.sessionIdentifier,
|
|
@@ -33,7 +34,7 @@ const CSRF_CONTEXT_KEY = '@csrfConfig';
|
|
|
33
34
|
export function setCsrfOptions(options) {
|
|
34
35
|
setRequestContext(CSRF_CONTEXT_KEY, options);
|
|
35
36
|
}
|
|
36
|
-
function useCsrfOptions() {
|
|
37
|
+
export function useCsrfOptions() {
|
|
37
38
|
return (useRequestContext(CSRF_CONTEXT_KEY) || {
|
|
38
39
|
exempt: true,
|
|
39
40
|
cookieName: defaultCookieName(),
|
|
@@ -92,6 +93,9 @@ const validateRequest = (req) => {
|
|
|
92
93
|
const [csrfToken, csrfTokenHash] = csrfCookie.split('|');
|
|
93
94
|
const csrfTokenFromRequest = req.headers['x-csrf-token'];
|
|
94
95
|
const secret = getCsrfSecret();
|
|
96
|
+
if (!csrfToken || !csrfTokenHash) {
|
|
97
|
+
throw Forbidden('Invalid CSRF token format');
|
|
98
|
+
}
|
|
95
99
|
if (!csrfTokenFromRequest) {
|
|
96
100
|
throw Forbidden('Missing CSRF token');
|
|
97
101
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { type ErrorHandler } from '
|
|
1
|
+
import { type ErrorHandler } from './composeMiddleware.js';
|
|
2
2
|
type AnyPayload = Record<string, unknown>;
|
|
3
3
|
export declare class ApiError extends Error {
|
|
4
4
|
status: number | null | undefined;
|
|
5
5
|
payload: AnyPayload | undefined;
|
|
6
6
|
}
|
|
7
|
-
export declare const BadRequest: (reason: any) => ApiError;
|
|
8
|
-
export declare const Forbidden: (detail?: string) => ApiError;
|
|
9
|
-
export declare const NotFound: (detail?: string) => ApiError;
|
|
10
|
-
export declare const Unauthorized: (detail?: string) => ApiError;
|
|
11
|
-
export declare const Conflict: (detail?: string) => ApiError;
|
|
7
|
+
export declare const BadRequest: (reason: any, options?: ErrorOptions) => ApiError;
|
|
8
|
+
export declare const Forbidden: (detail?: string, options?: ErrorOptions) => ApiError;
|
|
9
|
+
export declare const NotFound: (detail?: string, options?: ErrorOptions) => ApiError;
|
|
10
|
+
export declare const Unauthorized: (detail?: string, options?: ErrorOptions) => ApiError;
|
|
11
|
+
export declare const Conflict: (detail?: string, options?: ErrorOptions) => ApiError;
|
|
12
12
|
/**
|
|
13
13
|
* Create a new ApiError instance
|
|
14
14
|
*/
|
|
15
|
-
export declare function makeError(status: number, detail?: string, payload?: AnyPayload): ApiError;
|
|
15
|
+
export declare function makeError(status: number, detail?: string, payload?: AnyPayload, options?: ErrorOptions): ApiError;
|
|
16
16
|
/**
|
|
17
17
|
* Problem Details for HTTP APIs
|
|
18
18
|
*
|