@directus/api 13.2.0 → 14.0.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.
Files changed (109) hide show
  1. package/dist/__utils__/snapshots.js +9 -0
  2. package/dist/app.js +2 -0
  3. package/dist/cli/index.js +3 -4
  4. package/dist/cli/load-extensions.d.ts +1 -0
  5. package/dist/cli/load-extensions.js +19 -0
  6. package/dist/controllers/extensions.js +28 -19
  7. package/dist/controllers/versions.d.ts +2 -0
  8. package/dist/controllers/versions.js +188 -0
  9. package/dist/database/migrations/20230823A-add-content-versioning.d.ts +3 -0
  10. package/dist/database/migrations/20230823A-add-content-versioning.js +36 -0
  11. package/dist/database/migrations/20230927A-themes.d.ts +3 -0
  12. package/dist/database/migrations/20230927A-themes.js +49 -0
  13. package/dist/database/migrations/20231009B-update-panel-options.d.ts +3 -0
  14. package/dist/database/migrations/20231009B-update-panel-options.js +77 -0
  15. package/dist/database/migrations/20231010A-add-extensions.d.ts +3 -0
  16. package/dist/database/migrations/20231010A-add-extensions.js +9 -0
  17. package/dist/database/run-ast.js +1 -1
  18. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +5 -1
  19. package/dist/database/system-data/collections/collections.yaml +6 -0
  20. package/dist/database/system-data/fields/activity.yaml +4 -4
  21. package/dist/database/system-data/fields/collections.yaml +19 -0
  22. package/dist/database/system-data/fields/extensions.yaml +10 -0
  23. package/dist/database/system-data/fields/revisions.yaml +3 -0
  24. package/dist/database/system-data/fields/settings.yaml +73 -17
  25. package/dist/database/system-data/fields/users.yaml +50 -12
  26. package/dist/database/system-data/fields/versions.yaml +38 -0
  27. package/dist/database/system-data/fields/webhooks.yaml +9 -9
  28. package/dist/database/system-data/relations/relations.yaml +88 -20
  29. package/dist/emitter.d.ts +2 -2
  30. package/dist/env.js +4 -0
  31. package/dist/extensions/lib/get-extensions-settings.d.ts +7 -0
  32. package/dist/extensions/lib/get-extensions-settings.js +39 -0
  33. package/dist/extensions/lib/get-extensions.d.ts +1 -0
  34. package/dist/extensions/lib/get-extensions.js +11 -0
  35. package/dist/extensions/{get-shared-deps-mapping.js → lib/get-shared-deps-mapping.js} +3 -3
  36. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +31 -0
  37. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.js +80 -0
  38. package/dist/extensions/lib/sandbox/generate-host-function-reference.d.ts +11 -0
  39. package/dist/extensions/lib/sandbox/generate-host-function-reference.js +28 -0
  40. package/dist/extensions/lib/sandbox/register/action.d.ts +6 -0
  41. package/dist/extensions/lib/sandbox/register/action.js +18 -0
  42. package/dist/extensions/lib/sandbox/register/call-reference.d.ts +5 -0
  43. package/dist/extensions/lib/sandbox/register/call-reference.js +20 -0
  44. package/dist/extensions/lib/sandbox/register/filter.d.ts +6 -0
  45. package/dist/extensions/lib/sandbox/register/filter.js +21 -0
  46. package/dist/extensions/lib/sandbox/register/index.d.ts +5 -0
  47. package/dist/extensions/lib/sandbox/register/index.js +5 -0
  48. package/dist/extensions/lib/sandbox/register/operation.d.ts +6 -0
  49. package/dist/extensions/lib/sandbox/register/operation.js +19 -0
  50. package/dist/extensions/lib/sandbox/register/route.d.ts +17 -0
  51. package/dist/extensions/lib/sandbox/register/route.js +44 -0
  52. package/dist/extensions/lib/sandbox/sdk/generators/index.d.ts +3 -0
  53. package/dist/extensions/lib/sandbox/sdk/generators/index.js +3 -0
  54. package/dist/extensions/lib/sandbox/sdk/generators/log.d.ts +3 -0
  55. package/dist/extensions/lib/sandbox/sdk/generators/log.js +11 -0
  56. package/dist/extensions/lib/sandbox/sdk/generators/request.d.ts +12 -0
  57. package/dist/extensions/lib/sandbox/sdk/generators/request.js +49 -0
  58. package/dist/extensions/lib/sandbox/sdk/generators/sleep.d.ts +3 -0
  59. package/dist/extensions/lib/sandbox/sdk/generators/sleep.js +11 -0
  60. package/dist/extensions/lib/sandbox/sdk/index.d.ts +2 -0
  61. package/dist/extensions/lib/sandbox/sdk/index.js +2 -0
  62. package/dist/extensions/lib/sandbox/sdk/instantiate.d.ts +11 -0
  63. package/dist/extensions/lib/sandbox/sdk/instantiate.js +28 -0
  64. package/dist/extensions/lib/sandbox/sdk/sdk.d.ts +20 -0
  65. package/dist/extensions/lib/sandbox/sdk/sdk.js +11 -0
  66. package/dist/extensions/lib/sandbox/sdk/utils/index.d.ts +1 -0
  67. package/dist/extensions/lib/sandbox/sdk/utils/index.js +1 -0
  68. package/dist/extensions/lib/sandbox/sdk/utils/wrap.d.ts +11 -0
  69. package/dist/extensions/lib/sandbox/sdk/utils/wrap.js +17 -0
  70. package/dist/extensions/manager.d.ts +128 -14
  71. package/dist/extensions/manager.js +310 -136
  72. package/dist/extensions/types.d.ts +1 -5
  73. package/dist/flows.d.ts +1 -1
  74. package/dist/flows.js +6 -6
  75. package/dist/middleware/respond.js +12 -0
  76. package/dist/server.js +2 -1
  77. package/dist/services/assets.js +1 -1
  78. package/dist/services/extensions.d.ts +31 -0
  79. package/dist/services/extensions.js +136 -0
  80. package/dist/services/graphql/index.d.ts +1 -1
  81. package/dist/services/graphql/index.js +87 -24
  82. package/dist/services/index.d.ts +2 -0
  83. package/dist/services/index.js +2 -0
  84. package/dist/services/server.js +3 -1
  85. package/dist/services/users.js +2 -0
  86. package/dist/services/versions.d.ts +21 -0
  87. package/dist/services/versions.js +238 -0
  88. package/dist/types/collection.d.ts +1 -0
  89. package/dist/utils/apply-query.d.ts +1 -1
  90. package/dist/utils/apply-query.js +30 -2
  91. package/dist/utils/delete-from-require-cache.d.ts +1 -0
  92. package/dist/utils/delete-from-require-cache.js +5 -0
  93. package/dist/utils/get-service.js +3 -1
  94. package/dist/utils/import-file-url.d.ts +5 -0
  95. package/dist/utils/import-file-url.js +6 -0
  96. package/dist/utils/job-queue.d.ts +2 -3
  97. package/dist/utils/redact-object.d.ts +1 -1
  98. package/dist/utils/redact-object.js +37 -24
  99. package/dist/utils/sanitize-query.js +3 -0
  100. package/dist/utils/validate-query.js +1 -0
  101. package/dist/worker-pool.js +8 -0
  102. package/package.json +28 -27
  103. package/dist/extensions/get-extensions.d.ts +0 -47
  104. package/dist/extensions/get-extensions.js +0 -9
  105. package/dist/extensions/normalize-extension-info.d.ts +0 -5
  106. package/dist/extensions/normalize-extension-info.js +0 -30
  107. /package/dist/extensions/{get-shared-deps-mapping.d.ts → lib/get-shared-deps-mapping.d.ts} +0 -0
  108. /package/dist/extensions/{wrap-embeds.d.ts → lib/wrap-embeds.d.ts} +0 -0
  109. /package/dist/extensions/{wrap-embeds.js → lib/wrap-embeds.js} +0 -0
