@directus/api 32.1.0 → 32.2.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/ai/chat/constants/system-prompt.d.ts +1 -0
- package/dist/ai/chat/constants/system-prompt.js +51 -0
- package/dist/ai/chat/controllers/chat.post.d.ts +2 -0
- package/dist/ai/chat/controllers/chat.post.js +47 -0
- package/dist/ai/chat/lib/create-ui-stream.d.ts +15 -0
- package/dist/ai/chat/lib/create-ui-stream.js +42 -0
- package/dist/ai/chat/middleware/load-settings.d.ts +2 -0
- package/dist/ai/chat/middleware/load-settings.js +18 -0
- package/dist/ai/chat/models/chat-request.d.ts +34 -0
- package/dist/ai/chat/models/chat-request.js +26 -0
- package/dist/ai/chat/models/providers.d.ts +9 -0
- package/dist/ai/chat/models/providers.js +9 -0
- package/dist/ai/chat/router.d.ts +1 -0
- package/dist/ai/chat/router.js +5 -0
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.d.ts +9 -0
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +38 -0
- package/dist/ai/chat/utils/fix-error-tool-calls.d.ts +12 -0
- package/dist/ai/chat/utils/fix-error-tool-calls.js +30 -0
- package/dist/ai/chat/utils/parse-json-schema-7.d.ts +13 -0
- package/dist/ai/chat/utils/parse-json-schema-7.js +75 -0
- package/dist/{mcp → ai/mcp}/server.d.ts +13 -16
- package/dist/{mcp → ai/mcp}/server.js +4 -13
- package/dist/ai/mcp/types.d.ts +15 -0
- package/dist/{mcp/tools/assets.js → ai/tools/assets/index.js} +8 -5
- package/dist/{mcp/tools/collections.js → ai/tools/collections/index.js} +7 -4
- package/dist/{mcp/tools/fields.js → ai/tools/fields/index.js} +12 -9
- package/dist/{mcp/tools/files.js → ai/tools/files/index.js} +11 -5
- package/dist/{mcp/tools/flows.js → ai/tools/flows/index.js} +11 -5
- package/dist/{mcp/tools/folders.js → ai/tools/folders/index.js} +12 -5
- package/dist/ai/tools/index.d.ts +15 -0
- package/dist/ai/tools/index.js +29 -0
- package/dist/{mcp/tools/items.js → ai/tools/items/index.js} +13 -6
- package/dist/{mcp/tools/prompts/items.md → ai/tools/items/prompt.md} +19 -15
- package/dist/{mcp/tools/operations.d.ts → ai/tools/operations/index.d.ts} +46 -0
- package/dist/{mcp/tools/operations.js → ai/tools/operations/index.js} +12 -5
- package/dist/{mcp/tools/relations.js → ai/tools/relations/index.js} +7 -4
- package/dist/{mcp/tools/schema.d.ts → ai/tools/schema/index.d.ts} +1 -1
- package/dist/{mcp/tools/schema.js → ai/tools/schema/index.js} +9 -6
- package/dist/{mcp/tools/system.js → ai/tools/system/index.js} +7 -4
- package/dist/{mcp/tools/trigger-flow.js → ai/tools/trigger-flow/index.js} +8 -5
- package/dist/{mcp → ai/tools}/types.d.ts +1 -17
- package/dist/ai/tools/utils.d.ts +9 -0
- package/dist/ai/tools/utils.js +17 -0
- package/dist/app.js +5 -0
- package/dist/auth/drivers/oauth2.d.ts +2 -1
- package/dist/auth/drivers/oauth2.js +17 -22
- package/dist/auth/drivers/openid.d.ts +2 -1
- package/dist/auth/drivers/openid.js +13 -18
- package/dist/auth/drivers/saml.js +6 -3
- package/dist/controllers/assets.js +39 -2
- package/dist/controllers/mcp.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
- package/dist/database/migrations/20251103A-add-ai-settings.d.ts +3 -0
- package/dist/database/migrations/20251103A-add-ai-settings.js +14 -0
- package/dist/database/run-ast/run-ast.js +1 -1
- package/dist/extensions/lib/installation/manager.js +5 -9
- package/dist/extensions/lib/sync/status.d.ts +11 -0
- package/dist/extensions/lib/sync/status.js +34 -0
- package/dist/extensions/lib/sync/sync.d.ts +6 -0
- package/dist/extensions/lib/sync/sync.js +90 -0
- package/dist/extensions/lib/sync/tracker.d.ts +18 -0
- package/dist/extensions/lib/sync/tracker.js +71 -0
- package/dist/extensions/lib/sync/utils.d.ts +24 -0
- package/dist/extensions/lib/sync/utils.js +62 -0
- package/dist/extensions/manager.d.ts +8 -4
- package/dist/extensions/manager.js +30 -13
- package/dist/middleware/respond.js +2 -2
- package/dist/permissions/lib/fetch-policies.d.ts +1 -1
- package/dist/permissions/lib/fetch-roles-tree.d.ts +6 -3
- package/dist/permissions/lib/fetch-roles-tree.js +5 -27
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +9 -7
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +17 -9
- package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +1 -1
- package/dist/permissions/utils/fetch-raw-permissions.d.ts +1 -1
- package/dist/permissions/utils/fetch-share-info.d.ts +1 -1
- package/dist/permissions/utils/fetch-share-info.js +1 -1
- package/dist/permissions/utils/filter-policies-by-ip.js +1 -1
- package/dist/permissions/utils/get-permissions-for-share.js +8 -8
- package/dist/permissions/utils/with-cache.d.ts +8 -6
- package/dist/permissions/utils/with-cache.js +12 -10
- package/dist/request/is-denied-ip.js +2 -2
- package/dist/services/assets/name-deduper.d.ts +7 -0
- package/dist/services/assets/name-deduper.js +23 -0
- package/dist/services/assets.d.ts +15 -2
- package/dist/services/assets.js +98 -5
- package/dist/services/authentication.js +4 -4
- package/dist/services/comments.js +2 -2
- package/dist/services/extensions.js +4 -0
- package/dist/services/folders.d.ts +27 -2
- package/dist/services/folders.js +75 -0
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/import-export.d.ts +1 -1
- package/dist/services/import-export.js +4 -5
- package/dist/services/notifications.js +2 -2
- package/dist/services/payload.js +20 -0
- package/dist/services/roles.js +2 -2
- package/dist/services/tus/server.js +3 -3
- package/dist/telemetry/utils/get-settings.d.ts +15 -0
- package/dist/telemetry/utils/get-settings.js +25 -9
- package/dist/test-utils/README.md +95 -24
- package/dist/test-utils/cache.d.ts +2 -2
- package/dist/test-utils/cache.js +2 -2
- package/dist/test-utils/{fields-service.d.ts → services/fields-service.d.ts} +1 -1
- package/dist/test-utils/{fields-service.js → services/fields-service.js} +3 -2
- package/dist/test-utils/services/files-service.d.ts +28 -0
- package/dist/test-utils/services/files-service.js +34 -0
- package/dist/test-utils/services/folders-service.d.ts +28 -0
- package/dist/test-utils/services/folders-service.js +33 -0
- package/dist/utils/encrypt.d.ts +2 -0
- package/dist/utils/encrypt.js +64 -0
- package/dist/utils/get-accountability-for-role.js +2 -2
- package/dist/utils/get-accountability-for-token.js +4 -4
- package/dist/utils/get-cache-key.js +2 -2
- package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
- package/dist/{auth/utils → utils}/is-login-redirect-allowed.js +8 -16
- package/dist/utils/require-text.d.ts +1 -0
- package/dist/utils/require-text.js +4 -0
- package/dist/utils/require-yaml.js +2 -2
- package/package.json +31 -25
- package/dist/auth/utils/generate-callback-url.d.ts +0 -8
- package/dist/auth/utils/generate-callback-url.js +0 -11
- package/dist/auth/utils/is-login-redirect-allowed.d.ts +0 -8
- package/dist/extensions/lib/sync-extensions.d.ts +0 -3
- package/dist/extensions/lib/sync-extensions.js +0 -70
- package/dist/extensions/lib/sync-status.d.ts +0 -10
- package/dist/extensions/lib/sync-status.js +0 -27
- package/dist/mcp/tools/index.d.ts +0 -15
- package/dist/mcp/tools/index.js +0 -29
- package/dist/mcp/tools/prompts/index.d.ts +0 -16
- package/dist/mcp/tools/prompts/index.js +0 -19
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +0 -5
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +0 -7
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +0 -5
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +0 -10
- package/dist/permissions/modules/fetch-global-access/types.d.ts +0 -4
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +0 -4
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +0 -27
- package/dist/utils/get-date-formatted.d.ts +0 -1
- package/dist/utils/get-date-formatted.js +0 -10
- package/dist/utils/ip-in-networks.d.ts +0 -6
- package/dist/utils/ip-in-networks.js +0 -13
- /package/dist/{mcp → ai/mcp}/index.d.ts +0 -0
- /package/dist/{mcp → ai/mcp}/index.js +0 -0
- /package/dist/{mcp → ai/mcp}/transport.d.ts +0 -0
- /package/dist/{mcp → ai/mcp}/transport.js +0 -0
- /package/dist/{mcp → ai/mcp}/types.js +0 -0
- /package/dist/{mcp/tools/assets.d.ts → ai/tools/assets/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/assets.md → ai/tools/assets/prompt.md} +0 -0
- /package/dist/{mcp/tools/collections.d.ts → ai/tools/collections/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/collections.md → ai/tools/collections/prompt.md} +0 -0
- /package/dist/{mcp/define.d.ts → ai/tools/define-tool.d.ts} +0 -0
- /package/dist/{mcp/define.js → ai/tools/define-tool.js} +0 -0
- /package/dist/{mcp/tools/fields.d.ts → ai/tools/fields/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/fields.md → ai/tools/fields/prompt.md} +0 -0
- /package/dist/{mcp/tools/files.d.ts → ai/tools/files/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/files.md → ai/tools/files/prompt.md} +0 -0
- /package/dist/{mcp/tools/flows.d.ts → ai/tools/flows/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/flows.md → ai/tools/flows/prompt.md} +0 -0
- /package/dist/{mcp/tools/folders.d.ts → ai/tools/folders/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/folders.md → ai/tools/folders/prompt.md} +0 -0
- /package/dist/{mcp/tools/items.d.ts → ai/tools/items/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/operations.md → ai/tools/operations/prompt.md} +0 -0
- /package/dist/{mcp/tools/relations.d.ts → ai/tools/relations/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/relations.md → ai/tools/relations/prompt.md} +0 -0
- /package/dist/{mcp/tools/prompts/schema.md → ai/tools/schema/prompt.md} +0 -0
- /package/dist/{mcp → ai/tools}/schema.d.ts +0 -0
- /package/dist/{mcp → ai/tools}/schema.js +0 -0
- /package/dist/{mcp/tools/system.d.ts → ai/tools/system/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/system-prompt-description.md → ai/tools/system/prompt-description.md} +0 -0
- /package/dist/{mcp/tools/prompts/system-prompt.md → ai/tools/system/prompt.md} +0 -0
- /package/dist/{mcp/tools/trigger-flow.d.ts → ai/tools/trigger-flow/index.d.ts} +0 -0
- /package/dist/{mcp/tools/prompts/trigger-flow.md → ai/tools/trigger-flow/prompt.md} +0 -0
- /package/dist/{permissions/modules/fetch-global-access → ai/tools}/types.js +0 -0
- /package/dist/test-utils/{items-service.d.ts → services/items-service.d.ts} +0 -0
- /package/dist/test-utils/{items-service.js → services/items-service.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Accountability,
|
|
1
|
+
import type { Accountability, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import type { ZodType } from 'zod';
|
|
4
4
|
export type ToolResultBase = {
|
|
@@ -18,7 +18,6 @@ export type ToolResult = TextToolResult | AssetToolResult;
|
|
|
18
18
|
export type ToolHandler<T> = {
|
|
19
19
|
(options: {
|
|
20
20
|
args: T;
|
|
21
|
-
sanitizedQuery: Query;
|
|
22
21
|
schema: SchemaOverview;
|
|
23
22
|
accountability: Accountability | undefined;
|
|
24
23
|
}): Promise<ToolResult | undefined>;
|
|
@@ -39,18 +38,3 @@ export interface ToolConfig<T> {
|
|
|
39
38
|
annotations?: ToolAnnotations;
|
|
40
39
|
handler: ToolHandler<T>;
|
|
41
40
|
}
|
|
42
|
-
export interface Prompt {
|
|
43
|
-
name: string;
|
|
44
|
-
system_prompt?: string | null;
|
|
45
|
-
description?: string;
|
|
46
|
-
messages: {
|
|
47
|
-
role: 'user' | 'assistant';
|
|
48
|
-
text: string;
|
|
49
|
-
}[];
|
|
50
|
-
}
|
|
51
|
-
export interface MCPOptions {
|
|
52
|
-
promptsCollection?: string;
|
|
53
|
-
allowDeletes?: boolean;
|
|
54
|
-
systemPromptEnabled?: boolean;
|
|
55
|
-
systemPrompt?: string | null;
|
|
56
|
-
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Accountability, SchemaOverview } from '@directus/types';
|
|
2
|
+
/**
|
|
3
|
+
* Build a sanitized query object from a tool's args payload.
|
|
4
|
+
* - Ensures fields defaults to '*' when not provided
|
|
5
|
+
* - Returns an empty object when no args.query is present
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildSanitizedQueryFromArgs<T extends {
|
|
8
|
+
query?: Record<string, any> | undefined;
|
|
9
|
+
}>(args: T, schema: SchemaOverview, accountability?: Accountability | null): Promise<Record<string, any>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sanitizeQuery } from '../../utils/sanitize-query.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a sanitized query object from a tool's args payload.
|
|
4
|
+
* - Ensures fields defaults to '*' when not provided
|
|
5
|
+
* - Returns an empty object when no args.query is present
|
|
6
|
+
*/
|
|
7
|
+
export async function buildSanitizedQueryFromArgs(args, schema, accountability) {
|
|
8
|
+
let sanitizedQuery = {};
|
|
9
|
+
if (args?.query) {
|
|
10
|
+
const q = args.query;
|
|
11
|
+
sanitizedQuery = await sanitizeQuery({
|
|
12
|
+
fields: q['fields'] ?? '*',
|
|
13
|
+
...q,
|
|
14
|
+
}, schema, accountability ?? undefined);
|
|
15
|
+
}
|
|
16
|
+
return sanitizedQuery;
|
|
17
|
+
}
|
package/dist/app.js
CHANGED
|
@@ -68,6 +68,7 @@ import projectSchedule from './schedules/project.js';
|
|
|
68
68
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
69
69
|
import { Url } from './utils/url.js';
|
|
70
70
|
import { validateStorage } from './utils/validate-storage.js';
|
|
71
|
+
import { aiChatRouter } from './ai/chat/router.js';
|
|
71
72
|
const require = createRequire(import.meta.url);
|
|
72
73
|
export default async function createApp() {
|
|
73
74
|
const env = useEnv();
|
|
@@ -84,6 +85,9 @@ export default async function createApp() {
|
|
|
84
85
|
if (!env['SECRET']) {
|
|
85
86
|
logger.warn(`"SECRET" env variable is missing. Using a random value instead. Tokens will not persist between restarts. This is not appropriate for production usage.`);
|
|
86
87
|
}
|
|
88
|
+
if (typeof env['SECRET'] === 'string' && Buffer.byteLength(env['SECRET']) < 32) {
|
|
89
|
+
logger.warn('"SECRET" env variable is shorter than 32 bytes which is insecure. This is not appropriate for production usage.');
|
|
90
|
+
}
|
|
87
91
|
if (!new Url(env['PUBLIC_URL']).isAbsolute()) {
|
|
88
92
|
logger.warn('"PUBLIC_URL" should be a full URL');
|
|
89
93
|
}
|
|
@@ -231,6 +235,7 @@ export default async function createApp() {
|
|
|
231
235
|
if (toBoolean(env['MCP_ENABLED']) === true) {
|
|
232
236
|
app.use('/mcp', mcpRouter);
|
|
233
237
|
}
|
|
238
|
+
app.use('/ai/chat', aiChatRouter);
|
|
234
239
|
if (env['METRICS_ENABLED'] === true) {
|
|
235
240
|
app.use('/metrics', metricsRouter);
|
|
236
241
|
}
|
|
@@ -5,11 +5,12 @@ import type { RoleMap } from '../../types/rolemap.js';
|
|
|
5
5
|
import { LocalAuthDriver } from './local.js';
|
|
6
6
|
export declare class OAuth2AuthDriver extends LocalAuthDriver {
|
|
7
7
|
client: Client;
|
|
8
|
+
redirectUrl: string;
|
|
8
9
|
config: Record<string, any>;
|
|
9
10
|
roleMap: RoleMap;
|
|
10
11
|
constructor(options: AuthDriverOptions, config: Record<string, any>);
|
|
11
12
|
generateCodeVerifier(): string;
|
|
12
|
-
generateAuthUrl(codeVerifier: string, prompt?: boolean
|
|
13
|
+
generateAuthUrl(codeVerifier: string, prompt?: boolean): string;
|
|
13
14
|
private fetchUserId;
|
|
14
15
|
getUserID(payload: Record<string, any>): Promise<string>;
|
|
15
16
|
login(user: User): Promise<void>;
|
|
@@ -16,37 +16,40 @@ import { AuthenticationService } from '../../services/authentication.js';
|
|
|
16
16
|
import asyncHandler from '../../utils/async-handler.js';
|
|
17
17
|
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
18
18
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
19
|
-
import { getSchema } from '../../utils/get-schema.js';
|
|
20
19
|
import { getSecret } from '../../utils/get-secret.js';
|
|
20
|
+
import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
|
|
21
21
|
import { verifyJWT } from '../../utils/jwt.js';
|
|
22
22
|
import { Url } from '../../utils/url.js';
|
|
23
|
-
import { generateCallbackUrl } from '../utils/generate-callback-url.js';
|
|
24
|
-
import { isLoginRedirectAllowed } from '../utils/is-login-redirect-allowed.js';
|
|
25
23
|
import { LocalAuthDriver } from './local.js';
|
|
24
|
+
import { getSchema } from '../../utils/get-schema.js';
|
|
26
25
|
export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
27
26
|
client;
|
|
27
|
+
redirectUrl;
|
|
28
28
|
config;
|
|
29
29
|
roleMap;
|
|
30
30
|
constructor(options, config) {
|
|
31
31
|
super(options, config);
|
|
32
|
+
const env = useEnv();
|
|
32
33
|
const logger = useLogger();
|
|
33
34
|
const { authorizeUrl, accessUrl, profileUrl, clientId, clientSecret, ...additionalConfig } = config;
|
|
34
35
|
if (!authorizeUrl || !accessUrl || !profileUrl || !clientId || !clientSecret || !additionalConfig['provider']) {
|
|
35
36
|
logger.error('Invalid provider config');
|
|
36
37
|
throw new InvalidProviderConfigError({ provider: additionalConfig['provider'] });
|
|
37
38
|
}
|
|
39
|
+
const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', additionalConfig['provider'], 'callback');
|
|
40
|
+
this.redirectUrl = redirectUrl.toString();
|
|
38
41
|
this.config = additionalConfig;
|
|
39
42
|
this.roleMap = {};
|
|
40
43
|
const roleMapping = this.config['roleMapping'];
|
|
44
|
+
if (roleMapping) {
|
|
45
|
+
this.roleMap = roleMapping;
|
|
46
|
+
}
|
|
41
47
|
// role mapping will fail on login if AUTH_<provider>_ROLE_MAPPING is an array instead of an object.
|
|
42
48
|
// This happens if the 'json:' prefix is missing from the variable declaration. To save the user from exhaustive debugging, we'll try to fail early here.
|
|
43
49
|
if (roleMapping instanceof Array) {
|
|
44
50
|
logger.error("[OAuth2] Expected a JSON-Object as role mapping, got an Array instead. Make sure you declare the variable with 'json:' prefix.");
|
|
45
51
|
throw new InvalidProviderError();
|
|
46
52
|
}
|
|
47
|
-
if (roleMapping) {
|
|
48
|
-
this.roleMap = roleMapping;
|
|
49
|
-
}
|
|
50
53
|
const issuer = new Issuer({
|
|
51
54
|
authorization_endpoint: authorizeUrl,
|
|
52
55
|
token_endpoint: accessUrl,
|
|
@@ -64,6 +67,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
64
67
|
this.client = new issuer.Client({
|
|
65
68
|
client_id: clientId,
|
|
66
69
|
client_secret: clientSecret,
|
|
70
|
+
redirect_uris: [this.redirectUrl],
|
|
67
71
|
response_types: ['code'],
|
|
68
72
|
...clientOptionsOverrides,
|
|
69
73
|
});
|
|
@@ -71,7 +75,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
71
75
|
generateCodeVerifier() {
|
|
72
76
|
return generators.codeVerifier();
|
|
73
77
|
}
|
|
74
|
-
generateAuthUrl(codeVerifier, prompt = false
|
|
78
|
+
generateAuthUrl(codeVerifier, prompt = false) {
|
|
75
79
|
const { plainCodeChallenge } = this.config;
|
|
76
80
|
try {
|
|
77
81
|
const codeChallenge = plainCodeChallenge ? codeVerifier : generators.codeChallenge(codeVerifier);
|
|
@@ -85,7 +89,6 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
85
89
|
code_challenge_method: plainCodeChallenge ? 'plain' : 'S256',
|
|
86
90
|
// Some providers require state even with PKCE
|
|
87
91
|
state: codeChallenge,
|
|
88
|
-
redirect_uri: callbackUrl,
|
|
89
92
|
});
|
|
90
93
|
}
|
|
91
94
|
catch (e) {
|
|
@@ -113,7 +116,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
113
116
|
const codeChallenge = plainCodeChallenge
|
|
114
117
|
? payload['codeVerifier']
|
|
115
118
|
: generators.codeChallenge(payload['codeVerifier']);
|
|
116
|
-
tokenSet = await this.client.oauthCallback(
|
|
119
|
+
tokenSet = await this.client.oauthCallback(this.redirectUrl, { code: payload['code'], state: payload['state'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge });
|
|
117
120
|
userInfo = await this.client.userinfo(tokenSet.access_token);
|
|
118
121
|
}
|
|
119
122
|
catch (e) {
|
|
@@ -272,19 +275,12 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
272
275
|
const provider = getAuthProvider(providerName);
|
|
273
276
|
const codeVerifier = provider.generateCodeVerifier();
|
|
274
277
|
const prompt = !!req.query['prompt'];
|
|
275
|
-
const otp = req.query['otp'];
|
|
276
278
|
const redirect = req.query['redirect'];
|
|
277
|
-
|
|
279
|
+
const otp = req.query['otp'];
|
|
280
|
+
if (isLoginRedirectAllowed(redirect, providerName) === false) {
|
|
278
281
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
279
282
|
}
|
|
280
|
-
const
|
|
281
|
-
const token = jwt.sign({
|
|
282
|
-
verifier: codeVerifier,
|
|
283
|
-
redirect,
|
|
284
|
-
prompt,
|
|
285
|
-
otp,
|
|
286
|
-
callbackUrl,
|
|
287
|
-
}, getSecret(), {
|
|
283
|
+
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
|
|
288
284
|
expiresIn: '5m',
|
|
289
285
|
issuer: 'directus',
|
|
290
286
|
});
|
|
@@ -292,7 +288,7 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
292
288
|
httpOnly: true,
|
|
293
289
|
sameSite: 'lax',
|
|
294
290
|
});
|
|
295
|
-
return res.redirect(provider.generateAuthUrl(codeVerifier, prompt
|
|
291
|
+
return res.redirect(provider.generateAuthUrl(codeVerifier, prompt));
|
|
296
292
|
}, respond);
|
|
297
293
|
router.post('/callback', express.urlencoded({ extended: false }), (req, res) => {
|
|
298
294
|
res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
|
|
@@ -307,7 +303,7 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
307
303
|
logger.warn(e, `[OAuth2] Couldn't verify OAuth2 cookie`);
|
|
308
304
|
throw new InvalidCredentialsError();
|
|
309
305
|
}
|
|
310
|
-
const { verifier, prompt, otp
|
|
306
|
+
const { verifier, prompt, otp } = tokenData;
|
|
311
307
|
let { redirect } = tokenData;
|
|
312
308
|
const accountability = createDefaultAccountability({
|
|
313
309
|
ip: getIPFromReq(req),
|
|
@@ -330,7 +326,6 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
330
326
|
code: req.query['code'],
|
|
331
327
|
codeVerifier: verifier,
|
|
332
328
|
state: req.query['state'],
|
|
333
|
-
callbackUrl,
|
|
334
329
|
}, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
|
|
335
330
|
}
|
|
336
331
|
catch (error) {
|
|
@@ -5,12 +5,13 @@ import type { RoleMap } from '../../types/rolemap.js';
|
|
|
5
5
|
import { LocalAuthDriver } from './local.js';
|
|
6
6
|
export declare class OpenIDAuthDriver extends LocalAuthDriver {
|
|
7
7
|
client: null | Client;
|
|
8
|
+
redirectUrl: string;
|
|
8
9
|
config: Record<string, any>;
|
|
9
10
|
roleMap: RoleMap;
|
|
10
11
|
constructor(options: AuthDriverOptions, config: Record<string, any>);
|
|
11
12
|
private getClient;
|
|
12
13
|
generateCodeVerifier(): string;
|
|
13
|
-
generateAuthUrl(codeVerifier: string, prompt?: boolean
|
|
14
|
+
generateAuthUrl(codeVerifier: string, prompt?: boolean): Promise<string>;
|
|
14
15
|
private fetchUserId;
|
|
15
16
|
getUserID(payload: Record<string, any>): Promise<string>;
|
|
16
17
|
login(user: User): Promise<void>;
|
|
@@ -16,19 +16,20 @@ import { AuthenticationService } from '../../services/authentication.js';
|
|
|
16
16
|
import asyncHandler from '../../utils/async-handler.js';
|
|
17
17
|
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
18
18
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
19
|
-
import { getSchema } from '../../utils/get-schema.js';
|
|
20
19
|
import { getSecret } from '../../utils/get-secret.js';
|
|
20
|
+
import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
|
|
21
21
|
import { verifyJWT } from '../../utils/jwt.js';
|
|
22
22
|
import { Url } from '../../utils/url.js';
|
|
23
|
-
import { generateCallbackUrl } from '../utils/generate-callback-url.js';
|
|
24
|
-
import { isLoginRedirectAllowed } from '../utils/is-login-redirect-allowed.js';
|
|
25
23
|
import { LocalAuthDriver } from './local.js';
|
|
24
|
+
import { getSchema } from '../../utils/get-schema.js';
|
|
26
25
|
export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
27
26
|
client;
|
|
27
|
+
redirectUrl;
|
|
28
28
|
config;
|
|
29
29
|
roleMap;
|
|
30
30
|
constructor(options, config) {
|
|
31
31
|
super(options, config);
|
|
32
|
+
const env = useEnv();
|
|
32
33
|
const logger = useLogger();
|
|
33
34
|
const { issuerUrl, clientId, clientSecret, clientPrivateKeys, clientTokenEndpointAuthMethod, provider, issuerDiscoveryMustSucceed, } = config;
|
|
34
35
|
const isPrivateKeyJwtAuthMethod = clientTokenEndpointAuthMethod === 'private_key_jwt';
|
|
@@ -36,6 +37,8 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
36
37
|
logger.error('Invalid provider config');
|
|
37
38
|
throw new InvalidProviderConfigError({ provider });
|
|
38
39
|
}
|
|
40
|
+
const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', provider, 'callback');
|
|
41
|
+
this.redirectUrl = redirectUrl.toString();
|
|
39
42
|
this.config = config;
|
|
40
43
|
this.roleMap = {};
|
|
41
44
|
const roleMapping = this.config['roleMapping'];
|
|
@@ -95,6 +98,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
95
98
|
const client = new issuer.Client({
|
|
96
99
|
client_id: clientId,
|
|
97
100
|
...(!isPrivateKeyJwtAuthMethod && { client_secret: clientSecret }),
|
|
101
|
+
redirect_uris: [this.redirectUrl],
|
|
98
102
|
response_types: ['code'],
|
|
99
103
|
...clientOptionsOverrides,
|
|
100
104
|
}, isPrivateKeyJwtAuthMethod ? { keys: clientPrivateKeys } : undefined);
|
|
@@ -112,7 +116,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
112
116
|
generateCodeVerifier() {
|
|
113
117
|
return generators.codeVerifier();
|
|
114
118
|
}
|
|
115
|
-
async generateAuthUrl(codeVerifier, prompt = false
|
|
119
|
+
async generateAuthUrl(codeVerifier, prompt = false) {
|
|
116
120
|
const { plainCodeChallenge } = this.config;
|
|
117
121
|
try {
|
|
118
122
|
const client = await this.getClient();
|
|
@@ -128,7 +132,6 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
128
132
|
// Some providers require state even with PKCE
|
|
129
133
|
state: codeChallenge,
|
|
130
134
|
nonce: codeChallenge,
|
|
131
|
-
redirect_uri: callbackUrl,
|
|
132
135
|
});
|
|
133
136
|
}
|
|
134
137
|
catch (e) {
|
|
@@ -157,7 +160,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
157
160
|
const codeChallenge = plainCodeChallenge
|
|
158
161
|
? payload['codeVerifier']
|
|
159
162
|
: generators.codeChallenge(payload['codeVerifier']);
|
|
160
|
-
tokenSet = await client.callback(
|
|
163
|
+
tokenSet = await client.callback(this.redirectUrl, { code: payload['code'], state: payload['state'], iss: payload['iss'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge, nonce: codeChallenge });
|
|
161
164
|
userInfo = tokenSet.claims();
|
|
162
165
|
if (client.issuer.metadata['userinfo_endpoint']) {
|
|
163
166
|
userInfo = {
|
|
@@ -326,17 +329,10 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
326
329
|
const prompt = !!req.query['prompt'];
|
|
327
330
|
const redirect = req.query['redirect'];
|
|
328
331
|
const otp = req.query['otp'];
|
|
329
|
-
if (
|
|
332
|
+
if (isLoginRedirectAllowed(redirect, providerName) === false) {
|
|
330
333
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
331
334
|
}
|
|
332
|
-
const
|
|
333
|
-
const token = jwt.sign({
|
|
334
|
-
verifier: codeVerifier,
|
|
335
|
-
redirect,
|
|
336
|
-
prompt,
|
|
337
|
-
otp,
|
|
338
|
-
callbackUrl,
|
|
339
|
-
}, getSecret(), {
|
|
335
|
+
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
|
|
340
336
|
expiresIn: (env[`AUTH_${providerName.toUpperCase()}_LOGIN_TIMEOUT`] ?? '5m'),
|
|
341
337
|
issuer: 'directus',
|
|
342
338
|
});
|
|
@@ -345,7 +341,7 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
345
341
|
sameSite: 'lax',
|
|
346
342
|
});
|
|
347
343
|
try {
|
|
348
|
-
return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt
|
|
344
|
+
return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt));
|
|
349
345
|
}
|
|
350
346
|
catch {
|
|
351
347
|
return res.redirect(new Url(env['PUBLIC_URL'])
|
|
@@ -369,7 +365,7 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
369
365
|
const url = new Url(env['PUBLIC_URL']).addPath('admin', 'login');
|
|
370
366
|
return res.redirect(`${url.toString()}?reason=${ErrorCode.InvalidCredentials}`);
|
|
371
367
|
}
|
|
372
|
-
const { verifier, prompt, otp
|
|
368
|
+
const { verifier, prompt, otp } = tokenData;
|
|
373
369
|
let { redirect } = tokenData;
|
|
374
370
|
const accountability = createDefaultAccountability({ ip: getIPFromReq(req) });
|
|
375
371
|
const userAgent = req.get('user-agent')?.substring(0, 1024);
|
|
@@ -391,7 +387,6 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
391
387
|
codeVerifier: verifier,
|
|
392
388
|
state: req.query['state'],
|
|
393
389
|
iss: req.query['iss'],
|
|
394
|
-
callbackUrl,
|
|
395
390
|
}, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
|
|
396
391
|
}
|
|
397
392
|
catch (error) {
|
|
@@ -12,9 +12,9 @@ import { respond } from '../../middleware/respond.js';
|
|
|
12
12
|
import { AuthenticationService } from '../../services/authentication.js';
|
|
13
13
|
import asyncHandler from '../../utils/async-handler.js';
|
|
14
14
|
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
15
|
-
import { LocalAuthDriver } from './local.js';
|
|
16
15
|
import { getSchema } from '../../utils/get-schema.js';
|
|
17
|
-
import { isLoginRedirectAllowed } from '
|
|
16
|
+
import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
|
|
17
|
+
import { LocalAuthDriver } from './local.js';
|
|
18
18
|
// Register the samlify schema validator
|
|
19
19
|
samlify.setSchemaValidator(validator);
|
|
20
20
|
export class SAMLAuthDriver extends LocalAuthDriver {
|
|
@@ -95,7 +95,7 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
95
95
|
const parsedUrl = new URL(url);
|
|
96
96
|
if (req.query['redirect']) {
|
|
97
97
|
const redirect = req.query['redirect'];
|
|
98
|
-
if (
|
|
98
|
+
if (isLoginRedirectAllowed(redirect, providerName) === false) {
|
|
99
99
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
100
100
|
}
|
|
101
101
|
parsedUrl.searchParams.append('RelayState', redirect);
|
|
@@ -117,6 +117,9 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
117
117
|
const logger = useLogger();
|
|
118
118
|
const relayState = req.body?.RelayState;
|
|
119
119
|
const authMode = (env[`AUTH_${providerName.toUpperCase()}_MODE`] ?? 'session');
|
|
120
|
+
if (relayState && isLoginRedirectAllowed(relayState, providerName) === false) {
|
|
121
|
+
throw new InvalidPayloadError({ reason: `URL "${relayState}" can't be used to redirect after login` });
|
|
122
|
+
}
|
|
120
123
|
try {
|
|
121
124
|
const { sp, idp } = getAuthProvider(providerName);
|
|
122
125
|
const { extract } = await sp.parseLoginResponse(idp, 'post', req);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import { InvalidQueryError, RangeNotSatisfiableError } from '@directus/errors';
|
|
2
|
+
import { InvalidPayloadError, InvalidQueryError, RangeNotSatisfiableError } from '@directus/errors';
|
|
3
3
|
import { TransformationMethods } from '@directus/types';
|
|
4
|
-
import { parseJSON } from '@directus/utils';
|
|
4
|
+
import { getDateTimeFormatted, parseJSON } from '@directus/utils';
|
|
5
5
|
import contentDisposition from 'content-disposition';
|
|
6
6
|
import { Router } from 'express';
|
|
7
7
|
import { merge, pick } from 'lodash-es';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
import { fromZodError } from 'zod-validation-error';
|
|
8
10
|
import { ASSET_TRANSFORM_QUERY_KEYS, SYSTEM_ASSET_ALLOW_LIST } from '../constants.js';
|
|
9
11
|
import getDatabase from '../database/index.js';
|
|
10
12
|
import { useLogger } from '../logger/index.js';
|
|
@@ -15,9 +17,44 @@ import asyncHandler from '../utils/async-handler.js';
|
|
|
15
17
|
import { getCacheControlHeader } from '../utils/get-cache-headers.js';
|
|
16
18
|
import { getConfigFromEnv } from '../utils/get-config-from-env.js';
|
|
17
19
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
20
|
+
import { isValidUuid } from '../utils/is-valid-uuid.js';
|
|
18
21
|
const router = Router();
|
|
19
22
|
const env = useEnv();
|
|
20
23
|
router.use(useCollection('directus_files'));
|
|
24
|
+
router.post('/folder/:pk', asyncHandler(async (req, res) => {
|
|
25
|
+
const service = new AssetsService({
|
|
26
|
+
accountability: req.accountability,
|
|
27
|
+
schema: req.schema,
|
|
28
|
+
});
|
|
29
|
+
const { archive, complete, metadata } = await service.zipFolder(req.params['pk']);
|
|
30
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
31
|
+
res.setHeader('Content-Disposition', `attachment; filename="folder-${metadata['name'] ? metadata['name'] : 'unknown'}-${getDateTimeFormatted()}.zip"`);
|
|
32
|
+
archive.pipe(res);
|
|
33
|
+
await complete();
|
|
34
|
+
}));
|
|
35
|
+
router.post('/files/', asyncHandler(async (req, res) => {
|
|
36
|
+
const service = new AssetsService({
|
|
37
|
+
accountability: req.accountability,
|
|
38
|
+
schema: req.schema,
|
|
39
|
+
});
|
|
40
|
+
const { error, data } = z
|
|
41
|
+
.object({
|
|
42
|
+
ids: z
|
|
43
|
+
.array(z.string().refine((v) => isValidUuid(v), {
|
|
44
|
+
error: '"id" must be a uuid',
|
|
45
|
+
}))
|
|
46
|
+
.min(1),
|
|
47
|
+
})
|
|
48
|
+
.safeParse(req.body);
|
|
49
|
+
if (error) {
|
|
50
|
+
throw new InvalidPayloadError({ reason: fromZodError(error).message });
|
|
51
|
+
}
|
|
52
|
+
const { archive, complete } = await service.zipFiles(data.ids);
|
|
53
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
54
|
+
res.setHeader('Content-Disposition', `attachment; filename="files-${getDateTimeFormatted()}.zip"`);
|
|
55
|
+
archive.pipe(res);
|
|
56
|
+
await complete();
|
|
57
|
+
}));
|
|
21
58
|
router.get('/:pk/:filename?',
|
|
22
59
|
// Validate query params
|
|
23
60
|
asyncHandler(async (req, res, next) => {
|
package/dist/controllers/mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ForbiddenError } from '@directus/errors';
|
|
2
2
|
import { Router } from 'express';
|
|
3
|
-
import { DirectusMCP } from '../mcp/index.js';
|
|
3
|
+
import { DirectusMCP } from '../ai/mcp/index.js';
|
|
4
4
|
import { SettingsService } from '../services/settings.js';
|
|
5
5
|
import asyncHandler from '../utils/async-handler.js';
|
|
6
6
|
const router = Router();
|
|
@@ -6,8 +6,8 @@ import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
|
|
|
6
6
|
import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
|
|
7
7
|
import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
|
|
8
8
|
import { getSchema } from '../../utils/get-schema.js';
|
|
9
|
-
import { getSchemaInspector } from '../index.js';
|
|
10
9
|
import { mergePermissions } from '../../permissions/utils/merge-permissions.js';
|
|
10
|
+
import { getSchemaInspector } from '../index.js';
|
|
11
11
|
async function fetchRoleAccess(roles, context) {
|
|
12
12
|
const roleAccess = {
|
|
13
13
|
admin_access: false,
|
|
@@ -184,7 +184,7 @@ export async function down(knex) {
|
|
|
184
184
|
// role permissions to be inserted once all processing is completed
|
|
185
185
|
const rolePermissions = [];
|
|
186
186
|
for (const role of roles) {
|
|
187
|
-
const roleTree = await fetchRolesTree(role.id, knex);
|
|
187
|
+
const roleTree = await fetchRolesTree(role.id, { knex });
|
|
188
188
|
let roleAccess = null;
|
|
189
189
|
if (role.id !== null) {
|
|
190
190
|
roleAccess = await fetchRoleAccess(roleTree, context);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
3
|
+
table.text('ai_openai_api_key');
|
|
4
|
+
table.text('ai_anthropic_api_key');
|
|
5
|
+
table.text('ai_system_prompt');
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export async function down(knex) {
|
|
9
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
10
|
+
table.dropColumn('ai_openai_api_key');
|
|
11
|
+
table.dropColumn('ai_anthropic_api_key');
|
|
12
|
+
table.dropColumn('ai_system_prompt');
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -48,7 +48,7 @@ export async function runAst(originalAST, schema, accountability, options) {
|
|
|
48
48
|
if (!rawItems)
|
|
49
49
|
return null;
|
|
50
50
|
// Run the items through the special transforms
|
|
51
|
-
const payloadService = new PayloadService(collection, { knex, schema });
|
|
51
|
+
const payloadService = new PayloadService(collection, { accountability, knex, schema });
|
|
52
52
|
let items = await payloadService.processValues('read', rawItems, query.alias ?? {}, query.aggregate ?? {});
|
|
53
53
|
if (!items || (Array.isArray(items) && items.length === 0))
|
|
54
54
|
return items;
|
|
@@ -63,11 +63,9 @@ export class InstallationManager {
|
|
|
63
63
|
}
|
|
64
64
|
await queue.onIdle();
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await move(join(tempDir, extractedPath), dest, { overwrite: true });
|
|
70
|
-
}
|
|
66
|
+
// move to regular local extensions folder
|
|
67
|
+
const dest = join(this.extensionPath, '.registry', versionId);
|
|
68
|
+
await move(join(tempDir, extractedPath), dest, { overwrite: true });
|
|
71
69
|
}
|
|
72
70
|
catch (err) {
|
|
73
71
|
logger.warn(err);
|
|
@@ -92,9 +90,7 @@ export class InstallationManager {
|
|
|
92
90
|
}
|
|
93
91
|
await queue.onIdle();
|
|
94
92
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
await remove(path);
|
|
98
|
-
}
|
|
93
|
+
const path = join(this.extensionPath, '.registry', folder);
|
|
94
|
+
await remove(path);
|
|
99
95
|
}
|
|
100
96
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const SyncStatus: {
|
|
2
|
+
readonly SYNCING: "SYNCING";
|
|
3
|
+
readonly IDLE: "IDLE";
|
|
4
|
+
};
|
|
5
|
+
export type SyncStatus = keyof typeof SyncStatus;
|
|
6
|
+
export declare function getSyncStatus(): Promise<SyncStatus>;
|
|
7
|
+
export declare function setSyncStatus(status: SyncStatus): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Checks the filesystem lock file if we are currently synchronizing
|
|
10
|
+
*/
|
|
11
|
+
export declare function isSynchronizing(): Promise<boolean>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions to write a `.status` file on the filesystem to indicate active synchronization
|
|
3
|
+
*/
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { exists } from 'fs-extra';
|
|
6
|
+
import { writeFile, rm } from 'node:fs/promises';
|
|
7
|
+
import { getExtensionsPath } from '../get-extensions-path.js';
|
|
8
|
+
export const SyncStatus = {
|
|
9
|
+
SYNCING: 'SYNCING',
|
|
10
|
+
IDLE: 'IDLE',
|
|
11
|
+
};
|
|
12
|
+
export async function getSyncStatus() {
|
|
13
|
+
const statusFilePath = join(getExtensionsPath(), '.status');
|
|
14
|
+
if (await exists(statusFilePath)) {
|
|
15
|
+
return SyncStatus.SYNCING;
|
|
16
|
+
}
|
|
17
|
+
return SyncStatus.IDLE;
|
|
18
|
+
}
|
|
19
|
+
export async function setSyncStatus(status) {
|
|
20
|
+
const statusFilePath = join(getExtensionsPath(), '.status');
|
|
21
|
+
if (status === SyncStatus.SYNCING) {
|
|
22
|
+
await writeFile(statusFilePath, '');
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await rm(statusFilePath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks the filesystem lock file if we are currently synchronizing
|
|
30
|
+
*/
|
|
31
|
+
export async function isSynchronizing() {
|
|
32
|
+
const status = await getSyncStatus();
|
|
33
|
+
return status === SyncStatus.SYNCING;
|
|
34
|
+
}
|