@directus/api 33.3.1 → 34.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.
- package/dist/ai/chat/lib/create-ui-stream.js +2 -1
- package/dist/ai/chat/lib/transform-file-parts.d.ts +12 -0
- package/dist/ai/chat/lib/transform-file-parts.js +36 -0
- package/dist/ai/files/adapters/anthropic.d.ts +3 -0
- package/dist/ai/files/adapters/anthropic.js +25 -0
- package/dist/ai/files/adapters/google.d.ts +3 -0
- package/dist/ai/files/adapters/google.js +58 -0
- package/dist/ai/files/adapters/index.d.ts +3 -0
- package/dist/ai/files/adapters/index.js +3 -0
- package/dist/ai/files/adapters/openai.d.ts +3 -0
- package/dist/ai/files/adapters/openai.js +22 -0
- package/dist/ai/files/controllers/upload.d.ts +2 -0
- package/dist/ai/files/controllers/upload.js +101 -0
- package/dist/ai/files/lib/fetch-provider.d.ts +1 -0
- package/dist/ai/files/lib/fetch-provider.js +23 -0
- package/dist/ai/files/lib/upload-to-provider.d.ts +4 -0
- package/dist/ai/files/lib/upload-to-provider.js +26 -0
- package/dist/ai/files/router.d.ts +1 -0
- package/dist/ai/files/router.js +5 -0
- package/dist/ai/files/types.d.ts +5 -0
- package/dist/ai/files/types.js +1 -0
- package/dist/ai/providers/anthropic-file-support.d.ts +12 -0
- package/dist/ai/providers/anthropic-file-support.js +94 -0
- package/dist/ai/providers/registry.js +3 -6
- package/dist/ai/tools/fields/index.d.ts +3 -3
- package/dist/ai/tools/fields/index.js +9 -3
- package/dist/ai/tools/flows/index.d.ts +16 -16
- package/dist/ai/tools/schema.d.ts +8 -8
- package/dist/ai/tools/schema.js +2 -2
- package/dist/app.js +10 -1
- package/dist/auth/drivers/oauth2.js +10 -4
- package/dist/auth/drivers/openid.js +10 -4
- package/dist/auth/drivers/saml.js +20 -10
- package/dist/auth/utils/resolve-login-redirect.d.ts +11 -0
- package/dist/auth/utils/resolve-login-redirect.js +62 -0
- package/dist/controllers/deployment-webhooks.d.ts +2 -0
- package/dist/controllers/deployment-webhooks.js +95 -0
- package/dist/controllers/deployment.js +61 -165
- package/dist/controllers/files.js +2 -1
- package/dist/controllers/server.js +32 -26
- package/dist/controllers/tus.js +33 -2
- package/dist/controllers/utils.js +18 -0
- package/dist/database/get-ast-from-query/lib/parse-fields.js +52 -26
- package/dist/database/helpers/date/dialects/oracle.js +2 -0
- package/dist/database/helpers/date/dialects/sqlite.js +2 -0
- package/dist/database/helpers/date/types.d.ts +1 -1
- package/dist/database/helpers/date/types.js +3 -1
- package/dist/database/helpers/fn/dialects/mssql.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/mssql.js +21 -0
- package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -0
- package/dist/database/helpers/fn/dialects/mysql.js +30 -0
- package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/oracle.js +21 -0
- package/dist/database/helpers/fn/dialects/postgres.d.ts +14 -0
- package/dist/database/helpers/fn/dialects/postgres.js +40 -0
- package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/sqlite.js +12 -0
- package/dist/database/helpers/fn/json/parse-function.d.ts +19 -0
- package/dist/database/helpers/fn/json/parse-function.js +66 -0
- package/dist/database/helpers/fn/types.d.ts +8 -0
- package/dist/database/helpers/fn/types.js +19 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mysql.js +11 -0
- package/dist/database/helpers/schema/types.d.ts +1 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/20260211A-add-deployment-webhooks.d.ts +3 -0
- package/dist/database/migrations/20260211A-add-deployment-webhooks.js +37 -0
- package/dist/database/run-ast/lib/apply-query/aggregate.js +4 -4
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +17 -7
- package/dist/database/run-ast/lib/parse-current-level.js +8 -1
- package/dist/database/run-ast/run-ast.js +11 -1
- package/dist/database/run-ast/utils/apply-function-to-column-name.js +7 -1
- package/dist/database/run-ast/utils/get-column.js +13 -2
- package/dist/deployment/deployment.d.ts +25 -2
- package/dist/deployment/drivers/netlify.d.ts +6 -2
- package/dist/deployment/drivers/netlify.js +114 -12
- package/dist/deployment/drivers/vercel.d.ts +5 -2
- package/dist/deployment/drivers/vercel.js +84 -5
- package/dist/deployment.d.ts +5 -0
- package/dist/deployment.js +34 -0
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +1 -1
- package/dist/permissions/utils/get-unaliased-field-key.js +9 -1
- package/dist/request/is-denied-ip.js +24 -23
- package/dist/services/authentication.js +27 -22
- package/dist/services/collections.js +1 -0
- package/dist/services/deployment-projects.d.ts +31 -2
- package/dist/services/deployment-projects.js +109 -5
- package/dist/services/deployment-runs.d.ts +19 -1
- package/dist/services/deployment-runs.js +86 -0
- package/dist/services/deployment.d.ts +44 -3
- package/dist/services/deployment.js +263 -15
- package/dist/services/files/utils/get-metadata.js +6 -6
- package/dist/services/files.d.ts +3 -1
- package/dist/services/files.js +26 -3
- package/dist/services/graphql/resolvers/query.js +23 -6
- package/dist/services/graphql/resolvers/system.js +35 -27
- package/dist/services/payload.d.ts +6 -0
- package/dist/services/payload.js +27 -2
- package/dist/services/server.js +1 -1
- package/dist/services/users.js +6 -1
- package/dist/test-utils/README.md +112 -0
- package/dist/test-utils/controllers.d.ts +65 -0
- package/dist/test-utils/controllers.js +100 -0
- package/dist/test-utils/database.d.ts +1 -1
- package/dist/test-utils/database.js +3 -1
- package/dist/utils/get-field-relational-depth.d.ts +13 -0
- package/dist/utils/get-field-relational-depth.js +22 -0
- package/dist/utils/parse-value.d.ts +4 -0
- package/dist/utils/parse-value.js +11 -0
- package/dist/utils/sanitize-query.js +3 -2
- package/dist/utils/split-fields.d.ts +4 -0
- package/dist/utils/split-fields.js +32 -0
- package/dist/utils/validate-query.js +2 -1
- package/package.json +36 -36
- package/dist/auth/utils/is-login-redirect-allowed.d.ts +0 -7
- package/dist/auth/utils/is-login-redirect-allowed.js +0 -39
|
@@ -11,13 +11,13 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
11
11
|
active: "active";
|
|
12
12
|
inactive: "inactive";
|
|
13
13
|
}>>;
|
|
14
|
-
trigger: z.ZodOptional<z.
|
|
14
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
15
15
|
operation: "operation";
|
|
16
16
|
schedule: "schedule";
|
|
17
17
|
event: "event";
|
|
18
18
|
webhook: "webhook";
|
|
19
19
|
manual: "manual";
|
|
20
|
-
}
|
|
20
|
+
}>>;
|
|
21
21
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
22
22
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
23
23
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -36,10 +36,10 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
36
36
|
}, z.core.$strip>>>;
|
|
37
37
|
date_created: z.ZodOptional<z.ZodString>;
|
|
38
38
|
user_created: z.ZodOptional<z.ZodString>;
|
|
39
|
-
accountability: z.ZodOptional<z.
|
|
39
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
40
40
|
all: "all";
|
|
41
41
|
activity: "activity";
|
|
42
|
-
}
|
|
42
|
+
}>>;
|
|
43
43
|
}, z.core.$strip>;
|
|
44
44
|
}, z.core.$strict>, z.ZodObject<{
|
|
45
45
|
action: z.ZodLiteral<"read">;
|
|
@@ -79,13 +79,13 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
79
79
|
active: "active";
|
|
80
80
|
inactive: "inactive";
|
|
81
81
|
}>>;
|
|
82
|
-
trigger: z.ZodOptional<z.
|
|
82
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
83
83
|
operation: "operation";
|
|
84
84
|
schedule: "schedule";
|
|
85
85
|
event: "event";
|
|
86
86
|
webhook: "webhook";
|
|
87
87
|
manual: "manual";
|
|
88
|
-
}
|
|
88
|
+
}>>;
|
|
89
89
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
90
90
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
91
91
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -104,10 +104,10 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
104
104
|
}, z.core.$strip>>>;
|
|
105
105
|
date_created: z.ZodOptional<z.ZodString>;
|
|
106
106
|
user_created: z.ZodOptional<z.ZodString>;
|
|
107
|
-
accountability: z.ZodOptional<z.
|
|
107
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
108
108
|
all: "all";
|
|
109
109
|
activity: "activity";
|
|
110
|
-
}
|
|
110
|
+
}>>;
|
|
111
111
|
}, z.core.$strip>;
|
|
112
112
|
query: z.ZodOptional<z.ZodObject<{
|
|
113
113
|
fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -176,13 +176,13 @@ export declare const FlowsInputSchema: z.ZodObject<{
|
|
|
176
176
|
active: "active";
|
|
177
177
|
inactive: "inactive";
|
|
178
178
|
}>>;
|
|
179
|
-
trigger: z.ZodOptional<z.
|
|
179
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
180
180
|
operation: "operation";
|
|
181
181
|
schedule: "schedule";
|
|
182
182
|
event: "event";
|
|
183
183
|
webhook: "webhook";
|
|
184
184
|
manual: "manual";
|
|
185
|
-
}
|
|
185
|
+
}>>;
|
|
186
186
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
187
187
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
188
188
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -201,10 +201,10 @@ export declare const FlowsInputSchema: z.ZodObject<{
|
|
|
201
201
|
}, z.core.$strip>>>;
|
|
202
202
|
date_created: z.ZodOptional<z.ZodString>;
|
|
203
203
|
user_created: z.ZodOptional<z.ZodString>;
|
|
204
|
-
accountability: z.ZodOptional<z.
|
|
204
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
205
205
|
all: "all";
|
|
206
206
|
activity: "activity";
|
|
207
|
-
}
|
|
207
|
+
}>>;
|
|
208
208
|
}, z.core.$strip>>;
|
|
209
209
|
key: z.ZodOptional<z.ZodString>;
|
|
210
210
|
}, z.core.$strip>;
|
|
@@ -217,7 +217,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
217
217
|
color?: string | null | undefined;
|
|
218
218
|
description?: string | null | undefined;
|
|
219
219
|
status?: "active" | "inactive" | undefined;
|
|
220
|
-
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" |
|
|
220
|
+
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" | undefined;
|
|
221
221
|
options?: Record<string, any> | null | undefined;
|
|
222
222
|
operation?: string | null | undefined;
|
|
223
223
|
operations?: {
|
|
@@ -236,7 +236,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
236
236
|
}[] | undefined;
|
|
237
237
|
date_created?: string | undefined;
|
|
238
238
|
user_created?: string | undefined;
|
|
239
|
-
accountability?: "all" | "activity" |
|
|
239
|
+
accountability?: "all" | "activity" | undefined;
|
|
240
240
|
};
|
|
241
241
|
} | {
|
|
242
242
|
action: "read";
|
|
@@ -273,7 +273,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
273
273
|
color?: string | null | undefined;
|
|
274
274
|
description?: string | null | undefined;
|
|
275
275
|
status?: "active" | "inactive" | undefined;
|
|
276
|
-
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" |
|
|
276
|
+
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" | undefined;
|
|
277
277
|
options?: Record<string, any> | null | undefined;
|
|
278
278
|
operation?: string | null | undefined;
|
|
279
279
|
operations?: {
|
|
@@ -292,7 +292,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
292
292
|
}[] | undefined;
|
|
293
293
|
date_created?: string | undefined;
|
|
294
294
|
user_created?: string | undefined;
|
|
295
|
-
accountability?: "all" | "activity" |
|
|
295
|
+
accountability?: "all" | "activity" | undefined;
|
|
296
296
|
};
|
|
297
297
|
query?: {
|
|
298
298
|
fields?: string[] | undefined;
|
|
@@ -272,13 +272,13 @@ export declare const FlowItemInputSchema: z.ZodObject<{
|
|
|
272
272
|
active: "active";
|
|
273
273
|
inactive: "inactive";
|
|
274
274
|
}>>;
|
|
275
|
-
trigger: z.ZodOptional<z.
|
|
275
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
276
276
|
operation: "operation";
|
|
277
277
|
schedule: "schedule";
|
|
278
278
|
event: "event";
|
|
279
279
|
webhook: "webhook";
|
|
280
280
|
manual: "manual";
|
|
281
|
-
}
|
|
281
|
+
}>>;
|
|
282
282
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
283
283
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
284
284
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -297,10 +297,10 @@ export declare const FlowItemInputSchema: z.ZodObject<{
|
|
|
297
297
|
}, z.core.$strip>>>;
|
|
298
298
|
date_created: z.ZodOptional<z.ZodString>;
|
|
299
299
|
user_created: z.ZodOptional<z.ZodString>;
|
|
300
|
-
accountability: z.ZodOptional<z.
|
|
300
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
301
301
|
all: "all";
|
|
302
302
|
activity: "activity";
|
|
303
|
-
}
|
|
303
|
+
}>>;
|
|
304
304
|
}, z.core.$strip>;
|
|
305
305
|
export declare const FlowItemValidateSchema: z.ZodObject<{
|
|
306
306
|
id: z.ZodOptional<z.ZodString>;
|
|
@@ -312,13 +312,13 @@ export declare const FlowItemValidateSchema: z.ZodObject<{
|
|
|
312
312
|
active: "active";
|
|
313
313
|
inactive: "inactive";
|
|
314
314
|
}>>;
|
|
315
|
-
trigger: z.ZodOptional<z.
|
|
315
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
316
316
|
operation: "operation";
|
|
317
317
|
schedule: "schedule";
|
|
318
318
|
event: "event";
|
|
319
319
|
webhook: "webhook";
|
|
320
320
|
manual: "manual";
|
|
321
|
-
}
|
|
321
|
+
}>>;
|
|
322
322
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
323
323
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
324
324
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -337,10 +337,10 @@ export declare const FlowItemValidateSchema: z.ZodObject<{
|
|
|
337
337
|
}, z.core.$strip>>>;
|
|
338
338
|
date_created: z.ZodOptional<z.ZodString>;
|
|
339
339
|
user_created: z.ZodOptional<z.ZodString>;
|
|
340
|
-
accountability: z.ZodOptional<z.
|
|
340
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
341
341
|
all: "all";
|
|
342
342
|
activity: "activity";
|
|
343
|
-
}
|
|
343
|
+
}>>;
|
|
344
344
|
}, z.core.$strip>;
|
|
345
345
|
export declare const TriggerFlowInputSchema: z.ZodObject<{
|
|
346
346
|
id: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
package/dist/ai/tools/schema.js
CHANGED
|
@@ -135,13 +135,13 @@ export const FlowItemInputSchema = z
|
|
|
135
135
|
color: z.union([z.string(), z.null()]),
|
|
136
136
|
description: z.union([z.string(), z.null()]),
|
|
137
137
|
status: z.enum(['active', 'inactive']),
|
|
138
|
-
trigger: z.
|
|
138
|
+
trigger: z.enum(['event', 'schedule', 'operation', 'webhook', 'manual']),
|
|
139
139
|
options: z.union([z.record(z.string(), z.any()), z.null()]),
|
|
140
140
|
operation: z.union([z.string(), z.null()]),
|
|
141
141
|
operations: z.array(OperationItemInputSchema),
|
|
142
142
|
date_created: z.string(),
|
|
143
143
|
user_created: z.string(),
|
|
144
|
-
accountability: z.
|
|
144
|
+
accountability: z.enum(['all', 'activity']),
|
|
145
145
|
})
|
|
146
146
|
.partial();
|
|
147
147
|
export const FlowItemValidateSchema = FlowItemInputSchema;
|
package/dist/app.js
CHANGED
|
@@ -10,6 +10,7 @@ import express from 'express';
|
|
|
10
10
|
import { merge } from 'lodash-es';
|
|
11
11
|
import qs from 'qs';
|
|
12
12
|
import { aiChatRouter } from './ai/chat/router.js';
|
|
13
|
+
import { aiFilesRouter } from './ai/files/router.js';
|
|
13
14
|
import { registerAuthProviders } from './auth.js';
|
|
14
15
|
import accessRouter from './controllers/access.js';
|
|
15
16
|
import activityRouter from './controllers/activity.js';
|
|
@@ -18,6 +19,7 @@ import authRouter from './controllers/auth.js';
|
|
|
18
19
|
import collectionsRouter from './controllers/collections.js';
|
|
19
20
|
import commentsRouter from './controllers/comments.js';
|
|
20
21
|
import dashboardsRouter from './controllers/dashboards.js';
|
|
22
|
+
import deploymentWebhookRouter from './controllers/deployment-webhooks.js';
|
|
21
23
|
import deploymentRouter from './controllers/deployment.js';
|
|
22
24
|
import extensionsRouter from './controllers/extensions.js';
|
|
23
25
|
import fieldsRouter from './controllers/fields.js';
|
|
@@ -48,7 +50,7 @@ import usersRouter from './controllers/users.js';
|
|
|
48
50
|
import utilsRouter from './controllers/utils.js';
|
|
49
51
|
import versionsRouter from './controllers/versions.js';
|
|
50
52
|
import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations, } from './database/index.js';
|
|
51
|
-
import { registerDeploymentDrivers } from './deployment.js';
|
|
53
|
+
import { ensureDeploymentWebhooks, registerDeploymentDrivers } from './deployment.js';
|
|
52
54
|
import emitter from './emitter.js';
|
|
53
55
|
import { getExtensionManager } from './extensions/index.js';
|
|
54
56
|
import { getFlowManager } from './flows.js';
|
|
@@ -96,6 +98,7 @@ export default async function createApp() {
|
|
|
96
98
|
await validateStorage();
|
|
97
99
|
await registerAuthProviders();
|
|
98
100
|
registerDeploymentDrivers();
|
|
101
|
+
await ensureDeploymentWebhooks();
|
|
99
102
|
const extensionManager = getExtensionManager();
|
|
100
103
|
const flowManager = getFlowManager();
|
|
101
104
|
await extensionManager.initialize();
|
|
@@ -161,6 +164,9 @@ export default async function createApp() {
|
|
|
161
164
|
app.use((req, res, next) => {
|
|
162
165
|
express.json({
|
|
163
166
|
limit: env['MAX_PAYLOAD_SIZE'],
|
|
167
|
+
verify: (req, _res, buf) => {
|
|
168
|
+
req.rawBody = buf;
|
|
169
|
+
},
|
|
164
170
|
})(req, res, (err) => {
|
|
165
171
|
if (err) {
|
|
166
172
|
return next(new InvalidPayloadError({ reason: err.message }));
|
|
@@ -214,6 +220,8 @@ export default async function createApp() {
|
|
|
214
220
|
app.use(rateLimiter);
|
|
215
221
|
}
|
|
216
222
|
app.get('/server/ping', (_req, res) => res.send('pong'));
|
|
223
|
+
// Public webhook endpoint (signature-verified by the provider)
|
|
224
|
+
app.use('/deployments/webhooks', deploymentWebhookRouter);
|
|
217
225
|
app.use(authenticate);
|
|
218
226
|
app.use(schema);
|
|
219
227
|
app.use(sanitizeQuery);
|
|
@@ -243,6 +251,7 @@ export default async function createApp() {
|
|
|
243
251
|
}
|
|
244
252
|
if (toBoolean(env['AI_ENABLED']) === true) {
|
|
245
253
|
app.use('/ai/chat', aiChatRouter);
|
|
254
|
+
app.use('/ai/files', aiFilesRouter);
|
|
246
255
|
}
|
|
247
256
|
if (env['METRICS_ENABLED'] === true) {
|
|
248
257
|
app.use('/metrics', metricsRouter);
|
|
@@ -21,7 +21,7 @@ import { getSecret } from '../../utils/get-secret.js';
|
|
|
21
21
|
import { verifyJWT } from '../../utils/jwt.js';
|
|
22
22
|
import { Url } from '../../utils/url.js';
|
|
23
23
|
import { generateCallbackUrl } from '../utils/generate-callback-url.js';
|
|
24
|
-
import {
|
|
24
|
+
import { resolveLoginRedirect } from '../utils/resolve-login-redirect.js';
|
|
25
25
|
import { LocalAuthDriver } from './local.js';
|
|
26
26
|
export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
27
27
|
client;
|
|
@@ -273,8 +273,12 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
273
273
|
const codeVerifier = provider.generateCodeVerifier();
|
|
274
274
|
const prompt = !!req.query['prompt'];
|
|
275
275
|
const otp = req.query['otp'];
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
let redirect = req.query['redirect'];
|
|
277
|
+
try {
|
|
278
|
+
redirect = resolveLoginRedirect(redirect, { provider: providerName });
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
useLogger().error(e);
|
|
278
282
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
279
283
|
}
|
|
280
284
|
const callbackUrl = generateCallbackUrl(providerName, `${req.protocol}://${req.get('host')}`);
|
|
@@ -356,8 +360,10 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
356
360
|
const claims = verifyJWT(accessToken, getSecret());
|
|
357
361
|
if (claims?.enforce_tfa === true) {
|
|
358
362
|
const url = new Url(env['PUBLIC_URL']).addPath('admin', 'tfa-setup');
|
|
359
|
-
if (redirect)
|
|
363
|
+
if (redirect) {
|
|
360
364
|
url.setQuery('redirect', redirect);
|
|
365
|
+
url.setQuery('provider', providerName);
|
|
366
|
+
}
|
|
361
367
|
redirect = url.toString();
|
|
362
368
|
}
|
|
363
369
|
}
|
|
@@ -21,7 +21,7 @@ import { getSecret } from '../../utils/get-secret.js';
|
|
|
21
21
|
import { verifyJWT } from '../../utils/jwt.js';
|
|
22
22
|
import { Url } from '../../utils/url.js';
|
|
23
23
|
import { generateCallbackUrl } from '../utils/generate-callback-url.js';
|
|
24
|
-
import {
|
|
24
|
+
import { resolveLoginRedirect } from '../utils/resolve-login-redirect.js';
|
|
25
25
|
import { LocalAuthDriver } from './local.js';
|
|
26
26
|
export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
27
27
|
client;
|
|
@@ -324,9 +324,13 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
324
324
|
const provider = getAuthProvider(providerName);
|
|
325
325
|
const codeVerifier = provider.generateCodeVerifier();
|
|
326
326
|
const prompt = !!req.query['prompt'];
|
|
327
|
-
|
|
327
|
+
let redirect = req.query['redirect'];
|
|
328
328
|
const otp = req.query['otp'];
|
|
329
|
-
|
|
329
|
+
try {
|
|
330
|
+
redirect = resolveLoginRedirect(redirect, { provider: providerName });
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
useLogger().error(e);
|
|
330
334
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
331
335
|
}
|
|
332
336
|
const callbackUrl = generateCallbackUrl(providerName, `${req.protocol}://${req.get('host')}`);
|
|
@@ -418,8 +422,10 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
418
422
|
const claims = verifyJWT(accessToken, getSecret());
|
|
419
423
|
if (claims?.enforce_tfa === true) {
|
|
420
424
|
const url = new Url(env['PUBLIC_URL']).addPath('admin', 'tfa-setup');
|
|
421
|
-
if (redirect)
|
|
425
|
+
if (redirect) {
|
|
422
426
|
url.setQuery('redirect', redirect);
|
|
427
|
+
url.setQuery('provider', providerName);
|
|
428
|
+
}
|
|
423
429
|
redirect = url.toString();
|
|
424
430
|
}
|
|
425
431
|
}
|
|
@@ -13,7 +13,7 @@ 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
15
|
import { getSchema } from '../../utils/get-schema.js';
|
|
16
|
-
import {
|
|
16
|
+
import { resolveLoginRedirect } from '../utils/resolve-login-redirect.js';
|
|
17
17
|
import { LocalAuthDriver } from './local.js';
|
|
18
18
|
// Register the samlify schema validator
|
|
19
19
|
samlify.setSchemaValidator(validator);
|
|
@@ -94,8 +94,12 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
94
94
|
const { context: url } = sp.createLoginRequest(idp, 'redirect');
|
|
95
95
|
const parsedUrl = new URL(url);
|
|
96
96
|
if (req.query['redirect']) {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
let redirect = req.query['redirect'];
|
|
98
|
+
try {
|
|
99
|
+
redirect = resolveLoginRedirect(redirect, { provider: providerName });
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
useLogger().error(e);
|
|
99
103
|
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
100
104
|
}
|
|
101
105
|
parsedUrl.searchParams.append('RelayState', redirect);
|
|
@@ -115,10 +119,16 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
115
119
|
}));
|
|
116
120
|
router.post('/acs', express.urlencoded({ extended: false }), asyncHandler(async (req, res, next) => {
|
|
117
121
|
const logger = useLogger();
|
|
118
|
-
|
|
122
|
+
let redirect = req.body?.RelayState;
|
|
119
123
|
const authMode = (env[`AUTH_${providerName.toUpperCase()}_MODE`] ?? 'session');
|
|
120
|
-
if (
|
|
121
|
-
|
|
124
|
+
if (redirect) {
|
|
125
|
+
try {
|
|
126
|
+
redirect = resolveLoginRedirect(redirect, { provider: providerName });
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
useLogger().error(e);
|
|
130
|
+
throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
|
|
131
|
+
}
|
|
122
132
|
}
|
|
123
133
|
try {
|
|
124
134
|
const { sp, idp } = getAuthProvider(providerName);
|
|
@@ -134,19 +144,19 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
134
144
|
expires,
|
|
135
145
|
},
|
|
136
146
|
};
|
|
137
|
-
if (
|
|
147
|
+
if (redirect) {
|
|
138
148
|
if (authMode === 'session') {
|
|
139
149
|
res.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
|
|
140
150
|
}
|
|
141
151
|
else {
|
|
142
152
|
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
143
153
|
}
|
|
144
|
-
return res.redirect(
|
|
154
|
+
return res.redirect(redirect);
|
|
145
155
|
}
|
|
146
156
|
return next();
|
|
147
157
|
}
|
|
148
158
|
catch (error) {
|
|
149
|
-
if (
|
|
159
|
+
if (redirect) {
|
|
150
160
|
let reason = 'UNKNOWN_EXCEPTION';
|
|
151
161
|
if (isDirectusError(error)) {
|
|
152
162
|
reason = error.code;
|
|
@@ -154,7 +164,7 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
154
164
|
else {
|
|
155
165
|
logger.warn(error, `[SAML] Unexpected error during SAML login`);
|
|
156
166
|
}
|
|
157
|
-
return res.redirect(`${
|
|
167
|
+
return res.redirect(`${redirect.split('?')[0]}?reason=${reason}`);
|
|
158
168
|
}
|
|
159
169
|
logger.warn(error, `[SAML] Unexpected error during SAML login`);
|
|
160
170
|
throw error;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves and validates the redirect URL after a successful SSO login.
|
|
3
|
+
* Returns a safe redirect path or URL, or throws if the redirect is invalid or not allowed.
|
|
4
|
+
* @param redirect URL or relative path to redirect to after login
|
|
5
|
+
* @param opts.provider SSO provider name, used to check provider-specific allow lists
|
|
6
|
+
* @returns Resolved redirect path or URL string
|
|
7
|
+
* @throws If the redirect is not a string, PUBLIC_URL is not defined, or the redirect is not allowed
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveLoginRedirect(redirect: unknown, opts?: {
|
|
10
|
+
provider?: string | undefined;
|
|
11
|
+
}): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toArray } from '@directus/utils';
|
|
3
|
+
import isUrlAllowed from '../../utils/is-url-allowed.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves and validates the redirect URL after a successful SSO login.
|
|
6
|
+
* Returns a safe redirect path or URL, or throws if the redirect is invalid or not allowed.
|
|
7
|
+
* @param redirect URL or relative path to redirect to after login
|
|
8
|
+
* @param opts.provider SSO provider name, used to check provider-specific allow lists
|
|
9
|
+
* @returns Resolved redirect path or URL string
|
|
10
|
+
* @throws If the redirect is not a string, PUBLIC_URL is not defined, or the redirect is not allowed
|
|
11
|
+
*/
|
|
12
|
+
export function resolveLoginRedirect(redirect, opts = {}) {
|
|
13
|
+
const env = useEnv();
|
|
14
|
+
const publicURL = env['PUBLIC_URL'];
|
|
15
|
+
// Default empty redirect to root
|
|
16
|
+
if (!redirect)
|
|
17
|
+
return '/';
|
|
18
|
+
if (typeof redirect !== 'string')
|
|
19
|
+
throw new Error('"redirect" must be a string');
|
|
20
|
+
if (!publicURL)
|
|
21
|
+
throw new Error('"PUBLIC_URL" must be defined');
|
|
22
|
+
// Relative URL
|
|
23
|
+
if (URL.canParse(redirect) === false) {
|
|
24
|
+
try {
|
|
25
|
+
const dummyDomain = 'http://dummy.local';
|
|
26
|
+
const { protocol: dummyProtocol, host: dummyHost } = new URL(dummyDomain);
|
|
27
|
+
const parsedRelativeURL = new URL(redirect, dummyDomain);
|
|
28
|
+
if (dummyProtocol !== parsedRelativeURL.protocol || dummyHost !== parsedRelativeURL.host) {
|
|
29
|
+
throw new Error('Relative URL mismatch');
|
|
30
|
+
}
|
|
31
|
+
// If the protocol & host match then it is a safe relative path
|
|
32
|
+
return parsedRelativeURL.toString().replace(dummyDomain, '');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Unparsable URL
|
|
36
|
+
throw new Error('Invalid relative URL');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Absolute redirect
|
|
40
|
+
const parsedAbsoluteURL = new URL(redirect);
|
|
41
|
+
if (!['http:', 'https:'].includes(parsedAbsoluteURL.protocol)) {
|
|
42
|
+
throw new Error('Only http/https redirect protocols are allowed');
|
|
43
|
+
}
|
|
44
|
+
if (opts.provider) {
|
|
45
|
+
const envKey = `AUTH_${opts.provider.toUpperCase()}_REDIRECT_ALLOW_LIST`;
|
|
46
|
+
if (envKey in env) {
|
|
47
|
+
const allowedList = toArray(String(env[envKey]));
|
|
48
|
+
allowedList.push(publicURL);
|
|
49
|
+
if (isUrlAllowed(redirect, allowedList)) {
|
|
50
|
+
return parsedAbsoluteURL.toString();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (URL.canParse(publicURL) === false)
|
|
55
|
+
throw new Error('PUBLIC_URL must be a valid URL');
|
|
56
|
+
const { protocol: publicProtocol, host: publicHost } = new URL(publicURL);
|
|
57
|
+
// Reject "app" redirects not matching PUBLIC_URL
|
|
58
|
+
if (publicProtocol !== parsedAbsoluteURL.protocol || publicHost !== parsedAbsoluteURL.host) {
|
|
59
|
+
throw new Error('App "redirect" must match PUBLIC_URL');
|
|
60
|
+
}
|
|
61
|
+
return parsedAbsoluteURL.toString();
|
|
62
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DEPLOYMENT_PROVIDER_TYPES } from '@directus/types';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import getDatabase from '../database/index.js';
|
|
4
|
+
import { getDeploymentDriver } from '../deployment.js';
|
|
5
|
+
import emitter from '../emitter.js';
|
|
6
|
+
import { useLogger } from '../logger/index.js';
|
|
7
|
+
import { DeploymentProjectsService } from '../services/deployment-projects.js';
|
|
8
|
+
import { DeploymentRunsService } from '../services/deployment-runs.js';
|
|
9
|
+
import { DeploymentService } from '../services/deployment.js';
|
|
10
|
+
import asyncHandler from '../utils/async-handler.js';
|
|
11
|
+
import { getSchema } from '../utils/get-schema.js';
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
router.post('/:provider', asyncHandler(async (req, res) => {
|
|
14
|
+
const logger = useLogger();
|
|
15
|
+
const provider = req.params['provider'];
|
|
16
|
+
if (!DEPLOYMENT_PROVIDER_TYPES.includes(provider)) {
|
|
17
|
+
return res.sendStatus(404);
|
|
18
|
+
}
|
|
19
|
+
const rawBody = req.rawBody;
|
|
20
|
+
if (!rawBody) {
|
|
21
|
+
logger.debug(`[webhook:${provider}] No raw body`);
|
|
22
|
+
return res.sendStatus(400);
|
|
23
|
+
}
|
|
24
|
+
const schema = await getSchema();
|
|
25
|
+
const knex = getDatabase();
|
|
26
|
+
const deploymentService = new DeploymentService({
|
|
27
|
+
schema,
|
|
28
|
+
knex,
|
|
29
|
+
accountability: null,
|
|
30
|
+
});
|
|
31
|
+
let webhookConfig;
|
|
32
|
+
try {
|
|
33
|
+
webhookConfig = await deploymentService.getWebhookConfig(provider);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
logger.warn(`[webhook:${provider}] No webhook config found`);
|
|
37
|
+
return res.sendStatus(404);
|
|
38
|
+
}
|
|
39
|
+
if (!webhookConfig.webhook_secret) {
|
|
40
|
+
logger.warn(`[webhook:${provider}] No webhook secret configured`);
|
|
41
|
+
return res.sendStatus(404);
|
|
42
|
+
}
|
|
43
|
+
const driver = getDeploymentDriver(provider, webhookConfig.credentials, webhookConfig.options);
|
|
44
|
+
// Fallback for providers whose API doesn't support webhook signature headers (e.g. Netlify)
|
|
45
|
+
const headers = { ...req.headers };
|
|
46
|
+
const queryToken = req.query['token'];
|
|
47
|
+
if (typeof queryToken === 'string') {
|
|
48
|
+
headers['x-webhook-token'] = queryToken;
|
|
49
|
+
}
|
|
50
|
+
const event = driver.verifyAndParseWebhook(rawBody, headers, webhookConfig.webhook_secret);
|
|
51
|
+
if (!event) {
|
|
52
|
+
logger.warn(`[webhook:${provider}] Verification failed or unknown event`);
|
|
53
|
+
try {
|
|
54
|
+
const body = JSON.parse(rawBody.toString('utf-8'));
|
|
55
|
+
logger.warn(`[webhook:${provider}] Raw event type: ${body.type ?? body.state ?? 'unknown'}`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
logger.warn(`[webhook:${provider}] Unparseable body: ${rawBody.toString('utf-8').slice(0, 200)}`);
|
|
59
|
+
}
|
|
60
|
+
return res.sendStatus(401);
|
|
61
|
+
}
|
|
62
|
+
// Look up project by external_id
|
|
63
|
+
const projectsService = new DeploymentProjectsService({
|
|
64
|
+
schema,
|
|
65
|
+
knex,
|
|
66
|
+
accountability: null,
|
|
67
|
+
});
|
|
68
|
+
const project = await projectsService.readByExternalId(event.project_external_id);
|
|
69
|
+
if (!project) {
|
|
70
|
+
// 410 signals the provider this project is no longer tracked
|
|
71
|
+
logger.info(`[webhook:${provider}] Project ${event.project_external_id} not tracked`);
|
|
72
|
+
return res.sendStatus(410);
|
|
73
|
+
}
|
|
74
|
+
const runsService = new DeploymentRunsService({
|
|
75
|
+
schema,
|
|
76
|
+
knex,
|
|
77
|
+
accountability: null,
|
|
78
|
+
});
|
|
79
|
+
const runId = await runsService.processWebhookEvent(project.id, event);
|
|
80
|
+
// Emit action events
|
|
81
|
+
const eventPayload = {
|
|
82
|
+
provider,
|
|
83
|
+
project_id: project.id,
|
|
84
|
+
run_id: runId,
|
|
85
|
+
external_id: event.deployment_external_id,
|
|
86
|
+
status: event.status,
|
|
87
|
+
url: event.url,
|
|
88
|
+
target: event.target,
|
|
89
|
+
timestamp: event.timestamp,
|
|
90
|
+
};
|
|
91
|
+
emitter.emitAction(['deployment.webhook', `deployment.webhook.${event.type}`], eventPayload, null);
|
|
92
|
+
logger.info(`[webhook:${provider}] Processed: ${event.type} → run ${runId}`);
|
|
93
|
+
return res.sendStatus(200);
|
|
94
|
+
}));
|
|
95
|
+
export default router;
|