@@ -0,0 +1,31 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node/http.js" />
3
+ /// <reference types="pino-http" />
4
+ import type { ApiExtensionType, HybridExtensionType } from '@directus/extensions';
5
+ import type { Router } from 'express';
6
+ /**
7
+ * Generate the JS to run in the isolate to create the extension's entrypoint to the host
8
+ *
9
+ * @param type - Extension type to generate the entrypoint for
10
+ * @param name - Unique name of the extension
11
+ * @param endpointRouter - Scoped express router to register endpoint extension in
12
+ */
13
+ export declare function generateApiExtensionsSandboxEntrypoint(type: ApiExtensionType | HybridExtensionType, name: string, endpointRouter: Router): {
14
+ code: string;
15
+ hostFunctions: ((event: import("isolated-vm").Reference<string>, cb: import("isolated-vm").Reference<(payload: unknown) => void | Promise<void>>) => void)[];
16
+ unregisterFunction: () => Promise<void>;
17
+ } | {
18
+ code: string;
19
+ hostFunctions: ((path: import("isolated-vm").Reference<string>, method: import("isolated-vm").Reference<"GET" | "POST" | "DELETE" | "PUT" | "PATCH">, cb: import("isolated-vm").Reference<(req: {
20
+ url: string;
21
+ headers: import("http").IncomingHttpHeaders;
22
+ body: string;
23
+ }) => {
24
+ status: number;
25
+ body: string;
26
+ } | Promise<{
27
+ status: number;
28
+ body: string;
29
+ }>>) => void)[];
30
+ unregisterFunction: () => void;
31
+ };
@@ -0,0 +1,80 @@
1
+ import { numberGenerator } from '@directus/utils';
2
+ import { generateHostFunctionReference } from './generate-host-function-reference.js';
3
+ import { registerActionGenerator, registerFilterGenerator, registerOperationGenerator, registerRouteGenerator, } from './register/index.js';
4
+ /**
5
+ * Generate the JS to run in the isolate to create the extension's entrypoint to the host
6
+ *
7
+ * @param type - Extension type to generate the entrypoint for
8
+ * @param name - Unique name of the extension
9
+ * @param endpointRouter - Scoped express router to register endpoint extension in
10
+ */
11
+ export function generateApiExtensionsSandboxEntrypoint(type, name, endpointRouter) {
12
+ const index = numberGenerator();
13
+ const hostFunctions = [];
14
+ const extensionExport = `const extensionExport = $${index.next().value}.deref();`;
15
+ if (type === 'hook') {
16
+ const code = `
17
+ ${extensionExport}
18
+
19
+ const filter = ${generateHostFunctionReference(index, ['event', 'handler'], { async: false })}
20
+ const action = ${generateHostFunctionReference(index, ['event', 'handler'], { async: false })}
21
+
22
+ return extensionExport({ filter, action });
23
+ `;
24
+ const { register: filter, unregisterFunctions: filterUnregisterFunctions } = registerFilterGenerator();
25
+ const { register: action, unregisterFunctions: actionUnregisterFunctions } = registerActionGenerator();
26
+ hostFunctions.push(filter);
27
+ hostFunctions.push(action);
28
+ const unregisterFunction = async () => {
29
+ await Promise.all([...filterUnregisterFunctions, ...actionUnregisterFunctions].map((fn) => fn()));
30
+ };
31
+ return { code, hostFunctions, unregisterFunction };
32
+ }
33
+ else if (type === 'endpoint') {
34
+ const code = `
35
+ ${extensionExport}
36
+
37
+ const registerRoute = ${generateHostFunctionReference(index, ['path', 'method', 'handler'], { async: false })}
38
+
39
+ const router = {
40
+ get: (path, handler) => {
41
+ registerRoute(path, 'GET', handler);
42
+ },
43
+ post: (path, handler) => {
44
+ registerRoute(path, 'POST', handler);
45
+ },
46
+ put: (path, handler) => {
47
+ registerRoute(path, 'PUT', handler);
48
+ },
49
+ patch: (path, handler) => {
50
+ registerRoute(path, 'PATCH', handler);
51
+ },
52
+ delete: (path, handler) => {
53
+ registerRoute(path, 'DELETE', handler);
54
+ }
55
+ };
56
+
57
+ return extensionExport(router);
58
+ `;
59
+ const { register, unregisterFunction } = registerRouteGenerator(name, endpointRouter);
60
+ hostFunctions.push(register);
61
+ return { code, hostFunctions, unregisterFunction };
62
+ }
63
+ else {
64
+ const code = `
65
+ ${extensionExport}
66
+
67
+ const registerOperation = ${generateHostFunctionReference(index, ['id', 'handler'], { async: false })}
68
+
69
+ const operationConfig = extensionExport();
70
+
71
+ registerOperation(operationConfig.id, operationConfig.handler);
72
+ `;
73
+ const { register, unregisterFunctions } = registerOperationGenerator();
74
+ hostFunctions.push(register);
75
+ const unregisterFunction = async () => {
76
+ await Promise.all(unregisterFunctions.map((fn) => fn()));
77
+ };
78
+ return { code, hostFunctions, unregisterFunction };
79
+ }
80
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Generate an anonymous function wrapper with the provided arguments that applies the args against a referenced function in the host
3
+ *
4
+ * @param index - Generator function that tracks the indexes used
5
+ * @param args - Named arguments of the host function
6
+ * @param options - Options to modify the output function
7
+ * @param options.async - Whether or not to generate the wrapper function as an async function
8
+ */
9
+ export declare function generateHostFunctionReference(index: Generator<number, number, number>, args: string[], options: {
10
+ async: boolean;
11
+ }): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Generate an anonymous function wrapper with the provided arguments that applies the args against a referenced function in the host
3
+ *
4
+ * @param index - Generator function that tracks the indexes used
5
+ * @param args - Named arguments of the host function
6
+ * @param options - Options to modify the output function
7
+ * @param options.async - Whether or not to generate the wrapper function as an async function
8
+ */
9
+ export function generateHostFunctionReference(index, args, options) {
10
+ const argsList = args.join(', ');
11
+ const i = index.next().value;
12
+ if (options.async) {
13
+ return `
14
+ async (${argsList}) => {
15
+ const { result, error } = await $${i}.apply(undefined, [${argsList}], { arguments: { reference: true }, result: { copy: true, promise: true } });
16
+
17
+ if (error) {
18
+ throw result;
19
+ } else {
20
+ return result;
21
+ }
22
+ };
23
+ `;
24
+ }
25
+ else {
26
+ return `(${argsList}) => $${i}.applySync(undefined, [${argsList}], { arguments: { reference: true }, result: { copy: true } });`;
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ import type { PromiseCallback } from '@directus/types';
2
+ import type { Reference } from 'isolated-vm';
3
+ export declare function registerActionGenerator(): {
4
+ register: (event: Reference<string>, cb: Reference<(payload: unknown) => void | Promise<void>>) => void;
5
+ unregisterFunctions: PromiseCallback[];
6
+ };
@@ -0,0 +1,18 @@
1
+ import emitter from '../../../../emitter.js';
2
+ import { callReference } from './call-reference.js';
3
+ export function registerActionGenerator() {
4
+ const unregisterFunctions = [];
5
+ const registerAction = (event, cb) => {
6
+ if (event.typeof !== 'string')
7
+ throw new TypeError('Action event has to be of type string');
8
+ if (cb.typeof !== 'function')
9
+ throw new TypeError('Action handler has to be of type function');
10
+ const eventCopied = event.copySync();
11
+ const handler = (payload) => callReference(cb, [payload]);
12
+ emitter.onAction(eventCopied, handler);
13
+ unregisterFunctions.push(() => {
14
+ emitter.offAction(eventCopied, handler);
15
+ });
16
+ };
17
+ return { register: registerAction, unregisterFunctions };
18
+ }
@@ -0,0 +1,5 @@
1
+ import type { Reference } from 'isolated-vm';
2
+ type Args<T> = T extends (...args: infer Args) => unknown ? Args : any[];
3
+ type Result<T> = T extends (...args: any) => infer Result ? Result : unknown;
4
+ export declare function callReference<T extends (...args: any[]) => unknown | Promise<unknown>>(fn: Reference<T>, args: Args<T>): Promise<Reference<Result<T>>>;
5
+ export {};
@@ -0,0 +1,20 @@
1
+ import env from '../../../../env.js';
2
+ import logger from '../../../../logger.js';
3
+ export async function callReference(fn, args) {
4
+ const sandboxTimeout = Number(env['EXTENSIONS_SANDBOX_TIMEOUT']);
5
+ try {
6
+ return await fn.apply(undefined, args, {
7
+ arguments: { copy: true },
8
+ result: { reference: true, promise: true },
9
+ timeout: sandboxTimeout,
10
+ });
11
+ }
12
+ catch (e) {
13
+ if (e instanceof RangeError) {
14
+ logger.error(`Extension sandbox has reached the memory limit`);
15
+ logger.error(e);
16
+ process.abort();
17
+ }
18
+ throw e;
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ import type { PromiseCallback } from '@directus/types';
2
+ import type { Reference } from 'isolated-vm';
3
+ export declare function registerFilterGenerator(): {
4
+ register: (event: Reference<string>, cb: Reference<(payload: unknown) => unknown | Promise<unknown>>) => void;
5
+ unregisterFunctions: PromiseCallback[];
6
+ };
@@ -0,0 +1,21 @@
1
+ import emitter from '../../../../emitter.js';
2
+ import { callReference } from './call-reference.js';
3
+ export function registerFilterGenerator() {
4
+ const unregisterFunctions = [];
5
+ const registerFilter = (event, cb) => {
6
+ if (event.typeof !== 'string')
7
+ throw new TypeError('Filter event has to be of type string');
8
+ if (cb.typeof !== 'function')
9
+ throw new TypeError('Filter handler has to be of type function');
10
+ const eventCopied = event.copySync();
11
+ const handler = async (payload) => {
12
+ const response = await callReference(cb, [payload]);
13
+ return response.copy();
14
+ };
15
+ emitter.onFilter(eventCopied, handler);
16
+ unregisterFunctions.push(() => {
17
+ emitter.offFilter(eventCopied, handler);
18
+ });
19
+ };
20
+ return { register: registerFilter, unregisterFunctions };
21
+ }
@@ -0,0 +1,5 @@
1
+ export * from './action.js';
2
+ export * from './call-reference.js';
3
+ export * from './filter.js';
4
+ export * from './operation.js';
5
+ export * from './route.js';
@@ -0,0 +1,5 @@
1
+ export * from './action.js';
2
+ export * from './call-reference.js';
3
+ export * from './filter.js';
4
+ export * from './operation.js';
5
+ export * from './route.js';
@@ -0,0 +1,6 @@
1
+ import type { PromiseCallback } from '@directus/types';
2
+ import type { Reference } from 'isolated-vm';
3
+ export declare function registerOperationGenerator(): {
4
+ register: (id: Reference<string>, cb: Reference<(data: Record<string, unknown>) => unknown | Promise<unknown> | void>) => void;
5
+ unregisterFunctions: PromiseCallback[];
6
+ };
@@ -0,0 +1,19 @@
1
+ import { getFlowManager } from '../../../../flows.js';
2
+ import { callReference } from './call-reference.js';
3
+ export function registerOperationGenerator() {
4
+ const flowManager = getFlowManager();
5
+ const unregisterFunctions = [];
6
+ const registerOperation = (id, cb) => {
7
+ if (id.typeof !== 'string')
8
+ throw new TypeError('Operation config id has to be of type string');
9
+ if (cb.typeof !== 'function')
10
+ throw new TypeError('Operation config handler has to be of type function');
11
+ const idCopied = id.copySync();
12
+ const handler = async (data) => callReference(cb, [data]);
13
+ flowManager.addOperation(idCopied, handler);
14
+ unregisterFunctions.push(() => {
15
+ flowManager.removeOperation(idCopied);
16
+ });
17
+ };
18
+ return { register: registerOperation, unregisterFunctions };
19
+ }
@@ -0,0 +1,17 @@
1
+ import type { Router } from 'express';
2
+ import type { Reference } from 'isolated-vm';
3
+ import type { IncomingHttpHeaders } from 'node:http';
4
+ export declare function registerRouteGenerator(endpointName: string, endpointRouter: Router): {
5
+ register: (path: Reference<string>, method: Reference<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>, cb: Reference<(req: {
6
+ url: string;
7
+ headers: IncomingHttpHeaders;
8
+ body: string;
9
+ }) => {
10
+ status: number;
11
+ body: string;
12
+ } | Promise<{
13
+ status: number;
14
+ body: string;
15
+ }>>) => void;
16
+ unregisterFunction: () => void;
17
+ };
@@ -0,0 +1,44 @@
1
+ import express from 'express';
2
+ import asyncHandler from '../../../../utils/async-handler.js';
3
+ import { callReference } from './call-reference.js';
4
+ export function registerRouteGenerator(endpointName, endpointRouter) {
5
+ const router = express.Router();
6
+ endpointRouter.use(`/${endpointName}`, router);
7
+ const registerRoute = (path, method, cb) => {
8
+ if (path.typeof !== 'string')
9
+ throw new TypeError('Route path has to be of type string');
10
+ if (method.typeof !== 'string')
11
+ throw new TypeError('Route method has to be of type string');
12
+ if (cb.typeof !== 'function')
13
+ throw new TypeError('Route handler has to be of type function');
14
+ const pathCopied = path.copySync();
15
+ const methodCopied = method.copySync();
16
+ const handler = asyncHandler(async (req, res) => {
17
+ const request = { url: req.url, headers: req.headers, body: req.body };
18
+ const response = await callReference(cb, [request]);
19
+ const responseCopied = await response.copy();
20
+ res.status(responseCopied.status).send(responseCopied.body);
21
+ });
22
+ switch (methodCopied) {
23
+ case 'GET':
24
+ router.get(pathCopied, handler);
25
+ break;
26
+ case 'POST':
27
+ router.post(pathCopied, handler);
28
+ break;
29
+ case 'PUT':
30
+ router.put(pathCopied, handler);
31
+ break;
32
+ case 'PATCH':
33
+ router.patch(pathCopied, handler);
34
+ break;
35
+ case 'DELETE':
36
+ router.delete(pathCopied, handler);
37
+ break;
38
+ }
39
+ };
40
+ const unregisterFunction = () => {
41
+ endpointRouter.stack = endpointRouter.stack.filter((layer) => router !== layer.handle);
42
+ };
43
+ return { register: registerRoute, unregisterFunction };
44
+ }
@@ -0,0 +1,3 @@
1
+ export * from './log.js';
2
+ export * from './request.js';
3
+ export * from './sleep.js';
@@ -0,0 +1,3 @@
1
+ export * from './log.js';
2
+ export * from './request.js';
3
+ export * from './sleep.js';
@@ -0,0 +1,3 @@
1
+ import type { ExtensionSandboxRequestedScopes } from '@directus/extensions';
2
+ import type { Reference } from 'isolated-vm';
3
+ export declare function logGenerator(requestedScopes: ExtensionSandboxRequestedScopes): (message: Reference<string>) => void;
@@ -0,0 +1,11 @@
1
+ import logger from '../../../../../logger.js';
2
+ export function logGenerator(requestedScopes) {
3
+ return (message) => {
4
+ if (requestedScopes.log === undefined)
5
+ throw new Error('No permission to access "log"');
6
+ if (message.typeof !== 'string')
7
+ throw new TypeError('Log message has to be of type string');
8
+ const messageCopied = message.copySync();
9
+ logger.info(messageCopied);
10
+ };
11
+ }
@@ -0,0 +1,12 @@
1
+ import type { ExtensionSandboxRequestedScopes } from '@directus/extensions';
2
+ import type { Reference } from 'isolated-vm';
3
+ export declare function requestGenerator(requestedScopes: ExtensionSandboxRequestedScopes): (url: Reference<string>, options: Reference<{
4
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
5
+ body?: Record<string, any> | string;
6
+ headers?: Record<string, string>;
7
+ }>) => Promise<{
8
+ status: number;
9
+ statusText: string;
10
+ headers: Record<string, any>;
11
+ data: string;
12
+ }>;
@@ -0,0 +1,49 @@
1
+ import encodeUrl from 'encodeurl';
2
+ import globToRegExp from 'glob-to-regexp';
3
+ import { getAxios } from '../../../../../request/index.js';
4
+ export function requestGenerator(requestedScopes) {
5
+ return async (url, options) => {
6
+ if (url.typeof !== 'string')
7
+ throw new TypeError('Request url has to be of type string');
8
+ if (options.typeof !== 'undefined' && options.typeof !== 'object') {
9
+ throw new TypeError('Request options has to be of type object');
10
+ }
11
+ const urlCopied = await url.copy();
12
+ const permissions = requestedScopes.request;
13
+ if (permissions === undefined)
14
+ throw new Error('No permission to access "request"');
15
+ const urlAllowed = permissions.urls.some((urlScope) => {
16
+ const regex = globToRegExp(urlScope);
17
+ return regex.test(urlCopied);
18
+ });
19
+ if (urlAllowed === false) {
20
+ throw new Error(`No permission to request "${urlCopied}"`);
21
+ }
22
+ const method = options.typeof !== 'undefined' ? await options.get('method', { reference: true }) : undefined;
23
+ const body = options.typeof !== 'undefined' ? await options.get('body', { reference: true }) : undefined;
24
+ const headers = options.typeof !== 'undefined' ? await options.get('headers', { reference: true }) : undefined;
25
+ if (method !== undefined && method.typeof !== 'undefined' && method.typeof !== 'string') {
26
+ throw new TypeError('Request method has to be of type string');
27
+ }
28
+ if (body !== undefined && body.typeof !== 'undefined' && body.typeof !== 'string' && body.typeof !== 'object') {
29
+ throw new TypeError('Request body has to be of type string or object');
30
+ }
31
+ if (headers !== undefined && headers.typeof !== 'undefined' && headers.typeof !== 'array') {
32
+ throw new TypeError('Request headers has to be of type array');
33
+ }
34
+ const methodCopied = await method?.copy();
35
+ const bodyCopied = await body?.copy();
36
+ const headersCopied = await headers?.copy();
37
+ if (!permissions.methods.includes(methodCopied ?? 'GET')) {
38
+ throw new Error(`No permission to use request method "${methodCopied}"`);
39
+ }
40
+ const axios = await getAxios();
41
+ const result = await axios({
42
+ url: encodeUrl(urlCopied),
43
+ method: methodCopied ?? 'GET',
44
+ data: bodyCopied ?? null,
45
+ headers: headersCopied ?? {},
46
+ });
47
+ return { status: result.status, statusText: result.statusText, headers: result.headers, data: result.data };
48
+ };
49
+ }
@@ -0,0 +1,3 @@
1
+ import type { ExtensionSandboxRequestedScopes } from '@directus/extensions';
2
+ import type { Reference } from 'isolated-vm';
3
+ export declare function sleepGenerator(requestedScopes: ExtensionSandboxRequestedScopes): (milliseconds: Reference<number>) => Promise<void>;
@@ -0,0 +1,11 @@
1
+ import { setTimeout } from 'node:timers/promises';
2
+ export function sleepGenerator(requestedScopes) {
3
+ return async (milliseconds) => {
4
+ if (requestedScopes.sleep === undefined)
5
+ throw new Error('No permission to access "sleep"');
6
+ if (milliseconds.typeof !== 'number')
7
+ throw new TypeError('Sleep milliseconds has to be of type number');
8
+ const millisecondsCopied = await milliseconds.copy();
9
+ await setTimeout(millisecondsCopied);
10
+ };
11
+ }
@@ -0,0 +1,2 @@
1
+ export * from './instantiate.js';
2
+ export * from './sdk.js';
@@ -0,0 +1,2 @@
1
+ export * from './instantiate.js';
2
+ export * from './sdk.js';
@@ -0,0 +1,11 @@
1
+ import type { ExtensionSandboxRequestedScopes } from '@directus/extensions';
2
+ import type { Isolate, Module } from 'isolated-vm';
3
+ /**
4
+ * Creates a new isolate context, generates the sandbox SDK, and returns an isolate Module with the
5
+ * SDK included in it's global scope
6
+ *
7
+ * @param isolate - Existing isolate in which to inject the SDK globally
8
+ * @param requestedScopes - Permissions as requested by the extension to generate the SDK for
9
+ * @returns Isolate module with SDK available in it's global scope
10
+ */
11
+ export declare function instantiateSandboxSdk(isolate: Isolate, requestedScopes: ExtensionSandboxRequestedScopes): Promise<Module>;
@@ -0,0 +1,28 @@
1
+ import { numberGenerator } from '@directus/utils';
2
+ import { generateHostFunctionReference } from '../generate-host-function-reference.js';
3
+ import { getSdk } from './sdk.js';
4
+ import { wrap } from './utils/wrap.js';
5
+ /**
6
+ * Creates a new isolate context, generates the sandbox SDK, and returns an isolate Module with the
7
+ * SDK included in it's global scope
8
+ *
9
+ * @param isolate - Existing isolate in which to inject the SDK globally
10
+ * @param requestedScopes - Permissions as requested by the extension to generate the SDK for
11
+ * @returns Isolate module with SDK available in it's global scope
12
+ */
13
+ export async function instantiateSandboxSdk(isolate, requestedScopes) {
14
+ const apiContext = await isolate.createContext();
15
+ await apiContext.global.set('sdk', apiContext.global.derefInto());
16
+ const index = numberGenerator();
17
+ const sdk = getSdk();
18
+ const handlerCode = sdk
19
+ .map(({ name, args, async }) => `sdk.${name} = ${generateHostFunctionReference(index, args, { async })}`)
20
+ .join('\n');
21
+ await apiContext.evalClosure(handlerCode, sdk.map(({ generator, async }) => (async ? wrap(generator(requestedScopes)) : generator(requestedScopes))), { filename: '<extensions-sdk>', arguments: { reference: true } });
22
+ const exportCode = sdk.map(({ name }) => `export const ${name} = sdk.${name};`).join('\n');
23
+ const apiModule = await isolate.compileModule(exportCode);
24
+ await apiModule.instantiate(apiContext, () => {
25
+ throw new Error();
26
+ });
27
+ return apiModule;
28
+ }
@@ -0,0 +1,20 @@
1
+ import { logGenerator, requestGenerator, sleepGenerator } from './generators/index.js';
2
+ /**
3
+ * Create a new SDK context for use in the isolate
4
+ */
5
+ export declare function getSdk(): ({
6
+ name: string;
7
+ generator: typeof logGenerator;
8
+ args: string[];
9
+ async: boolean;
10
+ } | {
11
+ name: string;
12
+ generator: typeof sleepGenerator;
13
+ args: string[];
14
+ async: boolean;
15
+ } | {
16
+ name: string;
17
+ generator: typeof requestGenerator;
18
+ args: string[];
19
+ async: boolean;
20
+ })[];
@@ -0,0 +1,11 @@
1
+ import { logGenerator, requestGenerator, sleepGenerator } from './generators/index.js';
2
+ /**
3
+ * Create a new SDK context for use in the isolate
4
+ */
5
+ export function getSdk() {
6
+ return [
7
+ { name: 'log', generator: logGenerator, args: ['message'], async: false },
8
+ { name: 'sleep', generator: sleepGenerator, args: ['milliseconds'], async: true },
9
+ { name: 'request', generator: requestGenerator, args: ['url', 'options'], async: true },
10
+ ];
11
+ }
@@ -0,0 +1 @@
1
+ export * from './wrap.js';
@@ -0,0 +1 @@
1
+ export * from './wrap.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Call the passed function in a try-catch, and return the output wrapped in a state object.
3
+ *
4
+ * This is needed as isolated-vm doesn't allow the isolate to catch errors that are thrown in the
5
+ * host. Instead, we'll wrap the output in a known shape which allows the isolated sdk context to
6
+ * re-throw the error in the correct context
7
+ */
8
+ export declare function wrap(util: (...args: any[]) => any): (...args: any[]) => Promise<{
9
+ result: any;
10
+ error: boolean;
11
+ }>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Call the passed function in a try-catch, and return the output wrapped in a state object.
3
+ *
4
+ * This is needed as isolated-vm doesn't allow the isolate to catch errors that are thrown in the
5
+ * host. Instead, we'll wrap the output in a known shape which allows the isolated sdk context to
6
+ * re-throw the error in the correct context
7
+ */
8
+ export function wrap(util) {
9
+ return async (...args) => {
10
+ try {
11
+ return { result: await util(...args), error: false };
12
+ }
13
+ catch (e) {
14
+ return { result: e, error: true };
15
+ }
16
+ };
17
+ }