@devmoods/express-extras 0.64.0 → 0.65.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 +0 -111
- package/dist/auth/emailAndPassword.js +2 -1
- package/dist/auth/google.d.ts +4 -4
- package/dist/auth/google.js +14 -11
- package/dist/auth/passwordless.js +2 -1
- package/dist/auth/token.js +6 -6
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +17 -10
- package/dist/cache.d.ts +3 -0
- package/dist/cache.js +3 -2
- package/dist/channels.d.ts +6 -5
- package/dist/channels.js +9 -4
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +72 -66
- package/dist/config.js +38 -36
- package/dist/context.d.ts +9 -2
- package/dist/context.js +18 -9
- package/dist/csrf.js +6 -6
- package/dist/email.d.ts +1 -1
- package/dist/email.js +3 -3
- package/dist/errors.js +1 -1
- package/dist/faktory.d.ts +1 -1
- package/dist/faktory.js +5 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/dist/listen.d.ts +4 -4
- package/dist/listen.js +41 -43
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +11 -5
- package/dist/manage.d.ts +4 -6
- package/dist/manage.js +41 -37
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.js +24 -7
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +25 -3
- package/dist/redis.d.ts +1 -1
- package/dist/redis.js +4 -4
- package/dist/sentry.js +11 -5
- package/dist/setupTests.global.d.ts +2 -0
- package/dist/setupTests.global.js +4 -0
- package/dist/setupTests.js +3 -0
- package/dist/slack.js +2 -2
- package/dist/utils.d.ts +0 -13
- package/dist/utils.js +0 -21
- package/dist/validate.js +7 -4
- package/dist/vite/__tests__/fixtures/src/entry-server.d.ts +5 -0
- package/dist/vite/__tests__/fixtures/src/entry-server.js +15 -0
- package/dist/vite/defineViteConfig.js +11 -1
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +3 -0
- package/dist/vite/ssr.d.ts +1 -0
- package/dist/vite/ssr.js +9 -4
- package/dist/vitest/index.d.ts +5 -0
- package/dist/vitest/index.js +24 -0
- package/dist/vitest/matchers.d.ts +1 -1
- package/dist/vitest/matchers.js +6 -2
- package/dist/vitest/testClient.d.ts +30 -0
- package/dist/vitest/testClient.js +98 -0
- package/dist/webapi.js +4 -0
- package/package.json +31 -42
- package/dist/__tests__/__migrations__/1_a.d.ts +0 -3
- package/dist/__tests__/__migrations__/1_a.js +0 -9
- package/dist/migrate.d.ts +0 -47
- package/dist/migrate.js +0 -255
- package/dist/postgres-ts-generator.d.ts +0 -11
- package/dist/postgres-ts-generator.js +0 -114
- package/dist/postgres.d.ts +0 -71
- package/dist/postgres.js +0 -261
- package/dist/sql.d.ts +0 -33
- package/dist/sql.js +0 -135
- package/dist/vite.d.ts +0 -1
- package/dist/vite.js +0 -1
package/README.md
CHANGED
|
@@ -1,112 +1 @@
|
|
|
1
1
|
# @devmoods/express-extras
|
|
2
|
-
|
|
3
|
-
> Express evolved: Type-safe routing, elegant transactions, and production-ready patterns for modern Node.js
|
|
4
|
-
|
|
5
|
-
## ⚠️ Express v5 ⚠️
|
|
6
|
-
|
|
7
|
-
Due to [internal changes in the router](https://github.com/pillarjs/router/issues/122) used in Express v5 that breaks the ability to generate a full set of routes using the `routes` management command and to generate the OpenAPI spec, this library doesn't officially support Express v5. But in most cases if will work just fine as long as you don't rely on these features.
|
|
8
|
-
|
|
9
|
-
## Get ready
|
|
10
|
-
|
|
11
|
-
_Extremely opinionated_ set of utilities for cranking out production-ready Node.js apps in a timely manner. We assume you are using PostgreSQL as the main database, Redis for data you don't want in Postgres and [Faktory](https://contribsys.com/faktory/) for background jobs.
|
|
12
|
-
|
|
13
|
-
- Only works with [ECMAScript Modules (ESM)](https://nodejs.org/api/esm.html)
|
|
14
|
-
- Thin layer on top of [Express](https://expressjs.com) with better support for `async`/`await` and type safety.
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
const router = createRouter();
|
|
18
|
-
|
|
19
|
-
router.put(
|
|
20
|
-
'/posts/:id',
|
|
21
|
-
{
|
|
22
|
-
request: type({ title: 'string' }),
|
|
23
|
-
response: type({ id: 'number', title: 'string' }),
|
|
24
|
-
},
|
|
25
|
-
async (req) => {
|
|
26
|
-
await delay(1000);
|
|
27
|
-
return {
|
|
28
|
-
id: req.params.id,
|
|
29
|
-
...req.body,
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
);
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
- Clean graceful shutdown procedures and built-in health checks. Runs perfectly in Kubernetes.
|
|
36
|
-
- PostgreSQL with transactions and migrations
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
bin/dx manage migrate up
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
await postgres.transaction(() => {
|
|
44
|
-
const post = await postgres.get<{ title: string }>(
|
|
45
|
-
sql`SELECT title FROM posts WHERE id = ${id} FOR UPDATE`,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
await postgres.query(
|
|
49
|
-
sql`UPDATE posts SET ${sql.spreadUpdate({ title: post.title.split('').reverse().join('') })} WHERE id = ${id}`,
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// Nested transactions with savepoints
|
|
54
|
-
await postgres.transaction(() => {
|
|
55
|
-
})
|
|
56
|
-
} catch () {
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
throw new Error('please rollback');
|
|
60
|
-
});
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
- CSRF protection
|
|
64
|
-
- Built in auth backends! Email+password, passwordless, easy to extend with more
|
|
65
|
-
- OpenAPI v3 generator
|
|
66
|
-
- Redis
|
|
67
|
-
- Slack/Discord client for sending operational notifications
|
|
68
|
-
- CLI commands inspired by Django's management commands
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
bin/dx manage --help
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
registerManagementCommand((command, cli) => {
|
|
76
|
-
command('hello').action(() => {
|
|
77
|
-
cli.writeLine('Hello World');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
- Delayed/async jobs with [Faktory](https://contribsys.com/faktory/)
|
|
83
|
-
- Validation of HTTP requests using [Standard Schema](https://github.com/standard-schema/standard-schema), including [`jsonSchema()`](https://json-schema.org/) and [arktype](https://arktype.io/).
|
|
84
|
-
- Logging helpers to write well-formatted contextual JSON logs to `stdout`
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
const logger = getLogger();
|
|
88
|
-
|
|
89
|
-
logger.info('hello world', { foo: 'bar', baz: 10 });
|
|
90
|
-
// {"level":"info","hostname":"machine.local","pid":43149,"name":"default","message":"hello world","foo":"bar","baz":10,"time":"2024-01-02T17:25:28.610Z","v":1}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
- Request local context using [AsyncLocalStorage](https://nodejs.org/api/async_context.html) (e.g. request ids)
|
|
94
|
-
- [RFC 7807 Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807) by default
|
|
95
|
-
- Uses [Sentry](https://sentry.io/) for external error monitoring
|
|
96
|
-
- [The Twelve-Factor App](https://12factor.net/)
|
|
97
|
-
|
|
98
|
-
## Goals
|
|
99
|
-
|
|
100
|
-
- Time-to-market and top-notch developer experience.
|
|
101
|
-
|
|
102
|
-
## Non-goals
|
|
103
|
-
|
|
104
|
-
- Support for multiple different database vendors and job processing systems.
|
|
105
|
-
|
|
106
|
-
## Example
|
|
107
|
-
|
|
108
|
-
> See the [example/](./example) folder for a complete example.
|
|
109
|
-
|
|
110
|
-
## Configure
|
|
111
|
-
|
|
112
|
-
Put environment variables in `.env` at the application will automatically read those on startup. For test configuration in unit and integrations tests, use `.env.test`.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { startTimer } from '@devmoods/observability';
|
|
1
2
|
import argon2 from 'argon2';
|
|
2
3
|
import bcrypt from 'bcrypt';
|
|
3
4
|
import {} from 'express';
|
|
@@ -5,7 +6,6 @@ import { createTokenResponse, tokenSchema } from './token.js';
|
|
|
5
6
|
import {} from './types.js';
|
|
6
7
|
import { Unauthorized } from '../errors.js';
|
|
7
8
|
import { getLogger } from '../logger.js';
|
|
8
|
-
import { startTimer } from '../utils.js';
|
|
9
9
|
import { jsonSchema } from '../validate.js';
|
|
10
10
|
const passwordSchema = { type: 'string', minLength: 4 };
|
|
11
11
|
const emailSchema = { type: 'string', minLength: 4 };
|
|
@@ -57,6 +57,7 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
57
57
|
}
|
|
58
58
|
context.setCurrentUser(user);
|
|
59
59
|
await options.onPasswordChanged?.(user, await hashPassword(password), {
|
|
60
|
+
/* c8 ignore next 2 */
|
|
60
61
|
ip: req.ip ?? '',
|
|
61
62
|
userAgent: req.headers['user-agent'] ?? '',
|
|
62
63
|
});
|
package/dist/auth/google.d.ts
CHANGED
|
@@ -4,10 +4,10 @@ interface GoogleProfile {
|
|
|
4
4
|
fullName: string;
|
|
5
5
|
}
|
|
6
6
|
interface GoogleAuthProviderOptions<UserModel = AnyUser> {
|
|
7
|
-
clientId
|
|
8
|
-
clientSecret
|
|
9
|
-
redirectUri
|
|
7
|
+
clientId?: string;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
redirectUri?: string;
|
|
10
10
|
getOrCreateUserFromUserIdentifier(userIdentifier: string, profile: GoogleProfile): Promise<UserModel>;
|
|
11
11
|
}
|
|
12
|
-
export declare function GoogleAuthProvider<UserModel extends AnyUser>(
|
|
12
|
+
export declare function GoogleAuthProvider<UserModel extends AnyUser>({ clientId, clientSecret, redirectUri, getOrCreateUserFromUserIdentifier, }: GoogleAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
13
13
|
export {};
|
package/dist/auth/google.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
1
2
|
import { type } from 'arktype';
|
|
2
3
|
import {} from 'express';
|
|
3
4
|
import { OAuth2Client } from 'google-auth-library';
|
|
@@ -6,11 +7,11 @@ import { getLogger } from '../logger.js';
|
|
|
6
7
|
import { createTokenResponse } from './token.js';
|
|
7
8
|
import {} from './types.js';
|
|
8
9
|
const logger = getLogger('auth');
|
|
9
|
-
export function GoogleAuthProvider(
|
|
10
|
+
export function GoogleAuthProvider({ clientId = config.GOOGLE_AUTH_CLIENT_ID, clientSecret = config.GOOGLE_AUTH_CLIENT_SECRET, redirectUri = `${config.PUBLIC_URL}/api/v1/auth/google/callback`, getOrCreateUserFromUserIdentifier, }) {
|
|
10
11
|
const client = new OAuth2Client({
|
|
11
|
-
clientId
|
|
12
|
-
clientSecret
|
|
13
|
-
redirectUri
|
|
12
|
+
clientId,
|
|
13
|
+
clientSecret,
|
|
14
|
+
redirectUri,
|
|
14
15
|
});
|
|
15
16
|
const verifyAuthorizationCode = async (inputCode) => {
|
|
16
17
|
try {
|
|
@@ -25,7 +26,7 @@ export function GoogleAuthProvider(options) {
|
|
|
25
26
|
const verifyIdToken = async (token) => {
|
|
26
27
|
const ticket = await client.verifyIdToken({
|
|
27
28
|
idToken: token,
|
|
28
|
-
audience:
|
|
29
|
+
audience: clientId,
|
|
29
30
|
});
|
|
30
31
|
const payload = ticket.getPayload();
|
|
31
32
|
if (!payload) {
|
|
@@ -36,12 +37,11 @@ export function GoogleAuthProvider(options) {
|
|
|
36
37
|
const requiredScopes = ['openid', 'email', 'profile'];
|
|
37
38
|
return {
|
|
38
39
|
setupRoutes(router, context) {
|
|
39
|
-
const googleUser = { id: 'google' };
|
|
40
40
|
router.get('/google/login', {}, async (req, res) => {
|
|
41
|
-
const state = await context.createTemporaryToken(
|
|
41
|
+
const state = await context.createTemporaryToken(null);
|
|
42
42
|
const params = new URLSearchParams();
|
|
43
|
-
params.set('client_id',
|
|
44
|
-
params.set('redirect_uri',
|
|
43
|
+
params.set('client_id', clientId);
|
|
44
|
+
params.set('redirect_uri', redirectUri);
|
|
45
45
|
params.set('response_type', 'code');
|
|
46
46
|
params.set('scope', requiredScopes.join(' '));
|
|
47
47
|
params.set('access_type', 'offline');
|
|
@@ -69,7 +69,7 @@ export function GoogleAuthProvider(options) {
|
|
|
69
69
|
throw BadRequest(`Missing required scopes: ${requiredScopes.join(', ')}`);
|
|
70
70
|
}
|
|
71
71
|
try {
|
|
72
|
-
await context.verifyTemporaryToken(state,
|
|
72
|
+
await context.verifyTemporaryToken(state, null, true);
|
|
73
73
|
}
|
|
74
74
|
catch (e) {
|
|
75
75
|
logger.error(e);
|
|
@@ -81,7 +81,10 @@ export function GoogleAuthProvider(options) {
|
|
|
81
81
|
}
|
|
82
82
|
const payload = await verifyIdToken(idToken);
|
|
83
83
|
logger.info('google payload', { payload });
|
|
84
|
-
const user = await
|
|
84
|
+
const user = await getOrCreateUserFromUserIdentifier(payload?.sub, {
|
|
85
|
+
email: payload.email,
|
|
86
|
+
fullName: payload.name,
|
|
87
|
+
});
|
|
85
88
|
const accessToken = await context.login(req, res, user);
|
|
86
89
|
logger.info('google-login', { email: user.email });
|
|
87
90
|
if (req.accepts('html')) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { DoesNotExistError } from '@devmoods/postgres';
|
|
1
2
|
import {} from 'express';
|
|
2
3
|
import { createTokenResponse } from './token.js';
|
|
3
4
|
import {} from './types.js';
|
|
4
5
|
import { getLogger } from '../logger.js';
|
|
5
|
-
import { DoesNotExistError } from '../postgres.js';
|
|
6
6
|
import { jsonSchema } from '../validate.js';
|
|
7
7
|
const logger = getLogger('auth');
|
|
8
8
|
export function PasswordlessAuthProvider(options) {
|
|
@@ -13,6 +13,7 @@ export function PasswordlessAuthProvider(options) {
|
|
|
13
13
|
catch (error) {
|
|
14
14
|
if (error instanceof DoesNotExistError) {
|
|
15
15
|
return null;
|
|
16
|
+
/* c8 ignore next 3 */
|
|
16
17
|
}
|
|
17
18
|
throw error;
|
|
18
19
|
}
|
package/dist/auth/token.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
1
2
|
import {} from 'express';
|
|
2
3
|
import jwt from 'jsonwebtoken';
|
|
3
4
|
import { v4 } from 'uuid';
|
|
4
5
|
import { InvalidToken } from './errors.js';
|
|
5
|
-
import { config } from '../config.js';
|
|
6
6
|
import {} from '../validate.js';
|
|
7
7
|
const ACCESS_TOKEN_COOKIE_NAME = 'access_token';
|
|
8
8
|
export const tokenSchema = {
|
|
@@ -24,8 +24,8 @@ export function createTokenResponse(accessToken) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Returns a signed JWT token.
|
|
26
26
|
*/
|
|
27
|
-
export function createAccessToken(payload, version = '1', secret = config.
|
|
28
|
-
const expiresIn = config.
|
|
27
|
+
export function createAccessToken(payload, version = '1', secret = config.AUTH_JWT_SECRET) {
|
|
28
|
+
const expiresIn = config.AUTH_TOKEN_EXPIRES;
|
|
29
29
|
const token = jwt.sign({ v: version, ...payload }, secret, {
|
|
30
30
|
expiresIn,
|
|
31
31
|
});
|
|
@@ -34,7 +34,7 @@ export function createAccessToken(payload, version = '1', secret = config.value.
|
|
|
34
34
|
/**
|
|
35
35
|
* Decode the given JWT token.
|
|
36
36
|
*/
|
|
37
|
-
export function decodeAccessToken(token, secret = config.
|
|
37
|
+
export function decodeAccessToken(token, secret = config.AUTH_JWT_SECRET) {
|
|
38
38
|
try {
|
|
39
39
|
const decoded = jwt.verify(token.trim(), secret);
|
|
40
40
|
/* c8 ignore next 3 */
|
|
@@ -95,7 +95,7 @@ export function extractAccessTokenFromRequest(req) {
|
|
|
95
95
|
* If the provided token is `null` the cookie will be cleared.
|
|
96
96
|
*/
|
|
97
97
|
export function setAccessTokenCookie(res, token) {
|
|
98
|
-
const sameSite =
|
|
98
|
+
const sameSite = config.AUTH_COOKIE_SAME_SITE;
|
|
99
99
|
if (token == null) {
|
|
100
100
|
res.cookie(ACCESS_TOKEN_COOKIE_NAME, v4(), {
|
|
101
101
|
expires: new Date(1970, 0, 1),
|
|
@@ -105,7 +105,7 @@ export function setAccessTokenCookie(res, token) {
|
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
107
|
res.cookie(ACCESS_TOKEN_COOKIE_NAME, token, {
|
|
108
|
-
expires: new Date(Date.now() + config.
|
|
108
|
+
expires: new Date(Date.now() + config.AUTH_TOKEN_EXPIRES * 1000),
|
|
109
109
|
httpOnly: true,
|
|
110
110
|
sameSite,
|
|
111
111
|
secure: process.env.NODE_ENV === 'production',
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
35
35
|
filterUser: AuthOptions<UserModel>['filterUser'];
|
|
36
36
|
tokenStorage: AuthOptions<UserModel>['tokenStorage'];
|
|
37
37
|
setCurrentUser: (user: UserModel) => void;
|
|
38
|
-
createTemporaryToken(user: UserModel): Promise<string>;
|
|
38
|
+
createTemporaryToken(user: UserModel | null): Promise<string>;
|
|
39
39
|
verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
40
40
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
41
41
|
createOtp(user: UserModel): Promise<string>;
|
package/dist/auth.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
13
13
|
before: () => RequestHandler;
|
|
14
14
|
allow: (fn?: AuthIsAllowedFn<UserModel>) => RequestHandler;
|
|
15
15
|
isAuthorized: (fn?: (user: UserModel) => boolean) => RequestHandler;
|
|
16
|
-
createTemporaryToken(user: UserModel): Promise<string>;
|
|
16
|
+
createTemporaryToken(user: UserModel | null): Promise<string>;
|
|
17
17
|
verifyTemporaryToken(token: string, user: UserModel | null): Promise<string>;
|
|
18
18
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
19
19
|
createOtp(user: UserModel): Promise<string>;
|
package/dist/auth.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
|
+
import { config } from '@devmoods/config';
|
|
2
3
|
import {} from 'express';
|
|
3
4
|
import { InvalidToken, PermissionDenied } from './auth/errors.js';
|
|
4
5
|
import { createAccessToken, decodeAccessToken, extractAccessTokenFromRequest, setAccessTokenCookie, tokenSchema, } from './auth/token.js';
|
|
5
6
|
import { TokenStorage, } from './auth/types.js';
|
|
6
|
-
import {
|
|
7
|
-
import { setContext, useContext } from './context.js';
|
|
7
|
+
import { setRequestContext, useRequestContext } from './context.js';
|
|
8
8
|
import { Forbidden, NotFound, Unauthorized } from './errors.js';
|
|
9
9
|
import { getLogger } from './logger.js';
|
|
10
10
|
import {} from './redis.js';
|
|
@@ -19,7 +19,7 @@ export * from './auth/google.js';
|
|
|
19
19
|
export class RedisTokenStorage extends TokenStorage {
|
|
20
20
|
redis;
|
|
21
21
|
keyPrefix;
|
|
22
|
-
constructor(redis, keyPrefix = `${config.
|
|
22
|
+
constructor(redis, keyPrefix = `${config.REDIS_KEY_PREFIX}auth:`) {
|
|
23
23
|
super();
|
|
24
24
|
this.redis = redis;
|
|
25
25
|
this.keyPrefix = keyPrefix;
|
|
@@ -38,8 +38,8 @@ export class RedisTokenStorage extends TokenStorage {
|
|
|
38
38
|
}
|
|
39
39
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
40
40
|
export function createAuth({ getUserById, filterUser, tokenStorage, providers, loginUrl = '/', onUserLoggedIn, sessionTokenVersion = '1', }) {
|
|
41
|
-
const useCurrentUser = () =>
|
|
42
|
-
const setCurrentUser = (user) =>
|
|
41
|
+
const useCurrentUser = () => useRequestContext('currentUser');
|
|
42
|
+
const setCurrentUser = (user) => setRequestContext('currentUser', user);
|
|
43
43
|
const login = async (req, res, user, isRefresh = false) => {
|
|
44
44
|
const accessToken = createAccessToken({ uid: user.id }, sessionTokenVersion);
|
|
45
45
|
const sessionId = sha256(accessToken.token);
|
|
@@ -63,7 +63,12 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
// Make timing attacks harder
|
|
66
|
-
const jitter = (duration = 0) =>
|
|
66
|
+
const jitter = (duration = 0) => {
|
|
67
|
+
if (process.env.NODE_ENV === 'test') {
|
|
68
|
+
return Promise.resolve();
|
|
69
|
+
}
|
|
70
|
+
return new Promise((resolve) => void setTimeout(resolve, Math.max((0.5 - duration) * 1000, 200)));
|
|
71
|
+
};
|
|
67
72
|
/**
|
|
68
73
|
* API for auth providers to use
|
|
69
74
|
*/
|
|
@@ -224,7 +229,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
224
229
|
}
|
|
225
230
|
async function createOtp(user) {
|
|
226
231
|
const otp = crypto.randomInt(100000, 999999).toString();
|
|
227
|
-
await tokenStorage.set(otp, String(user.id), config.
|
|
232
|
+
await tokenStorage.set(otp, String(user.id), config.AUTH_OTP_EXPIRES);
|
|
228
233
|
return otp;
|
|
229
234
|
}
|
|
230
235
|
async function verifyOtp(otp, user, consume = true) {
|
|
@@ -232,6 +237,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
232
237
|
if (!storedUid) {
|
|
233
238
|
throw new InvalidToken('Invalid OTP');
|
|
234
239
|
}
|
|
240
|
+
/* c8 ignore next 3 */
|
|
235
241
|
if (user && storedUid !== user.id) {
|
|
236
242
|
throw new InvalidToken('Invalid OTP');
|
|
237
243
|
}
|
|
@@ -250,7 +256,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
250
256
|
const randomBytes = crypto.randomBytes(32).toString('hex');
|
|
251
257
|
const payload = { v: '1', uid: uid, r: randomBytes };
|
|
252
258
|
const signature = crypto
|
|
253
|
-
.createHmac('sha256', config.
|
|
259
|
+
.createHmac('sha256', config.AUTH_JWT_SECRET)
|
|
254
260
|
.update(JSON.stringify(payload))
|
|
255
261
|
.digest('base64')
|
|
256
262
|
.slice(0, 12);
|
|
@@ -258,7 +264,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
258
264
|
throw new Error('User ID is required to create a token');
|
|
259
265
|
}
|
|
260
266
|
const temporaryToken = Buffer.from(JSON.stringify({ payload, signature })).toString('base64url');
|
|
261
|
-
await tokenStorage.set(temporaryToken, String(uid), config.
|
|
267
|
+
await tokenStorage.set(temporaryToken, String(uid), config.AUTH_TEMP_TOKEN_EXPIRES);
|
|
262
268
|
return temporaryToken;
|
|
263
269
|
}
|
|
264
270
|
catch (error) {
|
|
@@ -272,10 +278,11 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
272
278
|
async function verifyTemporaryToken(token, user, consume = true) {
|
|
273
279
|
const decoded = decodeTemporaryToken(token);
|
|
274
280
|
const expectedSignature = crypto
|
|
275
|
-
.createHmac('sha256', config.
|
|
281
|
+
.createHmac('sha256', config.AUTH_JWT_SECRET)
|
|
276
282
|
.update(JSON.stringify(decoded.payload))
|
|
277
283
|
.digest('base64')
|
|
278
284
|
.slice(0, 12);
|
|
285
|
+
/* c8 ignore next 3 */
|
|
279
286
|
if (expectedSignature !== decoded.signature) {
|
|
280
287
|
throw new InvalidToken('Invalid signature');
|
|
281
288
|
}
|
package/dist/cache.d.ts
CHANGED
package/dist/cache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { config } from '
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
2
2
|
import {} from './redis.js';
|
|
3
3
|
export function createRedisCacheAdapter(redis, options = {}) {
|
|
4
|
-
const { keyPrefix = `${config.
|
|
4
|
+
const { keyPrefix = `${config.REDIS_KEY_PREFIX}cache:` } = options;
|
|
5
5
|
const withKeyPrefix = (key) => `${keyPrefix}${key}`;
|
|
6
6
|
return {
|
|
7
7
|
async get(key) {
|
|
@@ -97,6 +97,7 @@ export function createCache(adapter) {
|
|
|
97
97
|
clear: adapter.clear.bind(adapter),
|
|
98
98
|
wrap: cached,
|
|
99
99
|
decorate: cachedFn,
|
|
100
|
+
hashKey,
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
function hashKey(obj) {
|
package/dist/channels.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { type StandardSchemaV1 } from '@standard-schema/spec';
|
|
1
2
|
import { type Request } from 'express';
|
|
2
3
|
import { type RedisClient } from './redis.js';
|
|
3
4
|
import { type ExtractParams, type TypedRequestHandler } from './router.js';
|
|
4
|
-
export interface EventStreamSseOptions {
|
|
5
|
+
export interface EventStreamSseOptions<Schema extends StandardSchemaV1> {
|
|
5
6
|
channels: (req: Request) => string[] | Promise<string[]>;
|
|
6
|
-
getIdFromMessage?: (message:
|
|
7
|
+
getIdFromMessage?: (message: StandardSchemaV1.InferOutput<Schema>) => string | undefined;
|
|
7
8
|
}
|
|
8
|
-
export declare function createEventStream<Channels extends string>(redis: RedisClient): {
|
|
9
|
-
sse: <Path extends string>({ channels, getIdFromMessage, }: EventStreamSseOptions) => TypedRequestHandler<ExtractParams<Path>, any, any, any>;
|
|
10
|
-
publish: (channel: Channels, message:
|
|
9
|
+
export declare function createEventStream<Channels extends string, Schema extends StandardSchemaV1>(redis: RedisClient, schema: Schema): {
|
|
10
|
+
sse: <const Path extends string>({ channels, getIdFromMessage, }: EventStreamSseOptions<Schema>) => TypedRequestHandler<ExtractParams<Path>, any, any, any>;
|
|
11
|
+
publish: (channel: Channels, message: StandardSchemaV1.InferOutput<Schema>) => Promise<number>;
|
|
11
12
|
close: () => Promise<void>;
|
|
12
13
|
};
|
package/dist/channels.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { config } from '@devmoods/config';
|
|
2
|
+
import {} from '@standard-schema/spec';
|
|
1
3
|
import {} from 'express';
|
|
2
|
-
import { config } from './config.js';
|
|
3
4
|
import { requestContext } from './context.js';
|
|
4
5
|
import { getLogger } from './logger.js';
|
|
5
6
|
import {} from './redis.js';
|
|
@@ -39,14 +40,15 @@ function writeChunk(res, chunk, state) {
|
|
|
39
40
|
};
|
|
40
41
|
res.once('drain', flush);
|
|
41
42
|
}
|
|
42
|
-
export function createEventStream(redis) {
|
|
43
|
-
const keyPrefix = `${config.
|
|
43
|
+
export function createEventStream(redis, schema) {
|
|
44
|
+
const keyPrefix = `${config.REDIS_KEY_PREFIX}events:`;
|
|
44
45
|
const withKeyPrefix = (key) => `${keyPrefix}${key}`;
|
|
45
46
|
const subscribers = new Set();
|
|
46
47
|
const sse = ({ channels, getIdFromMessage = defaultGetIdFromMessage, }) => {
|
|
47
48
|
return async (req, res, next) => {
|
|
48
49
|
try {
|
|
49
50
|
const subscriber = redis.duplicate();
|
|
51
|
+
/* c8 ignore next 3 */
|
|
50
52
|
subscriber.on('error', (error) => {
|
|
51
53
|
logger.error(error);
|
|
52
54
|
});
|
|
@@ -80,6 +82,7 @@ export function createEventStream(redis) {
|
|
|
80
82
|
writeChunk(res, `data: ${JSON.stringify(value)}\n\n`, state);
|
|
81
83
|
}
|
|
82
84
|
});
|
|
85
|
+
/* c8 ignore next 3 */
|
|
83
86
|
}
|
|
84
87
|
catch (error) {
|
|
85
88
|
logger.error(error);
|
|
@@ -90,6 +93,7 @@ export function createEventStream(redis) {
|
|
|
90
93
|
let parsedMessage;
|
|
91
94
|
try {
|
|
92
95
|
parsedMessage = JSON.parse(message);
|
|
96
|
+
/* c8 ignore next 3 */
|
|
93
97
|
}
|
|
94
98
|
catch (error) {
|
|
95
99
|
logger.error(error);
|
|
@@ -101,7 +105,7 @@ export function createEventStream(redis) {
|
|
|
101
105
|
logger.info('event stream subscribe', { channels: _channels });
|
|
102
106
|
const headers = {
|
|
103
107
|
'Content-Type': 'text/event-stream',
|
|
104
|
-
'Cache-Control': 'no-cache
|
|
108
|
+
'Cache-Control': 'no-cache',
|
|
105
109
|
Connection: 'keep-alive',
|
|
106
110
|
'x-accel-buffering': 'no',
|
|
107
111
|
};
|
|
@@ -139,6 +143,7 @@ export function createEventStream(redis) {
|
|
|
139
143
|
res.on('finish', cleanup);
|
|
140
144
|
req.on('aborted', cleanup);
|
|
141
145
|
req.socket.on('error', cleanup);
|
|
146
|
+
/* c8 ignore next 3 */
|
|
142
147
|
}
|
|
143
148
|
catch (error) {
|
|
144
149
|
next(error);
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { stdin, stdout, stderr } from 'node:process';
|
|
2
2
|
import * as readline from 'node:readline';
|
|
3
3
|
import {} from 'node:tty';
|
|
4
|
+
import { startTimer } from '@devmoods/observability';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
|
-
import { startTimer } from './utils.js';
|
|
6
6
|
export class CLI {
|
|
7
7
|
options;
|
|
8
8
|
constructor(options = {}) {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,66 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
declare module '@devmoods/config' {
|
|
2
|
+
interface Config extends ConfigFromDefs<typeof app> {
|
|
3
|
+
}
|
|
4
|
+
}
|
|
5
|
+
declare const app: import("@devmoods/config").Defs<{
|
|
6
|
+
CI: boolean;
|
|
7
|
+
PORT: number;
|
|
8
|
+
PUBLIC_URL: string;
|
|
9
|
+
AUTH_TOKEN_EXPIRES: number;
|
|
10
|
+
AUTH_TEMP_TOKEN_EXPIRES: number;
|
|
11
|
+
AUTH_OTP_EXPIRES: number;
|
|
12
|
+
AUTH_JWT_SECRET: string;
|
|
13
|
+
AUTH_COOKIE_SAME_SITE: string;
|
|
14
|
+
REDIS_URL: string;
|
|
15
|
+
REDIS_KEY_PREFIX: string;
|
|
16
|
+
SENTRY_DSN: string;
|
|
17
|
+
SENTRY_TRACES_SAMPLE_RATE: number;
|
|
18
|
+
SENTRY_PROFILES_SAMPLE_RATE: number;
|
|
19
|
+
SENTRY_EVENT_LOOP_THRESHOLD: number;
|
|
20
|
+
LOG_LEVEL: string;
|
|
21
|
+
LOG_SQL: boolean;
|
|
22
|
+
NODE_ENV: string;
|
|
23
|
+
CSRF_SECRET: string;
|
|
24
|
+
READINESS_PERIOD_MS: number;
|
|
25
|
+
GRACE_PERIOD_MS: number;
|
|
26
|
+
SLACK_WEBHOOK_URL: string;
|
|
27
|
+
VITE_USE_DEV_SERVER: boolean;
|
|
28
|
+
EMAIL_HOST: string;
|
|
29
|
+
EMAIL_PORT: number;
|
|
30
|
+
EMAIL_USER: string;
|
|
31
|
+
EMAIL_PASS: string;
|
|
32
|
+
EMAIL_SENDER_NAME: string;
|
|
33
|
+
EMAIL_SENDER_ADDRESS: string;
|
|
34
|
+
GOOGLE_AUTH_CLIENT_ID: string;
|
|
35
|
+
GOOGLE_AUTH_CLIENT_SECRET: string;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const config: import("@devmoods/config").Defs<{
|
|
38
|
+
CI: boolean;
|
|
39
|
+
PORT: number;
|
|
40
|
+
PUBLIC_URL: string;
|
|
41
|
+
AUTH_TOKEN_EXPIRES: number;
|
|
42
|
+
AUTH_TEMP_TOKEN_EXPIRES: number;
|
|
43
|
+
AUTH_OTP_EXPIRES: number;
|
|
44
|
+
AUTH_JWT_SECRET: string;
|
|
45
|
+
AUTH_COOKIE_SAME_SITE: string;
|
|
46
|
+
REDIS_URL: string;
|
|
47
|
+
REDIS_KEY_PREFIX: string;
|
|
48
|
+
SENTRY_DSN: string;
|
|
49
|
+
SENTRY_TRACES_SAMPLE_RATE: number;
|
|
50
|
+
SENTRY_PROFILES_SAMPLE_RATE: number;
|
|
51
|
+
SENTRY_EVENT_LOOP_THRESHOLD: number;
|
|
52
|
+
LOG_LEVEL: string;
|
|
53
|
+
LOG_SQL: boolean;
|
|
54
|
+
NODE_ENV: string;
|
|
55
|
+
CSRF_SECRET: string;
|
|
56
|
+
READINESS_PERIOD_MS: number;
|
|
57
|
+
GRACE_PERIOD_MS: number;
|
|
58
|
+
SLACK_WEBHOOK_URL: string;
|
|
59
|
+
VITE_USE_DEV_SERVER: boolean;
|
|
60
|
+
EMAIL_HOST: string;
|
|
61
|
+
EMAIL_PORT: number;
|
|
62
|
+
EMAIL_USER: string;
|
|
63
|
+
EMAIL_PASS: string;
|
|
64
|
+
EMAIL_SENDER_NAME: string;
|
|
65
|
+
EMAIL_SENDER_ADDRESS: string;
|
|
66
|
+
GOOGLE_AUTH_CLIENT_ID: string;
|
|
67
|
+
GOOGLE_AUTH_CLIENT_SECRET: string;
|
|
68
|
+
} & {
|
|
69
|
+
POSTGRES_URL: string;
|
|
70
|
+
POSTGRES_PRINT_SQL: boolean;
|
|
71
|
+
}>;
|
|
72
|
+
export {};
|