@wbce-d9/api 10.4.0 → 11.0.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/controllers/utils.js +7 -3
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +3 -0
- package/dist/env.js +4 -2
- package/dist/mailer.js +12 -4
- package/dist/middleware/respond.js +2 -1
- package/dist/operations/exec/index.d.ts +2 -2
- package/dist/operations/exec/index.js +41 -35
- package/dist/services/authentication.js +3 -13
- package/dist/services/graphql/index.js +3 -13
- package/dist/services/server.js +0 -3
- package/dist/services/users.js +6 -6
- package/package.json +27 -28
|
@@ -13,11 +13,15 @@ import asyncHandler from '../utils/async-handler.js';
|
|
|
13
13
|
import { generateHash } from '../utils/generate-hash.js';
|
|
14
14
|
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
15
15
|
const router = Router();
|
|
16
|
+
const randomStringSchemaDesc = Joi.object({
|
|
17
|
+
length: Joi.number().integer().min(1).max(500).default(32),
|
|
18
|
+
});
|
|
16
19
|
router.get('/random/string', asyncHandler(async (req, res) => {
|
|
17
20
|
const { nanoid } = await import('nanoid');
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const { error, value } = randomStringSchemaDesc.validate(req.query, { allowUnknown: true });
|
|
22
|
+
if (error)
|
|
23
|
+
throw new InvalidQueryException(error.message);
|
|
24
|
+
const string = nanoid(value.length);
|
|
21
25
|
return res.json({ data: string });
|
|
22
26
|
}));
|
|
23
27
|
router.post('/hash/generate', asyncHandler(async (req, res) => {
|
package/dist/env.js
CHANGED
|
@@ -191,8 +191,9 @@ const allowedEnvironmentVars = [
|
|
|
191
191
|
'RELATIONAL_BATCH_SIZE',
|
|
192
192
|
'EXPORT_BATCH_SIZE',
|
|
193
193
|
// flows
|
|
194
|
-
'FLOWS_EXEC_ALLOWED_MODULES',
|
|
195
194
|
'FLOWS_ENV_ALLOW_LIST',
|
|
195
|
+
'FLOWS_SCRIPT_MAX_MEMORY',
|
|
196
|
+
'FLOWS_SCRIPT_EXEC_TIMEOUT',
|
|
196
197
|
].map((name) => new RegExp(`^${name}$`));
|
|
197
198
|
const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
|
|
198
199
|
const defaults = {
|
|
@@ -270,8 +271,9 @@ const defaults = {
|
|
|
270
271
|
EXPORT_BATCH_SIZE: 5000,
|
|
271
272
|
FILE_METADATA_ALLOW_LIST: 'ifd0.Make,ifd0.Model,exif.FNumber,exif.ExposureTime,exif.FocalLength,exif.ISO',
|
|
272
273
|
GRAPHQL_INTROSPECTION: true,
|
|
273
|
-
FLOWS_EXEC_ALLOWED_MODULES: false,
|
|
274
274
|
FLOWS_ENV_ALLOW_LIST: false,
|
|
275
|
+
FLOWS_SCRIPT_MAX_MEMORY: 32,
|
|
276
|
+
FLOWS_SCRIPT_EXEC_TIMEOUT: 10000,
|
|
275
277
|
};
|
|
276
278
|
// Allows us to force certain environment variable into a type, instead of relying
|
|
277
279
|
// on the auto-parsed type in processValues. ref #3705
|
package/dist/mailer.js
CHANGED
|
@@ -55,10 +55,18 @@ export default function getMailer() {
|
|
|
55
55
|
}));
|
|
56
56
|
}
|
|
57
57
|
else if (transportName === 'sendgrid') {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
let auth = false;
|
|
59
|
+
if (env['EMAIL_SENDGRID_API_KEY'] || env['EMAIL_SMTP_PASSWORD']) {
|
|
60
|
+
auth = {
|
|
61
|
+
user: 'apikey',
|
|
62
|
+
pass: env['EMAIL_SENDGRID_API_KEY'],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
transporter = nodemailer.createTransport({
|
|
66
|
+
host: 'smtp.sendgrid.net',
|
|
67
|
+
port: 587,
|
|
68
|
+
auth,
|
|
69
|
+
});
|
|
62
70
|
}
|
|
63
71
|
else {
|
|
64
72
|
logger.warn('Illegal transport given for email. Check the EMAIL_TRANSPORT env var.');
|
|
@@ -22,7 +22,8 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
22
22
|
cache &&
|
|
23
23
|
!req.sanitizedQuery.export &&
|
|
24
24
|
res.locals['cache'] !== false &&
|
|
25
|
-
exceedsMaxSize === false
|
|
25
|
+
exceedsMaxSize === false &&
|
|
26
|
+
req.originalUrl?.startsWith('/auth')) {
|
|
26
27
|
const key = getCacheKey(req);
|
|
27
28
|
try {
|
|
28
29
|
await setCacheValue(cache, key, res.locals['payload'], getMilliseconds(env['CACHE_TTL']));
|
|
@@ -1,38 +1,44 @@
|
|
|
1
|
-
import { defineOperationApi
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { defineOperationApi } from '@wbce-d9/utils';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const ivm = require('isolated-vm');
|
|
4
5
|
export default defineOperationApi({
|
|
5
|
-
id: '
|
|
6
|
-
handler: async ({ code }, { data, env }) => {
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
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
|
-
const
|
|
36
|
-
|
|
6
|
+
id: 'execute_script',
|
|
7
|
+
handler: async ({ code }, { data, env, logger }) => {
|
|
8
|
+
// Fetching execution limits and environment variables
|
|
9
|
+
const memoryLimit = env['FLOWS_SCRIPT_MAX_MEMORY'];
|
|
10
|
+
const executionTimeout = env['FLOWS_SCRIPT_EXEC_TIMEOUT'];
|
|
11
|
+
const environmentVariables = data['$env'] ?? {};
|
|
12
|
+
// Creating an isolate with the specified memory limit
|
|
13
|
+
const isolate = new ivm.Isolate({ memoryLimit });
|
|
14
|
+
const context = isolate.createContextSync();
|
|
15
|
+
const jail = context.global;
|
|
16
|
+
jail.setSync('global', jail.derefInto());
|
|
17
|
+
jail.setSync('process', { env: environmentVariables }, { copy: true });
|
|
18
|
+
jail.setSync('module', { exports: null }, { copy: true });
|
|
19
|
+
// Adding a secure logger for the isolate
|
|
20
|
+
const createLoggerCallback = (logFn) => new ivm.Callback((...args) => logFn(args.length === 1 ? args[0] : args), { sync: true });
|
|
21
|
+
jail.setSync('console', {
|
|
22
|
+
log: createLoggerCallback(() => logger.info),
|
|
23
|
+
info: createLoggerCallback(() => logger.info),
|
|
24
|
+
warn: createLoggerCallback(() => logger.warn),
|
|
25
|
+
error: createLoggerCallback(() => logger.error),
|
|
26
|
+
debug: createLoggerCallback(() => logger.debug),
|
|
27
|
+
trace: createLoggerCallback(() => logger.trace),
|
|
28
|
+
}, { copy: true });
|
|
29
|
+
// Executing the code within the isolate
|
|
30
|
+
await context.eval(code, { timeout: executionTimeout });
|
|
31
|
+
const dataCopy = new ivm.ExternalCopy({ data });
|
|
32
|
+
const resultReference = await context.evalClosure(`return module.exports($0.data)`, [dataCopy.copyInto()], {
|
|
33
|
+
result: { reference: true, promise: true },
|
|
34
|
+
timeout: executionTimeout,
|
|
35
|
+
});
|
|
36
|
+
const finalResult = await resultReference.copy();
|
|
37
|
+
// Cleanup memory
|
|
38
|
+
resultReference.release();
|
|
39
|
+
dataCopy.release();
|
|
40
|
+
context.release();
|
|
41
|
+
isolate.dispose();
|
|
42
|
+
return finalResult;
|
|
37
43
|
},
|
|
38
44
|
});
|
|
@@ -7,7 +7,7 @@ import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
|
|
|
7
7
|
import getDatabase from '../database/index.js';
|
|
8
8
|
import emitter from '../emitter.js';
|
|
9
9
|
import env from '../env.js';
|
|
10
|
-
import { InvalidCredentialsException, InvalidOTPException,
|
|
10
|
+
import { InvalidCredentialsException, InvalidOTPException, UserSuspendedException } from '../exceptions/index.js';
|
|
11
11
|
import { createRateLimiter } from '../rate-limiter.js';
|
|
12
12
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
13
13
|
import { stall } from '../utils/stall.js';
|
|
@@ -72,20 +72,10 @@ export class AuthenticationService {
|
|
|
72
72
|
accountability: this.accountability,
|
|
73
73
|
});
|
|
74
74
|
};
|
|
75
|
-
if (user?.status !== 'active') {
|
|
75
|
+
if (user?.status !== 'active' || user.provider.toLowerCase() !== providerName.toLowerCase()) {
|
|
76
76
|
emitStatus('fail');
|
|
77
|
-
if (user?.status === 'suspended') {
|
|
78
|
-
await stall(STALL_TIME, timeStart);
|
|
79
|
-
throw new UserSuspendedException();
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
await stall(STALL_TIME, timeStart);
|
|
83
|
-
throw new InvalidCredentialsException();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
else if (user.provider.toLowerCase() !== providerName.toLowerCase()) {
|
|
87
77
|
await stall(STALL_TIME, timeStart);
|
|
88
|
-
throw new
|
|
78
|
+
throw new InvalidCredentialsException();
|
|
89
79
|
}
|
|
90
80
|
const settingsService = new SettingsService({
|
|
91
81
|
knex: this.knex,
|
|
@@ -1526,16 +1526,6 @@ export class GraphQLService {
|
|
|
1526
1526
|
}),
|
|
1527
1527
|
}
|
|
1528
1528
|
: GraphQLBoolean,
|
|
1529
|
-
flows: {
|
|
1530
|
-
type: new GraphQLObjectType({
|
|
1531
|
-
name: 'server_info_flows',
|
|
1532
|
-
fields: {
|
|
1533
|
-
execAllowedModules: {
|
|
1534
|
-
type: new GraphQLList(GraphQLString),
|
|
1535
|
-
},
|
|
1536
|
-
},
|
|
1537
|
-
}),
|
|
1538
|
-
},
|
|
1539
1529
|
});
|
|
1540
1530
|
}
|
|
1541
1531
|
if (this.accountability?.admin === true) {
|
|
@@ -1894,10 +1884,10 @@ export class GraphQLService {
|
|
|
1894
1884
|
},
|
|
1895
1885
|
resolve: async (_, args) => {
|
|
1896
1886
|
const { nanoid } = await import('nanoid');
|
|
1897
|
-
if (args['length'] &&
|
|
1898
|
-
throw new InvalidPayloadException(`"length"
|
|
1887
|
+
if (args['length'] !== undefined && (args['length'] < 1 || args['length'] > 500)) {
|
|
1888
|
+
throw new InvalidPayloadException(`"length" must be between 1 and 500`);
|
|
1899
1889
|
}
|
|
1900
|
-
return nanoid(args['length'] ?
|
|
1890
|
+
return nanoid(args['length'] ? args['length'] : 32);
|
|
1901
1891
|
},
|
|
1902
1892
|
},
|
|
1903
1893
|
utils_hash_generate: {
|
package/dist/services/server.js
CHANGED
|
@@ -61,9 +61,6 @@ export class ServerService {
|
|
|
61
61
|
else {
|
|
62
62
|
info['rateLimitGlobal'] = false;
|
|
63
63
|
}
|
|
64
|
-
info['flows'] = {
|
|
65
|
-
execAllowedModules: env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [],
|
|
66
|
-
};
|
|
67
64
|
}
|
|
68
65
|
if (this.accountability?.admin === true) {
|
|
69
66
|
const { osType, osVersion } = getOSInfo();
|
package/dist/services/users.js
CHANGED
|
@@ -117,7 +117,7 @@ export class UsersService extends ItemsService {
|
|
|
117
117
|
*/
|
|
118
118
|
async getUserByEmail(email) {
|
|
119
119
|
return await this.knex
|
|
120
|
-
.select('id', 'role', 'status', 'password')
|
|
120
|
+
.select('id', 'role', 'status', 'password', 'email')
|
|
121
121
|
.from('directus_users')
|
|
122
122
|
.whereRaw(`LOWER(??) = ?`, ['email', email.toLowerCase()])
|
|
123
123
|
.first();
|
|
@@ -307,13 +307,13 @@ export class UsersService extends ItemsService {
|
|
|
307
307
|
if (isEmpty(user) || user.status === 'invited') {
|
|
308
308
|
const subjectLine = subject ?? "You've been invited";
|
|
309
309
|
await mailService.send({
|
|
310
|
-
to: email,
|
|
310
|
+
to: user.email,
|
|
311
311
|
subject: subjectLine,
|
|
312
312
|
template: {
|
|
313
313
|
name: 'user-invitation',
|
|
314
314
|
data: {
|
|
315
315
|
url: this.inviteUrl(email, url),
|
|
316
|
-
email,
|
|
316
|
+
email: user.email,
|
|
317
317
|
},
|
|
318
318
|
},
|
|
319
319
|
});
|
|
@@ -351,20 +351,20 @@ export class UsersService extends ItemsService {
|
|
|
351
351
|
knex: this.knex,
|
|
352
352
|
accountability: this.accountability,
|
|
353
353
|
});
|
|
354
|
-
const payload = { email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
|
|
354
|
+
const payload = { email: user.email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
|
|
355
355
|
const token = jwt.sign(payload, env['SECRET'], { expiresIn: '1d', issuer: 'directus' });
|
|
356
356
|
const acceptURL = url
|
|
357
357
|
? new Url(url).setQuery('token', token).toString()
|
|
358
358
|
: new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password').setQuery('token', token).toString();
|
|
359
359
|
const subjectLine = subject ? subject : 'Password Reset Request';
|
|
360
360
|
await mailService.send({
|
|
361
|
-
to: email,
|
|
361
|
+
to: user.email,
|
|
362
362
|
subject: subjectLine,
|
|
363
363
|
template: {
|
|
364
364
|
name: 'password-reset',
|
|
365
365
|
data: {
|
|
366
366
|
url: acceptURL,
|
|
367
|
-
email,
|
|
367
|
+
email: user.email,
|
|
368
368
|
},
|
|
369
369
|
},
|
|
370
370
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wbce-d9/api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@wbce-d9/format-title": "10.0.0",
|
|
73
73
|
"argon2": "0.30.3",
|
|
74
74
|
"async": "3.2.4",
|
|
75
|
-
"axios": "^1.
|
|
75
|
+
"axios": "^1.8.2",
|
|
76
76
|
"busboy": "1.6.0",
|
|
77
77
|
"bytes": "3.1.2",
|
|
78
78
|
"camelcase": "7.0.1",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"chokidar": "3.5.3",
|
|
81
81
|
"commander": "10.0.1",
|
|
82
82
|
"content-disposition": "0.5.4",
|
|
83
|
-
"cookie-parser": "1.4.
|
|
83
|
+
"cookie-parser": "1.4.7",
|
|
84
84
|
"cors": "2.8.5",
|
|
85
85
|
"csv-parser": "3.0.0",
|
|
86
86
|
"date-fns": "2.29.3",
|
|
@@ -91,23 +91,24 @@
|
|
|
91
91
|
"eventemitter2": "6.4.9",
|
|
92
92
|
"execa": "7.1.1",
|
|
93
93
|
"exif-reader": "1.2.0",
|
|
94
|
-
"express": "4.
|
|
94
|
+
"express": "^4.21.2",
|
|
95
95
|
"fast-redact": "3.1.2",
|
|
96
96
|
"flat": "5.0.2",
|
|
97
97
|
"fs-extra": "11.1.1",
|
|
98
|
-
"graphql": "16.
|
|
98
|
+
"graphql": "16.8.1",
|
|
99
99
|
"graphql-compose": "9.0.10",
|
|
100
100
|
"helmet": "6.1.5",
|
|
101
101
|
"icc": "3.0.0",
|
|
102
102
|
"inquirer": "9.1.5",
|
|
103
103
|
"ioredis": "5.3.2",
|
|
104
|
+
"isolated-vm": "^5.0.3",
|
|
104
105
|
"joi": "17.9.1",
|
|
105
106
|
"js-yaml": "4.1.0",
|
|
106
107
|
"js2xmlparser": "5.0.0",
|
|
107
108
|
"json2csv": "5.0.7",
|
|
108
109
|
"jsonwebtoken": "9.0.0",
|
|
109
110
|
"keyv": "4.5.2",
|
|
110
|
-
"knex": "
|
|
111
|
+
"knex": "3.1.0",
|
|
111
112
|
"ldapjs": "2.3.3",
|
|
112
113
|
"liquidjs": "10.7.0",
|
|
113
114
|
"lodash-es": "4.17.21",
|
|
@@ -115,10 +116,10 @@
|
|
|
115
116
|
"micromustache": "8.0.3",
|
|
116
117
|
"mime-types": "2.1.35",
|
|
117
118
|
"ms": "2.1.3",
|
|
118
|
-
"nanoid": "
|
|
119
|
+
"nanoid": "5.0.9",
|
|
119
120
|
"node-cron": "3.0.2",
|
|
120
121
|
"node-machine-id": "1.1.12",
|
|
121
|
-
"nodemailer": "6.9.
|
|
122
|
+
"nodemailer": "6.9.9",
|
|
122
123
|
"object-hash": "3.0.0",
|
|
123
124
|
"openapi3-ts": "3.2.0",
|
|
124
125
|
"openid-client": "5.4.0",
|
|
@@ -130,10 +131,10 @@
|
|
|
130
131
|
"pino-pretty": "10.0.0",
|
|
131
132
|
"qs": "6.11.1",
|
|
132
133
|
"rate-limiter-flexible": "2.4.1",
|
|
133
|
-
"rollup": "3.
|
|
134
|
+
"rollup": "3.29.5",
|
|
134
135
|
"samlify": "2.8.10",
|
|
135
|
-
"sanitize-html": "2.
|
|
136
|
-
"sharp": "0.32.
|
|
136
|
+
"sanitize-html": "2.12.1",
|
|
137
|
+
"sharp": "0.32.6",
|
|
137
138
|
"snappy": "7.2.2",
|
|
138
139
|
"stream-json": "1.7.5",
|
|
139
140
|
"strip-bom-stream": "5.0.0",
|
|
@@ -141,22 +142,21 @@
|
|
|
141
142
|
"tsx": "3.12.6",
|
|
142
143
|
"uuid": "9.0.0",
|
|
143
144
|
"uuid-validate": "0.0.3",
|
|
144
|
-
"vm2": "^3.9.19",
|
|
145
145
|
"wellknown": "0.5.0",
|
|
146
|
-
"@wbce-d9/app": "9.
|
|
146
|
+
"@wbce-d9/app": "9.33.0",
|
|
147
|
+
"@wbce-d9/constants": "9.28.0",
|
|
148
|
+
"@wbce-d9/extensions-sdk": "9.27.1",
|
|
147
149
|
"@wbce-d9/exceptions": "9.26.4",
|
|
148
|
-
"@wbce-d9/constants": "9.27.0",
|
|
149
|
-
"@wbce-d9/extensions-sdk": "9.26.7",
|
|
150
|
-
"@wbce-d9/schema": "9.26.4",
|
|
151
|
-
"@wbce-d9/storage": "9.26.4",
|
|
152
150
|
"@wbce-d9/specs": "9.26.4",
|
|
153
|
-
"@wbce-d9/storage-driver-azure": "9.26.
|
|
154
|
-
"@wbce-d9/storage
|
|
155
|
-
"@wbce-d9/storage-driver-
|
|
156
|
-
"@wbce-d9/
|
|
157
|
-
"@wbce-d9/storage-driver-
|
|
158
|
-
"@wbce-d9/
|
|
159
|
-
"@wbce-d9/
|
|
151
|
+
"@wbce-d9/storage-driver-azure": "9.26.8",
|
|
152
|
+
"@wbce-d9/storage": "9.26.4",
|
|
153
|
+
"@wbce-d9/storage-driver-cloudinary": "9.27.0",
|
|
154
|
+
"@wbce-d9/schema": "9.26.4",
|
|
155
|
+
"@wbce-d9/storage-driver-s3": "9.26.10",
|
|
156
|
+
"@wbce-d9/utils": "9.29.1",
|
|
157
|
+
"@wbce-d9/storage-driver-local": "9.26.8",
|
|
158
|
+
"@wbce-d9/storage-driver-gcs": "9.26.8",
|
|
159
|
+
"@wbce-d9/update-check": "9.28.0"
|
|
160
160
|
},
|
|
161
161
|
"devDependencies": {
|
|
162
162
|
"@ngneat/falso": "6.4.0",
|
|
@@ -200,11 +200,11 @@
|
|
|
200
200
|
"@wbce-d9/tsconfig": "0.0.7",
|
|
201
201
|
"copyfiles": "2.4.1",
|
|
202
202
|
"form-data": "4.0.0",
|
|
203
|
-
"knex-mock-client": "
|
|
203
|
+
"knex-mock-client": "3.0.2",
|
|
204
204
|
"supertest": "6.3.3",
|
|
205
205
|
"typescript": "5.0.4",
|
|
206
206
|
"vitest": "0.30.1",
|
|
207
|
-
"@wbce-d9/types": "9.
|
|
207
|
+
"@wbce-d9/types": "9.28.0"
|
|
208
208
|
},
|
|
209
209
|
"optionalDependencies": {
|
|
210
210
|
"@keyv/redis": "2.5.7",
|
|
@@ -212,10 +212,9 @@
|
|
|
212
212
|
"memcached": "2.2.2",
|
|
213
213
|
"mysql": "2.18.1",
|
|
214
214
|
"nodemailer-mailgun-transport": "2.1.5",
|
|
215
|
-
"nodemailer-sendgrid": "1.0.3",
|
|
216
215
|
"pg": "8.10.0",
|
|
217
216
|
"sqlite3": "5.1.6",
|
|
218
|
-
"tedious": "
|
|
217
|
+
"tedious": "18.6.1"
|
|
219
218
|
},
|
|
220
219
|
"engines": {
|
|
221
220
|
"node": ">=18.0.0"
|