@devmoods/express-extras 0.37.0 → 0.39.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/cache.js +1 -0
- package/dist/cli.js +1 -1
- package/dist/csrf.d.ts +19 -0
- package/dist/csrf.js +114 -0
- package/dist/faktory.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/logger.js +1 -1
- package/dist/manage.js +2 -1
- package/dist/middleware.d.ts +3 -1
- package/dist/middleware.js +11 -3
- package/dist/postgres.js +2 -1
- package/dist/routes.js +4 -35
- package/package.json +14 -13
package/dist/cache.js
CHANGED
|
@@ -78,6 +78,7 @@ export function createCache(adapter) {
|
|
|
78
78
|
const expiresMs = typeof options === 'number' || options == null
|
|
79
79
|
? options
|
|
80
80
|
: options.expiresMs;
|
|
81
|
+
console.log({ expiresMs });
|
|
81
82
|
const result = await fn(...args);
|
|
82
83
|
await adapter.set(cacheKey, result, expiresMs);
|
|
83
84
|
return result;
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ export class CLI {
|
|
|
16
16
|
redLine(message, output = stderr) {
|
|
17
17
|
output.write(`${chalk.red(message)}\n`);
|
|
18
18
|
}
|
|
19
|
-
/*
|
|
19
|
+
/* c8 ignore next 10 */
|
|
20
20
|
readInput(question) {
|
|
21
21
|
const rl = readline.createInterface({ input: stdin, output: stderr });
|
|
22
22
|
return new Promise((resolve) => {
|
package/dist/csrf.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RequestHandler, type Request, type Response } from 'express';
|
|
2
|
+
export interface CsrfMiddlewareOptions {
|
|
3
|
+
excempt?: boolean;
|
|
4
|
+
cookieName?: string;
|
|
5
|
+
sessionIdentifier?: (req: Request) => string;
|
|
6
|
+
}
|
|
7
|
+
interface CsrfOptions {
|
|
8
|
+
excempt: boolean;
|
|
9
|
+
cookieName: string;
|
|
10
|
+
sessionIdentifier: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function csrf({ excempt, sessionIdentifier, cookieName, }?: CsrfMiddlewareOptions): RequestHandler;
|
|
13
|
+
export declare function setCsrfOptions(options: CsrfOptions): void;
|
|
14
|
+
interface TokenOptions {
|
|
15
|
+
overwrite?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const generateCsrfToken: (req: Request<any, any, any, any>, res: Response, options?: TokenOptions) => string;
|
|
18
|
+
export declare function doubleCsrfProtection(req: Request<any, any, any, any>): () => Promise<void>;
|
|
19
|
+
export {};
|
package/dist/csrf.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import {} from 'express';
|
|
3
|
+
import { setContext, useContext } from './context.js';
|
|
4
|
+
import { Forbidden } from './errors.js';
|
|
5
|
+
import { readEnv } from './utils.js';
|
|
6
|
+
const ignoredMethods = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
7
|
+
const defaultCookieName = () => {
|
|
8
|
+
return process.env.NODE_ENV === 'production'
|
|
9
|
+
? '__Host-devmoods.x-csrf-token'
|
|
10
|
+
: 'devmoods.x-csrf-token';
|
|
11
|
+
};
|
|
12
|
+
export function csrf({ excempt = false, sessionIdentifier, cookieName, } = {}) {
|
|
13
|
+
return (req, res, next) => {
|
|
14
|
+
const existingOptions = useCsrfOptions();
|
|
15
|
+
setCsrfOptions({
|
|
16
|
+
cookieName: cookieName ?? existingOptions.cookieName ?? defaultCookieName(),
|
|
17
|
+
sessionIdentifier: sessionIdentifier
|
|
18
|
+
? sessionIdentifier(req)
|
|
19
|
+
: existingOptions.sessionIdentifier,
|
|
20
|
+
excempt,
|
|
21
|
+
});
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const CSRF_CONTEXT_KEY = '@csrfConfig';
|
|
26
|
+
export function setCsrfOptions(options) {
|
|
27
|
+
setContext(CSRF_CONTEXT_KEY, options);
|
|
28
|
+
}
|
|
29
|
+
function useCsrfOptions() {
|
|
30
|
+
return (useContext(CSRF_CONTEXT_KEY) || {
|
|
31
|
+
excempt: true,
|
|
32
|
+
cookieName: defaultCookieName(),
|
|
33
|
+
sessionIdentifier: '',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const generateTokenAndHash = (req, { overwrite = false } = {}) => {
|
|
37
|
+
const secret = readEnv('CSRF_SECRET', '');
|
|
38
|
+
const { sessionIdentifier, cookieName } = useCsrfOptions();
|
|
39
|
+
const csrfCookie = req.cookies[cookieName];
|
|
40
|
+
if (typeof csrfCookie === 'string' && !overwrite) {
|
|
41
|
+
const [csrfToken, csrfTokenHash] = csrfCookie.split('|');
|
|
42
|
+
if (validateTokenHash({
|
|
43
|
+
csrfToken,
|
|
44
|
+
csrfTokenHash,
|
|
45
|
+
secret,
|
|
46
|
+
sessionIdentifier,
|
|
47
|
+
})) {
|
|
48
|
+
return { csrfToken, csrfTokenHash };
|
|
49
|
+
}
|
|
50
|
+
throw Forbidden('Invalid CSRF token');
|
|
51
|
+
}
|
|
52
|
+
const csrfToken = randomBytes(64).toString('hex');
|
|
53
|
+
const csrfTokenHash = createHash('sha256')
|
|
54
|
+
.update(`${sessionIdentifier}${csrfToken}${secret}`)
|
|
55
|
+
.digest('hex');
|
|
56
|
+
return { csrfToken, csrfTokenHash };
|
|
57
|
+
};
|
|
58
|
+
export const generateCsrfToken = (req, res, options = {}) => {
|
|
59
|
+
const { csrfToken, csrfTokenHash } = generateTokenAndHash(req, options);
|
|
60
|
+
const cookieContent = `${csrfToken}|${csrfTokenHash}`;
|
|
61
|
+
const { cookieName } = useCsrfOptions();
|
|
62
|
+
res.cookie(cookieName, cookieContent, {
|
|
63
|
+
sameSite: 'lax',
|
|
64
|
+
path: '/',
|
|
65
|
+
secure: process.env.NODE_ENV === 'production',
|
|
66
|
+
httpOnly: true,
|
|
67
|
+
});
|
|
68
|
+
return csrfToken;
|
|
69
|
+
};
|
|
70
|
+
const validateTokenHash = ({ csrfToken, csrfTokenHash, secret, sessionIdentifier, }) => {
|
|
71
|
+
if (typeof csrfToken !== 'string' || typeof csrfTokenHash !== 'string') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const expectedHash = createHash('sha256')
|
|
75
|
+
.update(`${sessionIdentifier}${csrfToken}${secret}`)
|
|
76
|
+
.digest('hex');
|
|
77
|
+
return csrfTokenHash === expectedHash;
|
|
78
|
+
};
|
|
79
|
+
const validateRequest = (req) => {
|
|
80
|
+
const { cookieName, sessionIdentifier } = useCsrfOptions();
|
|
81
|
+
const csrfCookie = req.cookies[cookieName];
|
|
82
|
+
if (typeof csrfCookie !== 'string') {
|
|
83
|
+
throw Forbidden('Missing CSRF cookie');
|
|
84
|
+
}
|
|
85
|
+
const [csrfToken, csrfTokenHash] = csrfCookie.split('|');
|
|
86
|
+
const csrfTokenFromRequest = req.headers['x-csrf-token'];
|
|
87
|
+
const secret = readEnv('CSRF_SECRET', '');
|
|
88
|
+
if (!csrfTokenFromRequest) {
|
|
89
|
+
throw Forbidden('Missing CSRF token');
|
|
90
|
+
}
|
|
91
|
+
if (csrfToken !== csrfTokenFromRequest) {
|
|
92
|
+
throw Forbidden('CSRF token mismatch');
|
|
93
|
+
}
|
|
94
|
+
if (!validateTokenHash({
|
|
95
|
+
csrfToken: csrfTokenFromRequest,
|
|
96
|
+
csrfTokenHash,
|
|
97
|
+
secret,
|
|
98
|
+
sessionIdentifier,
|
|
99
|
+
})) {
|
|
100
|
+
throw Forbidden('Invalid CSRF token');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
export function doubleCsrfProtection(req) {
|
|
104
|
+
return async () => {
|
|
105
|
+
const { excempt } = useCsrfOptions();
|
|
106
|
+
if (excempt) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (ignoredMethods.has(req.method)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
validateRequest(req);
|
|
113
|
+
};
|
|
114
|
+
}
|
package/dist/faktory.js
CHANGED
|
@@ -11,7 +11,7 @@ export function createJobs({ queueName, faktory: createFactory = create, }) {
|
|
|
11
11
|
const fn = typeof optionsOrFn === 'function' ? optionsOrFn : fnOrUndefined;
|
|
12
12
|
const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn;
|
|
13
13
|
if (!fn) {
|
|
14
|
-
throw new Error('job() must get a function
|
|
14
|
+
throw new Error('job() must get a function as the first or second argument');
|
|
15
15
|
}
|
|
16
16
|
const jobName = fn.name;
|
|
17
17
|
let executionCount = 0;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* c8 ignore next 100 */
|
|
2
2
|
export * from './auth.js';
|
|
3
3
|
export * from './cache.js';
|
|
4
4
|
export * from './cli.js';
|
|
5
5
|
export * from './context.js';
|
|
6
|
+
export * from './csrf.js';
|
|
6
7
|
export * from './email.js';
|
|
7
8
|
export * from './errors.js';
|
|
8
9
|
export * from './faktory.js';
|
package/dist/logger.js
CHANGED
package/dist/manage.js
CHANGED
|
@@ -21,8 +21,9 @@ export function registerDefaultCommands(app) {
|
|
|
21
21
|
.option('--openapi', 'Output OpenAPI schema')
|
|
22
22
|
.option('--json', 'Output JSON')
|
|
23
23
|
.action(cli.wrap(async (options) => {
|
|
24
|
-
if (!app)
|
|
24
|
+
if (!app) {
|
|
25
25
|
return;
|
|
26
|
+
}
|
|
26
27
|
if (options.openapi) {
|
|
27
28
|
console.error('Not implemented yet');
|
|
28
29
|
return;
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import bodyParser from 'body-parser';
|
|
2
2
|
import { type NextFunction, type RequestHandler, type Request, type Response } from 'express';
|
|
3
|
+
import { type CsrfMiddlewareOptions } from './csrf.js';
|
|
3
4
|
type UrlencodedParser = typeof bodyParser.urlencoded;
|
|
4
5
|
type JsonParser = typeof bodyParser.json;
|
|
5
6
|
export declare const urlencodedParser: UrlencodedParser;
|
|
@@ -15,13 +16,14 @@ export interface BeforeMiddlewareOptions {
|
|
|
15
16
|
cookies?: boolean;
|
|
16
17
|
helmet?: boolean;
|
|
17
18
|
json?: boolean;
|
|
19
|
+
csrf?: boolean | CsrfMiddlewareOptions;
|
|
18
20
|
allowUnsafeInlineScripts?: boolean;
|
|
19
21
|
cspDirectives?: (defaults: BeforeMiddlewareCSPDirectives) => BeforeMiddlewareCSPDirectives;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Register middleware that must come before the app routes.
|
|
23
25
|
*/
|
|
24
|
-
export declare function before({ cookies, helmet: useHelmet, json: useJson, allowUnsafeInlineScripts, cspDirectives, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<unknown, unknown, void>;
|
|
26
|
+
export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, allowUnsafeInlineScripts, cspDirectives, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<unknown, unknown, void>;
|
|
25
27
|
/**
|
|
26
28
|
* Middleware that sends a 404
|
|
27
29
|
*
|
package/dist/middleware.js
CHANGED
|
@@ -4,6 +4,7 @@ import cookieParser from 'cookie-parser';
|
|
|
4
4
|
import {} from 'express';
|
|
5
5
|
import helmet from 'helmet';
|
|
6
6
|
import { withRequestContext } from './context.js';
|
|
7
|
+
import { csrf, doubleCsrfProtection, } from './csrf.js';
|
|
7
8
|
import { NotFound } from './errors.js';
|
|
8
9
|
import { getLogger } from './logger.js';
|
|
9
10
|
import { startTimer } from './utils.js';
|
|
@@ -14,9 +15,14 @@ export { asyncMiddleware as route };
|
|
|
14
15
|
/**
|
|
15
16
|
* Register middleware that must come before the app routes.
|
|
16
17
|
*/
|
|
17
|
-
export function before({ cookies = false, helmet: useHelmet = true, json: useJson = true, allowUnsafeInlineScripts = false, cspDirectives = (x) => x, } = {}) {
|
|
18
|
+
export function before({ cookies = false, helmet: useHelmet = true, json: useJson = true, csrf: useCsrf = false, allowUnsafeInlineScripts = false, cspDirectives = (x) => x, } = {}) {
|
|
18
19
|
return compose([
|
|
19
20
|
withRequestContext(),
|
|
21
|
+
typeof useCsrf === 'object' && useCsrf != null
|
|
22
|
+
? csrf(useCsrf)
|
|
23
|
+
: useCsrf
|
|
24
|
+
? csrf()
|
|
25
|
+
: undefined,
|
|
20
26
|
logRequestDuration(),
|
|
21
27
|
useHelmet &&
|
|
22
28
|
helmet({
|
|
@@ -48,7 +54,7 @@ export function before({ cookies = false, helmet: useHelmet = true, json: useJso
|
|
|
48
54
|
},
|
|
49
55
|
}),
|
|
50
56
|
useJson && jsonParser(),
|
|
51
|
-
cookies && cookieParser(),
|
|
57
|
+
(cookies || useCsrf) && cookieParser(),
|
|
52
58
|
analyticsProxy(),
|
|
53
59
|
].filter(Boolean));
|
|
54
60
|
}
|
|
@@ -111,7 +117,9 @@ export function after() {
|
|
|
111
117
|
*/
|
|
112
118
|
export function asyncMiddleware(fn) {
|
|
113
119
|
const handler = (req, res, next) => {
|
|
114
|
-
return Promise.resolve(
|
|
120
|
+
return Promise.resolve()
|
|
121
|
+
.then(doubleCsrfProtection(req))
|
|
122
|
+
.then(() => fn(req, res, next))
|
|
115
123
|
.then((result) => {
|
|
116
124
|
if (Array.isArray(result) &&
|
|
117
125
|
(result.length === 2 || result.length === 3) &&
|
package/dist/postgres.js
CHANGED
|
@@ -88,8 +88,9 @@ export function createPostgres({ onError, url = readEnv('POSTGRES_URL'), ...conf
|
|
|
88
88
|
* Wipe the database schema
|
|
89
89
|
*/
|
|
90
90
|
async function clear(force = false) {
|
|
91
|
-
if (!force)
|
|
91
|
+
if (!force) {
|
|
92
92
|
return;
|
|
93
|
+
}
|
|
93
94
|
return withConnection(async (connection) => {
|
|
94
95
|
await connection.query('DROP SCHEMA public CASCADE');
|
|
95
96
|
await connection.query(`CREATE SCHEMA public`);
|
package/dist/routes.js
CHANGED
|
@@ -45,40 +45,24 @@ const buildPathParamsSchema = (pathParams) => {
|
|
|
45
45
|
required,
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
|
+
// Fork of express-route-parser
|
|
48
49
|
class ExpressPathParser {
|
|
49
50
|
getPaths() {
|
|
50
51
|
return this._appPaths;
|
|
51
52
|
}
|
|
52
53
|
constructor(app) {
|
|
53
|
-
/**
|
|
54
|
-
* Recursive traversal method for the express router and middleware tree.
|
|
55
|
-
*
|
|
56
|
-
* @param path The current path segment that we have traversed so far
|
|
57
|
-
* @param layer The current 'layer' of the router tree
|
|
58
|
-
* @param keys The keys for the parameter's in the current path branch of the traversal
|
|
59
|
-
* @returns void - base case saves result to internal object
|
|
60
|
-
*/
|
|
61
54
|
this.traverse = (path, layer, keys) => {
|
|
62
55
|
keys = [...keys, ...layer.keys];
|
|
63
56
|
if (layer.name === 'router' && layer.handle) {
|
|
64
|
-
layer.handle.stack
|
|
65
|
-
path
|
|
66
|
-
|
|
67
|
-
});
|
|
57
|
+
for (const l of layer.handle.stack) {
|
|
58
|
+
this.traverse(`${path || ''}/${pathRegexParser(layer.regexp, layer.keys)}`, l, keys);
|
|
59
|
+
}
|
|
68
60
|
}
|
|
69
61
|
if (!layer.route || !layer.route.stack.length) {
|
|
70
62
|
return;
|
|
71
63
|
}
|
|
72
64
|
this._appPaths.push(this.parseRouteLayer(layer, keys, path));
|
|
73
65
|
};
|
|
74
|
-
/**
|
|
75
|
-
* Parses a route object. Route objects are the leafs of an express router tree
|
|
76
|
-
*
|
|
77
|
-
* @param layer The layer of this route object - represents the stack of middleware and other meta data
|
|
78
|
-
* @param keys The full set of keys for this particular route
|
|
79
|
-
* @param basePath The base path as it was initial declared for this route
|
|
80
|
-
* @returns A ExpressPath object holding the meta data for a given route
|
|
81
|
-
*/
|
|
82
66
|
this.parseRouteLayer = (layer, keys, basePath) => {
|
|
83
67
|
const lastRequestHandler = layer.route.stack[layer.route.stack.length - 1];
|
|
84
68
|
const pathParams = keys.map((key) => {
|
|
@@ -105,14 +89,6 @@ class ExpressPathParser {
|
|
|
105
89
|
}
|
|
106
90
|
}
|
|
107
91
|
}
|
|
108
|
-
/** Parses an express layer's regex and converts it to the original format seen in code.
|
|
109
|
-
*
|
|
110
|
-
* @param layerRegexPath The layer's regex pattern
|
|
111
|
-
* @param keys The keys that represent the layer's path parameters
|
|
112
|
-
* @returns The path string for that layer
|
|
113
|
-
* Code inspired and modify from:
|
|
114
|
-
* https://github.com/expressjs/express/issues/3308#issuecomment-300957572
|
|
115
|
-
*/
|
|
116
92
|
const pathRegexParser = (layerRegexPath, keys) => {
|
|
117
93
|
if (typeof layerRegexPath === 'string') {
|
|
118
94
|
return layerRegexPath;
|
|
@@ -140,13 +116,6 @@ const pathRegexParser = (layerRegexPath, keys) => {
|
|
|
140
116
|
}
|
|
141
117
|
return layerRegexPath.toString();
|
|
142
118
|
};
|
|
143
|
-
/**
|
|
144
|
-
* Map's the keys/path variables to the regex inside a given path
|
|
145
|
-
*
|
|
146
|
-
* @param layerRegexPath The regex for a react router with path parameters
|
|
147
|
-
* @param keys The keys that represent the path parameters
|
|
148
|
-
* @returns The regex for a path variable converted to original string on the express route
|
|
149
|
-
*/
|
|
150
119
|
const mapKeysToPath = (layerRegexPath, keys) => {
|
|
151
120
|
if (!keys || keys.length === 0) {
|
|
152
121
|
throw Error('must include atleast one key to map');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
],
|
|
19
19
|
"license": "ISC",
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@devmoods/eslint-config": "^1.
|
|
21
|
+
"@devmoods/eslint-config": "^1.15.0",
|
|
22
22
|
"@types/koa-compose": "^3.2.8",
|
|
23
|
-
"@types/node": "22.8.
|
|
23
|
+
"@types/node": "22.8.6",
|
|
24
24
|
"@types/supertest": "^6.0.2",
|
|
25
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
26
|
-
"@typescript-eslint/parser": "^8.
|
|
27
|
-
"@vitest/coverage-v8": "^2.1.
|
|
28
|
-
"
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^8.12.2",
|
|
26
|
+
"@typescript-eslint/parser": "^8.12.2",
|
|
27
|
+
"@vitest/coverage-v8": "^2.1.4",
|
|
28
|
+
"cookiejar": "^2.1.4",
|
|
29
29
|
"eslint": "^8.57.0",
|
|
30
30
|
"eslint-plugin-import": "^2.31.0",
|
|
31
31
|
"eslint-plugin-prettier": "^5.2.1",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"prettier": "3.3.3",
|
|
34
34
|
"supertest": "7.0.0",
|
|
35
35
|
"ts-node": "10.9.2",
|
|
36
|
-
"tsx": "^4.19.
|
|
36
|
+
"tsx": "^4.19.2",
|
|
37
37
|
"typescript": "5.6.3",
|
|
38
|
-
"vitest": "^2.1.
|
|
38
|
+
"vitest": "^2.1.4",
|
|
39
39
|
"vitest-fail-on-console": "^0.7.1"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@devmoods/fetch": "^4.0.1",
|
|
43
|
-
"@sentry/node": "^8.
|
|
44
|
-
"@sentry/profiling-node": "^8.
|
|
43
|
+
"@sentry/node": "^8.36.0",
|
|
44
|
+
"@sentry/profiling-node": "^8.36.0",
|
|
45
45
|
"@types/bcrypt": "^5.0.2",
|
|
46
46
|
"@types/body-parser": "^1.19.5",
|
|
47
47
|
"@types/cookie-parser": "^1.4.7",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"ajv": "^8.17.1",
|
|
55
55
|
"bcrypt": "^5.1.1",
|
|
56
56
|
"body-parser": "^1.20.3",
|
|
57
|
+
"chalk": "^5.3.0",
|
|
57
58
|
"commander": "^12.1.0",
|
|
58
59
|
"compose-middleware": "^5.0.1",
|
|
59
60
|
"cookie-parser": "^1.4.7",
|
|
@@ -61,11 +62,11 @@
|
|
|
61
62
|
"express": "^4.21.1",
|
|
62
63
|
"helmet": "^8.0.0",
|
|
63
64
|
"jsonwebtoken": "^9.0.2",
|
|
64
|
-
"nodemailer": "^6.9.
|
|
65
|
+
"nodemailer": "^6.9.16",
|
|
65
66
|
"pg": "^8.13.1",
|
|
66
67
|
"redis": "^4.7.0",
|
|
67
68
|
"sql-template-strings": "^2.2.2",
|
|
68
|
-
"uuid": "^
|
|
69
|
+
"uuid": "^11.0.2"
|
|
69
70
|
},
|
|
70
71
|
"prettier": {
|
|
71
72
|
"singleQuote": true
|