@directus/api 12.1.3 → 13.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/cli/utils/create-env/env-stub.liquid +2 -2
- package/dist/database/migrations/20230721A-require-shares-fields.js +16 -0
- package/dist/env.js +4 -2
- package/dist/flows.d.ts +1 -0
- package/dist/flows.js +17 -12
- package/dist/logger.js +1 -2
- package/dist/operations/exec/index.js +42 -33
- package/dist/operations/json-web-token/index.d.ts +10 -0
- package/dist/operations/json-web-token/index.js +35 -0
- package/dist/operations/notification/index.js +2 -2
- package/dist/services/fields.js +2 -2
- package/dist/services/graphql/index.js +0 -10
- package/dist/services/server.js +0 -3
- package/dist/services/specifications.js +2 -1
- package/dist/utils/get-default-value.d.ts +4 -1
- package/dist/utils/get-default-value.js +2 -2
- package/dist/utils/redact-object.d.ts +23 -0
- package/dist/utils/{redact.js → redact-object.js} +45 -26
- package/dist/utils/sanitize-query.js +10 -6
- package/package.json +17 -17
- package/dist/utils/redact.d.ts +0 -15
|
@@ -148,8 +148,8 @@ CACHE_ENABLED=false
|
|
|
148
148
|
# How long the cache is persisted ["5m"]
|
|
149
149
|
# CACHE_TTL="30m"
|
|
150
150
|
|
|
151
|
-
# How to scope the cache data ["
|
|
152
|
-
# CACHE_NAMESPACE="
|
|
151
|
+
# How to scope the cache data ["system-cache"]
|
|
152
|
+
# CACHE_NAMESPACE="system-cache"
|
|
153
153
|
|
|
154
154
|
# Automatically purge the cache on create, update, and delete actions. [false]
|
|
155
155
|
# CACHE_AUTO_PURGE=true
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
export async function up(knex) {
|
|
2
2
|
await knex.schema.alterTable('directus_shares', (table) => {
|
|
3
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
4
|
+
// Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
|
|
5
|
+
table.dropForeign('collection', 'directus_shares_collection_foreign');
|
|
6
|
+
}
|
|
3
7
|
table.dropNullable('collection');
|
|
8
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
9
|
+
// Recreate foreign key constraint, from 20211211A-add-shares.ts
|
|
10
|
+
table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
|
|
11
|
+
}
|
|
4
12
|
table.dropNullable('item');
|
|
5
13
|
});
|
|
6
14
|
}
|
|
7
15
|
export async function down(knex) {
|
|
8
16
|
await knex.schema.alterTable('directus_shares', (table) => {
|
|
17
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
18
|
+
// Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
|
|
19
|
+
table.dropForeign('collection', 'directus_shares_collection_foreign');
|
|
20
|
+
}
|
|
9
21
|
table.setNullable('collection');
|
|
22
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
23
|
+
// Recreate foreign key constraint, from 20211211A-add-shares.ts
|
|
24
|
+
table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
|
|
25
|
+
}
|
|
10
26
|
table.setNullable('item');
|
|
11
27
|
});
|
|
12
28
|
}
|
package/dist/env.js
CHANGED
|
@@ -192,8 +192,9 @@ const allowedEnvironmentVars = [
|
|
|
192
192
|
'RELATIONAL_BATCH_SIZE',
|
|
193
193
|
'EXPORT_BATCH_SIZE',
|
|
194
194
|
// flows
|
|
195
|
-
'FLOWS_EXEC_ALLOWED_MODULES',
|
|
196
195
|
'FLOWS_ENV_ALLOW_LIST',
|
|
196
|
+
'FLOWS_RUN_SCRIPT_MAX_MEMORY',
|
|
197
|
+
'FLOWS_RUN_SCRIPT_TIMEOUT',
|
|
197
198
|
// websockets
|
|
198
199
|
'WEBSOCKETS_.+',
|
|
199
200
|
].map((name) => new RegExp(`^${name}$`));
|
|
@@ -282,8 +283,9 @@ const defaults = {
|
|
|
282
283
|
WEBSOCKETS_GRAPHQL_PATH: '/graphql',
|
|
283
284
|
WEBSOCKETS_HEARTBEAT_ENABLED: true,
|
|
284
285
|
WEBSOCKETS_HEARTBEAT_PERIOD: 30,
|
|
285
|
-
FLOWS_EXEC_ALLOWED_MODULES: false,
|
|
286
286
|
FLOWS_ENV_ALLOW_LIST: false,
|
|
287
|
+
FLOWS_RUN_SCRIPT_MAX_MEMORY: 32,
|
|
288
|
+
FLOWS_RUN_SCRIPT_TIMEOUT: 10000,
|
|
287
289
|
PRESSURE_LIMITER_ENABLED: true,
|
|
288
290
|
PRESSURE_LIMITER_SAMPLE_INTERVAL: 250,
|
|
289
291
|
PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION: 0.99,
|
package/dist/flows.d.ts
CHANGED
package/dist/flows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Action
|
|
2
|
-
import { applyOptionsData, isValidJSON, parseJSON, toArray } from '@directus/utils';
|
|
1
|
+
import { Action } from '@directus/constants';
|
|
2
|
+
import { applyOptionsData, getRedactedString, isValidJSON, parseJSON, toArray } from '@directus/utils';
|
|
3
3
|
import { omit, pick } from 'lodash-es';
|
|
4
4
|
import { get } from 'micromustache';
|
|
5
5
|
import getDatabase from './database/index.js';
|
|
@@ -16,7 +16,7 @@ import { constructFlowTree } from './utils/construct-flow-tree.js';
|
|
|
16
16
|
import { getSchema } from './utils/get-schema.js';
|
|
17
17
|
import { JobQueue } from './utils/job-queue.js';
|
|
18
18
|
import { mapValuesDeep } from './utils/map-values-deep.js';
|
|
19
|
-
import {
|
|
19
|
+
import { redactObject } from './utils/redact-object.js';
|
|
20
20
|
import { sanitizeError } from './utils/sanitize-error.js';
|
|
21
21
|
import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
|
|
22
22
|
let flowManager;
|
|
@@ -38,8 +38,10 @@ class FlowManager {
|
|
|
38
38
|
operationFlowHandlers = {};
|
|
39
39
|
webhookFlowHandlers = {};
|
|
40
40
|
reloadQueue;
|
|
41
|
+
envs;
|
|
41
42
|
constructor() {
|
|
42
43
|
this.reloadQueue = new JobQueue();
|
|
44
|
+
this.envs = env['FLOWS_ENV_ALLOW_LIST'] ? pick(env, toArray(env['FLOWS_ENV_ALLOW_LIST'])) : {};
|
|
43
45
|
const messenger = getMessenger();
|
|
44
46
|
messenger.subscribe('flows', (event) => {
|
|
45
47
|
if (event['type'] === 'reload') {
|
|
@@ -238,7 +240,7 @@ class FlowManager {
|
|
|
238
240
|
[TRIGGER_KEY]: data,
|
|
239
241
|
[LAST_KEY]: data,
|
|
240
242
|
[ACCOUNTABILITY_KEY]: context?.['accountability'] ?? null,
|
|
241
|
-
[ENV_KEY]:
|
|
243
|
+
[ENV_KEY]: this.envs,
|
|
242
244
|
};
|
|
243
245
|
let nextOperation = flow.operation;
|
|
244
246
|
let lastOperationStatus = 'unknown';
|
|
@@ -276,14 +278,17 @@ class FlowManager {
|
|
|
276
278
|
collection: 'directus_flows',
|
|
277
279
|
item: flow.id,
|
|
278
280
|
data: {
|
|
279
|
-
steps: steps,
|
|
280
|
-
data:
|
|
281
|
-
|
|
282
|
-
[
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
steps: steps.map((step) => redactObject(step, { values: this.envs }, getRedactedString)),
|
|
282
|
+
data: redactObject(omit(keyedData, '$accountability.permissions'), // Permissions is a ton of data, and is just a copy of what's in the directus_permissions table
|
|
283
|
+
{
|
|
284
|
+
keys: [
|
|
285
|
+
['**', 'headers', 'authorization'],
|
|
286
|
+
['**', 'headers', 'cookie'],
|
|
287
|
+
['**', 'query', 'access_token'],
|
|
288
|
+
['**', 'payload', 'password'],
|
|
289
|
+
],
|
|
290
|
+
values: this.envs,
|
|
291
|
+
}, getRedactedString),
|
|
287
292
|
},
|
|
288
293
|
});
|
|
289
294
|
}
|
package/dist/logger.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { REDACTED_TEXT } from '@directus/
|
|
2
|
-
import { toArray } from '@directus/utils';
|
|
1
|
+
import { REDACTED_TEXT, toArray } from '@directus/utils';
|
|
3
2
|
import { merge } from 'lodash-es';
|
|
4
3
|
import { pino } from 'pino';
|
|
5
4
|
import { pinoHttp, stdSerializers } from 'pino-http';
|
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
import { defineOperationApi
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { defineOperationApi } from '@directus/utils';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const ivm = require('isolated-vm');
|
|
5
|
+
/**
|
|
6
|
+
* A helper for making the logs prettier.
|
|
7
|
+
* The logger prints arrays with their indices but this looks "bad" when you have only one argument.
|
|
8
|
+
*/
|
|
9
|
+
function unpackArgs(args) {
|
|
10
|
+
return args.length === 1 ? args[0] : args;
|
|
11
|
+
}
|
|
4
12
|
export default defineOperationApi({
|
|
5
13
|
id: 'exec',
|
|
6
|
-
handler: async ({ code }, { data, env }) => {
|
|
7
|
-
const allowedModules = env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [];
|
|
8
|
-
const allowedModulesBuiltIn = [];
|
|
9
|
-
const allowedModulesExternal = [];
|
|
14
|
+
handler: async ({ code }, { data, env, logger }) => {
|
|
10
15
|
const allowedEnv = data['$env'] ?? {};
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
const isolateSizeMb = env['FLOWS_RUN_SCRIPT_MAX_MEMORY'];
|
|
17
|
+
const scriptTimeoutMs = env['FLOWS_RUN_SCRIPT_TIMEOUT'];
|
|
18
|
+
const isolate = new ivm.Isolate({ memoryLimit: isolateSizeMb });
|
|
19
|
+
const context = isolate.createContextSync();
|
|
20
|
+
const jail = context.global;
|
|
21
|
+
jail.setSync('global', jail.derefInto());
|
|
22
|
+
jail.setSync('process', { env: allowedEnv }, { copy: true });
|
|
23
|
+
jail.setSync('module', { exports: null }, { copy: true });
|
|
24
|
+
jail.setSync('console', {
|
|
25
|
+
log: new ivm.Callback((...args) => logger.info(unpackArgs(args)), { sync: true }),
|
|
26
|
+
info: new ivm.Callback((...args) => logger.info(unpackArgs(args)), { sync: true }),
|
|
27
|
+
warn: new ivm.Callback((...args) => logger.warn(unpackArgs(args)), { sync: true }),
|
|
28
|
+
error: new ivm.Callback((...args) => logger.error(unpackArgs(args)), { sync: true }),
|
|
29
|
+
trace: new ivm.Callback((...args) => logger.trace(unpackArgs(args)), { sync: true }),
|
|
30
|
+
debug: new ivm.Callback((...args) => logger.debug(unpackArgs(args)), { sync: true }),
|
|
31
|
+
}, { copy: true });
|
|
32
|
+
// Run the operation once to define the module.exports function
|
|
33
|
+
await context.eval(code, { timeout: scriptTimeoutMs });
|
|
34
|
+
const inputData = new ivm.ExternalCopy({ data });
|
|
35
|
+
const resultRef = await context.evalClosure(`return module.exports($0.data)`, [inputData.copyInto()], {
|
|
36
|
+
result: { reference: true, promise: true },
|
|
37
|
+
timeout: scriptTimeoutMs,
|
|
38
|
+
});
|
|
39
|
+
const result = await resultRef.copy();
|
|
40
|
+
// Memory cleanup
|
|
41
|
+
resultRef.release();
|
|
42
|
+
inputData.release();
|
|
43
|
+
context.release();
|
|
44
|
+
isolate.dispose();
|
|
45
|
+
return result;
|
|
37
46
|
},
|
|
38
47
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
type Options = {
|
|
3
|
+
operation: string;
|
|
4
|
+
payload?: Record<string, any> | string;
|
|
5
|
+
token?: string;
|
|
6
|
+
secret?: jwt.Secret;
|
|
7
|
+
options?: any;
|
|
8
|
+
};
|
|
9
|
+
declare const _default: import("@directus/types").OperationApiConfig<Options>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineOperationApi, optionToObject, optionToString } from '@directus/utils';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
export default defineOperationApi({
|
|
4
|
+
id: 'json-web-token',
|
|
5
|
+
handler: async ({ operation, payload, token, secret, options }) => {
|
|
6
|
+
if (operation === 'sign') {
|
|
7
|
+
if (!payload)
|
|
8
|
+
throw new Error('Undefined JSON Web Token payload');
|
|
9
|
+
if (!secret)
|
|
10
|
+
throw new Error('Undefined JSON Web Token secret');
|
|
11
|
+
const payloadObject = optionToObject(payload);
|
|
12
|
+
const secretString = optionToString(secret);
|
|
13
|
+
const optionsObject = optionToObject(options);
|
|
14
|
+
return jwt.sign(payloadObject, secretString, optionsObject);
|
|
15
|
+
}
|
|
16
|
+
else if (operation === 'verify') {
|
|
17
|
+
if (!token)
|
|
18
|
+
throw new Error('Undefined JSON Web Token token');
|
|
19
|
+
if (!secret)
|
|
20
|
+
throw new Error('Undefined JSON Web Token secret');
|
|
21
|
+
const tokenString = optionToString(token);
|
|
22
|
+
const secretString = optionToString(secret);
|
|
23
|
+
const optionsObject = optionToObject(options);
|
|
24
|
+
return jwt.verify(tokenString, secretString, optionsObject);
|
|
25
|
+
}
|
|
26
|
+
else if (operation === 'decode') {
|
|
27
|
+
if (!token)
|
|
28
|
+
throw new Error('Undefined JSON Web Token token');
|
|
29
|
+
const tokenString = optionToString(token);
|
|
30
|
+
const optionsObject = optionToObject(options);
|
|
31
|
+
return jwt.decode(tokenString, optionsObject);
|
|
32
|
+
}
|
|
33
|
+
throw new Error('Undefined "Operation" for JSON Web Token');
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -24,8 +24,8 @@ export default defineOperationApi({
|
|
|
24
24
|
knex: database,
|
|
25
25
|
});
|
|
26
26
|
const messageString = message ? optionToString(message) : null;
|
|
27
|
-
const collectionString =
|
|
28
|
-
const itemString =
|
|
27
|
+
const collectionString = collection ? optionToString(collection) : null;
|
|
28
|
+
const itemString = item ? optionToString(item) : null;
|
|
29
29
|
const payload = toArray(recipient).map((userId) => {
|
|
30
30
|
return {
|
|
31
31
|
recipient: userId,
|
package/dist/services/fields.js
CHANGED
|
@@ -67,7 +67,7 @@ export class FieldsService {
|
|
|
67
67
|
}
|
|
68
68
|
const columns = (await this.schemaInspector.columnInfo(collection)).map((column) => ({
|
|
69
69
|
...column,
|
|
70
|
-
default_value: getDefaultValue(column),
|
|
70
|
+
default_value: getDefaultValue(column, fields.find((field) => field.collection === column.table && field.field === column.name)),
|
|
71
71
|
}));
|
|
72
72
|
const columnsWithSystem = columns.map((column) => {
|
|
73
73
|
const field = fields.find((field) => {
|
|
@@ -184,7 +184,7 @@ export class FieldsService {
|
|
|
184
184
|
const columnWithCastDefaultValue = column
|
|
185
185
|
? {
|
|
186
186
|
...column,
|
|
187
|
-
default_value: getDefaultValue(column),
|
|
187
|
+
default_value: getDefaultValue(column, fieldInfo),
|
|
188
188
|
}
|
|
189
189
|
: null;
|
|
190
190
|
const data = {
|
|
@@ -1532,16 +1532,6 @@ export class GraphQLService {
|
|
|
1532
1532
|
}),
|
|
1533
1533
|
}
|
|
1534
1534
|
: GraphQLBoolean,
|
|
1535
|
-
flows: {
|
|
1536
|
-
type: new GraphQLObjectType({
|
|
1537
|
-
name: 'server_info_flows',
|
|
1538
|
-
fields: {
|
|
1539
|
-
execAllowedModules: {
|
|
1540
|
-
type: new GraphQLList(GraphQLString),
|
|
1541
|
-
},
|
|
1542
|
-
},
|
|
1543
|
-
}),
|
|
1544
|
-
},
|
|
1545
1535
|
websocket: toBoolean(env['WEBSOCKETS_ENABLED'])
|
|
1546
1536
|
? {
|
|
1547
1537
|
type: new GraphQLObjectType({
|
package/dist/services/server.js
CHANGED
|
@@ -60,9 +60,6 @@ export class ServerService {
|
|
|
60
60
|
else {
|
|
61
61
|
info['rateLimitGlobal'] = false;
|
|
62
62
|
}
|
|
63
|
-
info['flows'] = {
|
|
64
|
-
execAllowedModules: env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [],
|
|
65
|
-
};
|
|
66
63
|
info['queryLimit'] = {
|
|
67
64
|
default: env['QUERY_LIMIT_DEFAULT'],
|
|
68
65
|
max: Number.isFinite(env['QUERY_LIMIT_MAX']) ? env['QUERY_LIMIT_MAX'] : -1,
|
|
@@ -186,6 +186,7 @@ class OASSpecsService {
|
|
|
186
186
|
},
|
|
187
187
|
responses: {
|
|
188
188
|
'200': {
|
|
189
|
+
description: 'Successful request',
|
|
189
190
|
content: method === 'delete'
|
|
190
191
|
? undefined
|
|
191
192
|
: {
|
|
@@ -376,7 +377,7 @@ class OASSpecsService {
|
|
|
376
377
|
{
|
|
377
378
|
type: 'string',
|
|
378
379
|
},
|
|
379
|
-
relatedTags.map((tag) => ({
|
|
380
|
+
...relatedTags.map((tag) => ({
|
|
380
381
|
$ref: `#/components/schemas/${tag.name}`,
|
|
381
382
|
})),
|
|
382
383
|
],
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { SchemaOverview } from '@directus/schema/types/overview';
|
|
2
2
|
import type { Column } from '@directus/schema';
|
|
3
|
-
|
|
3
|
+
import type { FieldMeta } from '@directus/types';
|
|
4
|
+
export default function getDefaultValue(column: SchemaOverview[string]['columns'][string] | Column, field?: {
|
|
5
|
+
special?: FieldMeta['special'];
|
|
6
|
+
}): string | boolean | number | Record<string, any> | any[] | null;
|
|
@@ -2,8 +2,8 @@ import { parseJSON } from '@directus/utils';
|
|
|
2
2
|
import env from '../env.js';
|
|
3
3
|
import logger from '../logger.js';
|
|
4
4
|
import getLocalType from './get-local-type.js';
|
|
5
|
-
export default function getDefaultValue(column) {
|
|
6
|
-
const type = getLocalType(column);
|
|
5
|
+
export default function getDefaultValue(column, field) {
|
|
6
|
+
const type = getLocalType(column, field);
|
|
7
7
|
const defaultValue = column.default_value ?? null;
|
|
8
8
|
if (defaultValue === null)
|
|
9
9
|
return null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { UnknownObject } from '@directus/types';
|
|
2
|
+
type Keys = string[][];
|
|
3
|
+
type Values = Record<string, any>;
|
|
4
|
+
type Replacement = (key?: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Redact values in an object.
|
|
7
|
+
*
|
|
8
|
+
* @param input Input object in which values should be redacted.
|
|
9
|
+
* @param redact The key paths at which and values itself which should be redacted.
|
|
10
|
+
* @param redact.keys Nested array of key paths at which values should be redacted. (Supports `*` for shallow matching, `**` for deep matching.)
|
|
11
|
+
* @param redact.values Value names and the corresponding values that should be redacted.
|
|
12
|
+
* @param replacement Replacement function with which the values are redacted.
|
|
13
|
+
* @returns Redacted object.
|
|
14
|
+
*/
|
|
15
|
+
export declare function redactObject(input: UnknownObject, redact: {
|
|
16
|
+
keys?: Keys;
|
|
17
|
+
values?: Values;
|
|
18
|
+
}, replacement: Replacement): UnknownObject;
|
|
19
|
+
/**
|
|
20
|
+
* Replace values and extract Error objects for use with JSON.stringify()
|
|
21
|
+
*/
|
|
22
|
+
export declare function getReplacer(replacement: Replacement, values?: Values): (_key: string, value: unknown) => unknown;
|
|
23
|
+
export {};
|
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { isObject } from '@directus/utils';
|
|
2
2
|
/**
|
|
3
|
-
* Redact values
|
|
3
|
+
* Redact values in an object.
|
|
4
|
+
*
|
|
4
5
|
* @param input Input object in which values should be redacted.
|
|
5
|
-
* @param
|
|
6
|
-
* @param
|
|
6
|
+
* @param redact The key paths at which and values itself which should be redacted.
|
|
7
|
+
* @param redact.keys Nested array of key paths at which values should be redacted. (Supports `*` for shallow matching, `**` for deep matching.)
|
|
8
|
+
* @param redact.values Value names and the corresponding values that should be redacted.
|
|
9
|
+
* @param replacement Replacement function with which the values are redacted.
|
|
7
10
|
* @returns Redacted object.
|
|
8
11
|
*/
|
|
9
|
-
export function
|
|
12
|
+
export function redactObject(input, redact, replacement) {
|
|
10
13
|
const wildcardChars = ['*', '**'];
|
|
11
|
-
const clone = JSON.parse(JSON.stringify(input,
|
|
14
|
+
const clone = JSON.parse(JSON.stringify(input, getReplacer(replacement, redact.values)));
|
|
12
15
|
const visited = new WeakSet();
|
|
13
|
-
|
|
16
|
+
if (redact.keys) {
|
|
17
|
+
traverse(clone, redact.keys);
|
|
18
|
+
}
|
|
14
19
|
return clone;
|
|
15
|
-
function traverse(object,
|
|
16
|
-
if (
|
|
20
|
+
function traverse(object, checkKeyPaths) {
|
|
21
|
+
if (checkKeyPaths.length === 0) {
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
24
|
visited.add(object);
|
|
25
|
+
const REDACTED_TEXT = replacement();
|
|
20
26
|
const globalCheckPaths = [];
|
|
21
27
|
for (const key of Object.keys(object)) {
|
|
22
28
|
const localCheckPaths = [];
|
|
23
|
-
for (const [index, path] of [...
|
|
29
|
+
for (const [index, path] of [...checkKeyPaths].entries()) {
|
|
24
30
|
const [current, ...remaining] = path;
|
|
25
31
|
const escapedKey = wildcardChars.includes(key) ? `\\${key}` : key;
|
|
26
32
|
switch (current) {
|
|
@@ -29,17 +35,17 @@ export function redact(input, paths, replacement) {
|
|
|
29
35
|
localCheckPaths.push(remaining);
|
|
30
36
|
}
|
|
31
37
|
else {
|
|
32
|
-
object[key] =
|
|
33
|
-
|
|
38
|
+
object[key] = REDACTED_TEXT;
|
|
39
|
+
checkKeyPaths.splice(index, 1);
|
|
34
40
|
}
|
|
35
41
|
break;
|
|
36
42
|
case '*':
|
|
37
43
|
if (remaining.length > 0) {
|
|
38
44
|
globalCheckPaths.push(remaining);
|
|
39
|
-
|
|
45
|
+
checkKeyPaths.splice(index, 1);
|
|
40
46
|
}
|
|
41
47
|
else {
|
|
42
|
-
object[key] =
|
|
48
|
+
object[key] = REDACTED_TEXT;
|
|
43
49
|
}
|
|
44
50
|
break;
|
|
45
51
|
case '**':
|
|
@@ -47,7 +53,7 @@ export function redact(input, paths, replacement) {
|
|
|
47
53
|
const [next, ...nextRemaining] = remaining;
|
|
48
54
|
if (next === escapedKey) {
|
|
49
55
|
if (nextRemaining.length === 0) {
|
|
50
|
-
object[key] =
|
|
56
|
+
object[key] = REDACTED_TEXT;
|
|
51
57
|
}
|
|
52
58
|
else {
|
|
53
59
|
localCheckPaths.push(nextRemaining);
|
|
@@ -61,7 +67,7 @@ export function redact(input, paths, replacement) {
|
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
else {
|
|
64
|
-
object[key] =
|
|
70
|
+
object[key] = REDACTED_TEXT;
|
|
65
71
|
}
|
|
66
72
|
break;
|
|
67
73
|
}
|
|
@@ -74,16 +80,29 @@ export function redact(input, paths, replacement) {
|
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
/**
|
|
77
|
-
*
|
|
83
|
+
* Replace values and extract Error objects for use with JSON.stringify()
|
|
78
84
|
*/
|
|
79
|
-
export function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
export function getReplacer(replacement, values) {
|
|
86
|
+
const filteredValues = values
|
|
87
|
+
? Object.entries(values).filter(([_k, v]) => typeof v === 'string' && v.length > 0)
|
|
88
|
+
: [];
|
|
89
|
+
return (_key, value) => {
|
|
90
|
+
if (value instanceof Error) {
|
|
91
|
+
return {
|
|
92
|
+
name: value.name,
|
|
93
|
+
message: value.message,
|
|
94
|
+
stack: value.stack,
|
|
95
|
+
cause: value.cause,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!values || filteredValues.length === 0 || typeof value !== 'string')
|
|
99
|
+
return value;
|
|
100
|
+
let finalValue = value;
|
|
101
|
+
for (const [redactKey, valueToRedact] of filteredValues) {
|
|
102
|
+
if (finalValue.includes(valueToRedact)) {
|
|
103
|
+
finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return finalValue;
|
|
107
|
+
};
|
|
89
108
|
}
|
|
@@ -146,22 +146,26 @@ function sanitizeDeep(deep, accountability) {
|
|
|
146
146
|
parse(deep);
|
|
147
147
|
return result;
|
|
148
148
|
function parse(level, path = []) {
|
|
149
|
+
const subQuery = {};
|
|
149
150
|
const parsedLevel = {};
|
|
150
151
|
for (const [key, value] of Object.entries(level)) {
|
|
151
152
|
if (!key)
|
|
152
153
|
break;
|
|
153
154
|
if (key.startsWith('_')) {
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
// ...however we want to keep them for the nested structure of deep, otherwise there's no
|
|
157
|
-
// way of knowing when to keep nesting and when to stop
|
|
158
|
-
const [parsedKey, parsedValue] = Object.entries(parsedSubQuery)[0];
|
|
159
|
-
parsedLevel[`_${parsedKey}`] = parsedValue;
|
|
155
|
+
// Collect all sub query parameters without the leading underscore
|
|
156
|
+
subQuery[key.substring(1)] = value;
|
|
160
157
|
}
|
|
161
158
|
else if (isPlainObject(value)) {
|
|
162
159
|
parse(value, [...path, key]);
|
|
163
160
|
}
|
|
164
161
|
}
|
|
162
|
+
if (Object.keys(subQuery).length > 0) {
|
|
163
|
+
// Sanitize the entire sub query
|
|
164
|
+
const parsedSubQuery = sanitizeQuery(subQuery, accountability);
|
|
165
|
+
for (const [parsedKey, parsedValue] of Object.entries(parsedSubQuery)) {
|
|
166
|
+
parsedLevel[`_${parsedKey}`] = parsedValue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
165
169
|
if (Object.keys(parsedLevel).length > 0) {
|
|
166
170
|
set(result, path, merge({}, get(result, path, {}), parsedLevel));
|
|
167
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -97,11 +97,12 @@
|
|
|
97
97
|
"icc": "3.0.0",
|
|
98
98
|
"inquirer": "9.2.4",
|
|
99
99
|
"ioredis": "5.3.2",
|
|
100
|
+
"isolated-vm": "4.6.0",
|
|
100
101
|
"joi": "17.9.2",
|
|
101
102
|
"js-yaml": "4.1.0",
|
|
102
103
|
"js2xmlparser": "5.0.0",
|
|
103
104
|
"json2csv": "5.0.7",
|
|
104
|
-
"jsonwebtoken": "9.0.
|
|
105
|
+
"jsonwebtoken": "9.0.1",
|
|
105
106
|
"keyv": "4.5.2",
|
|
106
107
|
"knex": "2.4.2",
|
|
107
108
|
"ldapjs": "2.3.3",
|
|
@@ -138,27 +139,26 @@
|
|
|
138
139
|
"tsx": "3.12.7",
|
|
139
140
|
"uuid": "9.0.0",
|
|
140
141
|
"uuid-validate": "0.0.3",
|
|
141
|
-
"vm2": "3.9.19",
|
|
142
142
|
"wellknown": "0.5.0",
|
|
143
143
|
"ws": "8.12.1",
|
|
144
144
|
"zod": "3.21.4",
|
|
145
145
|
"zod-validation-error": "1.0.1",
|
|
146
|
-
"@directus/app": "10.
|
|
147
|
-
"@directus/constants": "10.2.
|
|
146
|
+
"@directus/app": "10.7.0",
|
|
147
|
+
"@directus/constants": "10.2.3",
|
|
148
148
|
"@directus/errors": "0.0.2",
|
|
149
|
-
"@directus/extensions-sdk": "10.1.
|
|
150
|
-
"@directus/pressure": "1.0.
|
|
149
|
+
"@directus/extensions-sdk": "10.1.9",
|
|
150
|
+
"@directus/pressure": "1.0.8",
|
|
151
151
|
"@directus/schema": "10.0.2",
|
|
152
|
-
"@directus/specs": "10.
|
|
152
|
+
"@directus/specs": "10.2.0",
|
|
153
153
|
"@directus/storage": "10.0.5",
|
|
154
|
-
"@directus/storage-driver-azure": "10.0.
|
|
155
|
-
"@directus/storage-driver-cloudinary": "10.0.
|
|
156
|
-
"@directus/storage-driver-gcs": "10.0.
|
|
157
|
-
"@directus/storage-driver-local": "10.0.
|
|
158
|
-
"@directus/storage-driver-s3": "10.0.
|
|
159
|
-
"@directus/storage-driver-supabase": "1.0.
|
|
160
|
-
"@directus/utils": "10.0.
|
|
161
|
-
"@directus/validation": "0.0.
|
|
154
|
+
"@directus/storage-driver-azure": "10.0.9",
|
|
155
|
+
"@directus/storage-driver-cloudinary": "10.0.9",
|
|
156
|
+
"@directus/storage-driver-gcs": "10.0.9",
|
|
157
|
+
"@directus/storage-driver-local": "10.0.9",
|
|
158
|
+
"@directus/storage-driver-s3": "10.0.9",
|
|
159
|
+
"@directus/storage-driver-supabase": "1.0.1",
|
|
160
|
+
"@directus/utils": "10.0.9",
|
|
161
|
+
"@directus/validation": "0.0.4"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@ngneat/falso": "6.4.0",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"vitest": "0.31.1",
|
|
208
208
|
"@directus/random": "0.2.2",
|
|
209
209
|
"@directus/tsconfig": "1.0.0",
|
|
210
|
-
"@directus/types": "10.1.
|
|
210
|
+
"@directus/types": "10.1.5"
|
|
211
211
|
},
|
|
212
212
|
"optionalDependencies": {
|
|
213
213
|
"@keyv/redis": "2.5.8",
|
package/dist/utils/redact.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { UnknownObject } from '@directus/types';
|
|
2
|
-
type Paths = string[][];
|
|
3
|
-
/**
|
|
4
|
-
* Redact values at certain paths in an object.
|
|
5
|
-
* @param input Input object in which values should be redacted.
|
|
6
|
-
* @param paths Nested array of object paths to be redacted (supports `*` for shallow matching, `**` for deep matching).
|
|
7
|
-
* @param replacement Replacement the values are redacted by.
|
|
8
|
-
* @returns Redacted object.
|
|
9
|
-
*/
|
|
10
|
-
export declare function redact(input: UnknownObject, paths: Paths, replacement: string): UnknownObject;
|
|
11
|
-
/**
|
|
12
|
-
* Extract values from Error objects for use with JSON.stringify()
|
|
13
|
-
*/
|
|
14
|
-
export declare function errorReplacer(_key: string, value: unknown): unknown;
|
|
15
|
-
export {};
|