@directus/api 35.2.0 → 36.0.0-rc.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/models/chat-request.js +48 -48
- package/dist/ai/chat/models/object-request.js +6 -6
- package/dist/ai/chat/models/providers.js +14 -14
- package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
- package/dist/ai/mcp/server.js +44 -6
- package/dist/ai/mcp/utils.js +31 -0
- package/dist/ai/tools/assets/index.js +3 -3
- package/dist/ai/tools/collections/index.js +18 -18
- package/dist/ai/tools/fields/index.js +18 -18
- package/dist/ai/tools/files/index.js +18 -18
- package/dist/ai/tools/flows/index.js +16 -16
- package/dist/ai/tools/folders/index.js +18 -18
- package/dist/ai/tools/items/index.js +17 -17
- package/dist/ai/tools/operations/index.js +16 -16
- package/dist/ai/tools/relations/index.js +22 -22
- package/dist/ai/tools/schema/index.js +3 -3
- package/dist/ai/tools/schema.js +159 -159
- package/dist/ai/tools/system/index.js +3 -3
- package/dist/ai/tools/trigger-flow/index.js +3 -3
- package/dist/app.js +35 -11
- package/dist/auth/drivers/ldap.js +3 -1
- package/dist/auth/drivers/local.js +2 -0
- package/dist/auth/drivers/oauth2.js +3 -1
- package/dist/auth/drivers/openid.js +3 -1
- package/dist/auth/drivers/saml.js +2 -0
- package/dist/auth/utils/check-local-disabled.js +16 -0
- package/dist/auth/utils/check-sso-enabled.js +14 -0
- package/dist/auth.js +8 -5
- package/dist/cli/commands/bootstrap/index.js +3 -0
- package/dist/cli/commands/cache/clear.js +6 -1
- package/dist/cli/commands/roles/create.js +4 -1
- package/dist/cli/commands/users/create.js +3 -0
- package/dist/constants.js +8 -1
- package/dist/controllers/access.js +1 -1
- package/dist/controllers/activity.js +2 -1
- package/dist/controllers/assets.js +2 -0
- package/dist/controllers/auth.js +13 -5
- package/dist/controllers/collections.js +1 -1
- package/dist/controllers/comments.js +1 -1
- package/dist/controllers/dashboards.js +1 -1
- package/dist/controllers/fields.js +1 -1
- package/dist/controllers/files.js +3 -1
- package/dist/controllers/flows.js +6 -5
- package/dist/controllers/folders.js +1 -1
- package/dist/controllers/graphql.js +2 -0
- package/dist/controllers/items.js +3 -1
- package/dist/controllers/license.js +119 -0
- package/dist/controllers/mcp/index.js +38 -0
- package/dist/controllers/mcp/oauth-clients.js +68 -0
- package/dist/controllers/mcp/oauth-consent-page.js +316 -0
- package/dist/controllers/mcp/oauth.js +381 -0
- package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
- package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
- package/dist/controllers/notifications.js +1 -1
- package/dist/controllers/operations.js +1 -1
- package/dist/controllers/panels.js +1 -1
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/policies.js +1 -1
- package/dist/controllers/presets.js +1 -1
- package/dist/controllers/revisions.js +3 -2
- package/dist/controllers/roles.js +1 -1
- package/dist/controllers/server.js +38 -10
- package/dist/controllers/shares.js +1 -1
- package/dist/controllers/translations.js +1 -1
- package/dist/controllers/users.js +1 -1
- package/dist/controllers/utils.js +2 -2
- package/dist/controllers/versions.js +12 -5
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
- package/dist/database/helpers/fn/dialects/mysql.js +7 -12
- package/dist/database/helpers/fn/dialects/oracle.js +3 -4
- package/dist/database/helpers/fn/dialects/postgres.js +4 -26
- package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
- package/dist/database/helpers/fn/json/parse-function.js +14 -6
- package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
- package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
- package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
- package/dist/database/migrations/20260507A-add-licensing.js +22 -0
- package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
- package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
- package/dist/database/run-ast/lib/apply-query/index.js +4 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
- package/dist/database/run-ast/lib/get-db-query.js +21 -9
- package/dist/database/run-ast/lib/parse-current-level.js +2 -1
- package/dist/database/run-ast/run-ast.js +2 -1
- package/dist/database/run-ast/utils/get-column.js +2 -1
- package/dist/database/run-ast/utils/merge-with-parent-items.js +5 -3
- package/dist/extensions/lib/installation/manager.js +1 -1
- package/dist/extensions/lib/sandbox/register/operation.js +1 -1
- package/dist/extensions/lib/sync/sync.js +1 -1
- package/dist/extensions/manager.js +3 -3
- package/dist/flows.js +5 -5
- package/dist/license/entitlements/lib/collections.js +37 -0
- package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
- package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
- package/dist/license/entitlements/lib/flows.js +29 -0
- package/dist/license/entitlements/lib/seats.js +103 -0
- package/dist/license/entitlements/lib/sso-enabled.js +45 -0
- package/dist/license/entitlements/manager.js +256 -0
- package/dist/license/index.js +4 -0
- package/dist/license/manager.js +505 -0
- package/dist/license/utils/compute-license-status.js +27 -0
- package/dist/license/utils/get-core-grace-expires-at.js +38 -0
- package/dist/license/utils/get-license-key.js +23 -0
- package/dist/license/utils/get-license-token.js +23 -0
- package/dist/license/utils/handle-license-error.js +41 -0
- package/dist/license/utils/is-in-core-grace-period.js +11 -0
- package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
- package/dist/license/utils/use-rpc.js +33 -0
- package/dist/middleware/cache.js +4 -1
- package/dist/middleware/error-handler.js +11 -0
- package/dist/middleware/extract-token.js +11 -2
- package/dist/middleware/is-admin.js +16 -0
- package/dist/middleware/is-locked.js +16 -0
- package/dist/middleware/mcp-oauth-guard.js +23 -0
- package/dist/middleware/request-counter.js +5 -2
- package/dist/packages/types/dist/index.js +117 -122
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
- package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
- package/dist/request/is-denied-ip.js +2 -0
- package/dist/schedules/license.js +31 -0
- package/dist/schedules/oauth-cleanup.js +26 -0
- package/dist/schedules/retention.js +1 -1
- package/dist/schedules/telemetry.js +4 -1
- package/dist/schedules/tus.js +1 -1
- package/dist/schedules/utils/duration-to-cron.js +36 -0
- package/dist/services/activity.js +15 -0
- package/dist/services/authentication.js +12 -5
- package/dist/services/collections.js +40 -10
- package/dist/services/fields.js +6 -6
- package/dist/services/flows.js +12 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -2
- package/dist/services/graphql/resolvers/system-global.js +1 -1
- package/dist/services/graphql/resolvers/system.js +43 -27
- package/dist/services/graphql/schema/get-types.js +28 -7
- package/dist/services/graphql/schema/parse-query.js +8 -0
- package/dist/services/graphql/schema/read.js +12 -0
- package/dist/services/graphql/types/json-filter.js +30 -0
- package/dist/services/index.js +6 -6
- package/dist/services/items.js +32 -14
- package/dist/services/mcp-oauth/cimd.js +307 -0
- package/dist/services/mcp-oauth/index.js +1185 -0
- package/dist/services/mcp-oauth/types/error.js +22 -0
- package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
- package/dist/services/mcp-oauth/utils/domain.js +21 -0
- package/dist/services/mcp-oauth/utils/loopback.js +11 -0
- package/dist/services/mcp-oauth/utils/redirect.js +84 -0
- package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
- package/dist/services/payload.js +2 -1
- package/dist/services/permissions.js +31 -9
- package/dist/services/revisions.js +15 -0
- package/dist/services/server.js +66 -68
- package/dist/services/settings.js +37 -3
- package/dist/services/users.js +23 -6
- package/dist/services/utils.js +6 -1
- package/dist/services/versions.js +160 -70
- package/dist/utils/calculate-field-depth.js +1 -0
- package/dist/utils/create-admin.js +3 -3
- package/dist/utils/deep-freeze.js +24 -0
- package/dist/utils/extract-function-name.js +13 -0
- package/dist/utils/generate-translations.js +5 -5
- package/dist/utils/get-accountability-for-token.js +13 -1
- package/dist/utils/get-cache-key.js +1 -1
- package/dist/utils/get-history-filter-query.js +22 -0
- package/dist/utils/get-schema.js +2 -2
- package/dist/utils/get-service.js +3 -3
- package/dist/utils/is-admin.js +9 -0
- package/dist/utils/is-unauthenticated.js +15 -0
- package/dist/utils/parse-oauth-scope.js +12 -0
- package/dist/utils/sanitize-query.js +2 -2
- package/dist/utils/split-field-path.js +29 -0
- package/dist/utils/store.js +1 -1
- package/dist/utils/transaction.js +2 -2
- package/dist/utils/translations-validation.js +2 -2
- package/dist/utils/validate-query.js +35 -4
- package/dist/utils/validate-user-count-integrity.js +28 -5
- package/dist/utils/verify-session-jwt.js +5 -2
- package/dist/utils/versioning/handle-version.js +131 -48
- package/dist/utils/versioning/remove-circular.js +17 -0
- package/dist/websocket/authenticate.js +2 -1
- package/dist/websocket/collab/collab.js +1 -1
- package/dist/websocket/collab/room.js +1 -1
- package/dist/websocket/controllers/base.js +12 -0
- package/dist/websocket/controllers/graphql.js +1 -1
- package/dist/websocket/handlers/subscribe.js +1 -1
- package/dist/websocket/messages.js +64 -64
- package/dist/websocket/utils/items.js +2 -2
- package/license +90 -80
- package/package.json +33 -32
- package/dist/controllers/mcp.js +0 -31
|
@@ -65,6 +65,14 @@ async function getQuery(rawQuery, schema, selections, variableValues, accountabi
|
|
|
65
65
|
for (const subSelection of selection.selectionSet.selections) {
|
|
66
66
|
if (subSelection.kind !== "Field") continue;
|
|
67
67
|
if (subSelection.name.value.startsWith("__")) continue;
|
|
68
|
+
if (subSelection.name.value === "json" && subSelection.arguments?.length) {
|
|
69
|
+
const pathArg = subSelection.arguments.find((a) => a.name.value === "path");
|
|
70
|
+
if (pathArg) {
|
|
71
|
+
const pathValue = parseArgs([pathArg], variableValues).path;
|
|
72
|
+
children.push(`json(${rootField}, ${pathValue})`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
68
76
|
children.push(`${subSelection.name.value}(${rootField})`);
|
|
69
77
|
}
|
|
70
78
|
} else children = await parseFields(selection.selectionSet.selections, currentAlias ?? current, childCollection, selection.kind === "Field" ? selection.name.value : void 0);
|
|
@@ -6,6 +6,7 @@ import { GraphQLGeoJSON } from "../types/geojson.js";
|
|
|
6
6
|
import { GraphQLHash } from "../types/hash.js";
|
|
7
7
|
import { getGraphQLType } from "../../../utils/get-graphql-type.js";
|
|
8
8
|
import { resolveQuery } from "../resolvers/query.js";
|
|
9
|
+
import { GraphQLJsonFilter } from "../types/json-filter.js";
|
|
9
10
|
import { GraphQLStringOrFloat } from "../types/string-or-float.js";
|
|
10
11
|
import { getTypes } from "./get-types.js";
|
|
11
12
|
import { SYSTEM_DENY_LIST } from "./index.js";
|
|
@@ -154,6 +155,14 @@ async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields)
|
|
|
154
155
|
_nempty: { type: GraphQLBoolean }
|
|
155
156
|
}
|
|
156
157
|
});
|
|
158
|
+
const JsonFilterOperators = schemaComposer.createInputTC({
|
|
159
|
+
name: "json_filter_operators",
|
|
160
|
+
fields: {
|
|
161
|
+
_json: { type: GraphQLJsonFilter },
|
|
162
|
+
_null: { type: GraphQLBoolean },
|
|
163
|
+
_nnull: { type: GraphQLBoolean }
|
|
164
|
+
}
|
|
165
|
+
});
|
|
157
166
|
const CountFunctionFilterOperators = schemaComposer.createInputTC({
|
|
158
167
|
name: "count_function_filter_operators",
|
|
159
168
|
fields: { count: { type: NumberFilterOperators } }
|
|
@@ -213,6 +222,9 @@ async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields)
|
|
|
213
222
|
case GraphQLDate:
|
|
214
223
|
filterOperatorType = DateFilterOperators;
|
|
215
224
|
break;
|
|
225
|
+
case GraphQLJSON:
|
|
226
|
+
filterOperatorType = JsonFilterOperators;
|
|
227
|
+
break;
|
|
216
228
|
case GraphQLGeoJSON:
|
|
217
229
|
filterOperatorType = GeometryFilterOperators;
|
|
218
230
|
break;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { GraphQLScalarType } from "graphql";
|
|
2
|
+
import { GraphQLJSON } from "graphql-compose";
|
|
3
|
+
|
|
4
|
+
//#region src/services/graphql/types/json-filter.ts
|
|
5
|
+
function validateJsonFilterValue(value) {
|
|
6
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("json filter value must be a plain object mapping JSON path keys to filter operators");
|
|
7
|
+
for (const [key, operatorObj] of Object.entries(value)) if (key.trim().length === 0) throw new Error("json filter: keys must not be empty");
|
|
8
|
+
else if (key === "_or" || key === "_and") {
|
|
9
|
+
if (!Array.isArray(operatorObj)) throw new Error(`json filter: "${key}" must be an array of filter objects`);
|
|
10
|
+
} else if (typeof operatorObj !== "object" || operatorObj === null || Array.isArray(operatorObj)) throw new Error(`json filter: "${key}" must be a filter operator object (e.g. { "_eq": "value" })`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A scalar representing a JSON filter value: a plain object mapping JSON path keys
|
|
15
|
+
* to filter operator objects (e.g. { "color": { "_eq": "red" }, "size": { "_gt": 5 } }).
|
|
16
|
+
*/
|
|
17
|
+
const GraphQLJsonFilter = new GraphQLScalarType({
|
|
18
|
+
...GraphQLJSON,
|
|
19
|
+
name: "GraphQLJsonFilter",
|
|
20
|
+
description: "A JSON filter value: a plain object mapping JSON path keys to filter operators (e.g. { \"key\": { \"_eq\": \"value\" } })",
|
|
21
|
+
parseValue(value) {
|
|
22
|
+
return validateJsonFilterValue(value);
|
|
23
|
+
},
|
|
24
|
+
parseLiteral(ast, variables) {
|
|
25
|
+
return validateJsonFilterValue(GraphQLJSON.parseLiteral(ast, variables));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { GraphQLJsonFilter };
|
package/dist/services/index.js
CHANGED
|
@@ -4,22 +4,22 @@ import { ItemsService } from "./items.js";
|
|
|
4
4
|
import { FilesService } from "./files.js";
|
|
5
5
|
import { FoldersService } from "./folders.js";
|
|
6
6
|
import { AssetsService } from "./assets.js";
|
|
7
|
-
import { RelationsService } from "./relations.js";
|
|
8
|
-
import { FieldsService, systemFieldUpdateSchema } from "./fields.js";
|
|
9
|
-
import { CollectionsService } from "./collections.js";
|
|
10
|
-
import { ActivityService } from "./activity.js";
|
|
11
7
|
import { AccessService } from "./access.js";
|
|
8
|
+
import { ActivityService } from "./activity.js";
|
|
12
9
|
import { MailService } from "./mail/index.js";
|
|
10
|
+
import { RelationsService } from "./relations.js";
|
|
11
|
+
import { FlowsService } from "./flows.js";
|
|
12
|
+
import { RevisionsService } from "./revisions.js";
|
|
13
13
|
import { ExtensionReadError, ExtensionsService } from "./extensions.js";
|
|
14
14
|
import { SettingsService } from "./settings.js";
|
|
15
15
|
import { UsersService } from "./users.js";
|
|
16
16
|
import { NotificationsService } from "./notifications.js";
|
|
17
17
|
import { ExportService, ImportService, createErrorTracker, getHeadingsForCsvExport } from "./import-export.js";
|
|
18
|
-
import { RevisionsService } from "./revisions.js";
|
|
19
18
|
import { TFAService } from "./tfa.js";
|
|
20
19
|
import { AuthenticationService } from "./authentication.js";
|
|
21
20
|
import { CommentsService } from "./comments.js";
|
|
22
21
|
import { DashboardsService } from "./dashboards.js";
|
|
22
|
+
import { FieldsService, systemFieldUpdateSchema } from "./fields.js";
|
|
23
23
|
import { DeploymentProjectsService } from "./deployment-projects.js";
|
|
24
24
|
import { DeploymentRunsService } from "./deployment-runs.js";
|
|
25
25
|
import { DeploymentService } from "./deployment.js";
|
|
@@ -39,7 +39,7 @@ import { SharesService } from "./shares.js";
|
|
|
39
39
|
import { TranslationsService } from "./translations.js";
|
|
40
40
|
import { VersionsService } from "./versions.js";
|
|
41
41
|
import { WebSocketService } from "./websocket.js";
|
|
42
|
-
import {
|
|
42
|
+
import { CollectionsService } from "./collections.js";
|
|
43
43
|
|
|
44
44
|
//#region src/services/index.ts
|
|
45
45
|
var services_exports = /* @__PURE__ */ __export({
|
package/dist/services/items.js
CHANGED
|
@@ -13,13 +13,13 @@ import { runAst } from "../database/run-ast/run-ast.js";
|
|
|
13
13
|
import { processPayload } from "../permissions/modules/process-payload/process-payload.js";
|
|
14
14
|
import { shouldClearCache } from "../utils/should-clear-cache.js";
|
|
15
15
|
import { validateKeys } from "../utils/validate-keys.js";
|
|
16
|
-
import { validateUserCountIntegrity } from "../utils/validate-user-count-integrity.js";
|
|
16
|
+
import { captureSeatCount, validateUserCountIntegrity } from "../utils/validate-user-count-integrity.js";
|
|
17
17
|
import { handleVersion } from "../utils/versioning/handle-version.js";
|
|
18
18
|
import { useEnv } from "@directus/env";
|
|
19
19
|
import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from "@directus/errors";
|
|
20
20
|
import { getRelationsForCollection } from "@directus/utils";
|
|
21
21
|
import { assign, clone, cloneDeep, difference, omit, pick, without } from "lodash-es";
|
|
22
|
-
import { Action } from "@directus/constants";
|
|
22
|
+
import { Action, isPublishedVersionKey } from "@directus/constants";
|
|
23
23
|
import { isSystemCollection } from "@directus/system-data";
|
|
24
24
|
|
|
25
25
|
//#region src/services/items.ts
|
|
@@ -86,6 +86,7 @@ var ItemsService = class ItemsService {
|
|
|
86
86
|
async createOne(data, opts = {}) {
|
|
87
87
|
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
|
|
88
88
|
if (!opts.bypassLimits) opts.mutationTracker.trackMutations(1);
|
|
89
|
+
if (this.collection === "directus_users") opts.userIntegrityCheckFlags = (opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None) | UserIntegrityCheckFlag.UserLimits;
|
|
89
90
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
90
91
|
const fields = Object.keys(this.schema.collections[this.collection].fields);
|
|
91
92
|
const aliases = Object.values(this.schema.collections[this.collection].fields).filter((field) => field.alias === true).map((field) => field.field);
|
|
@@ -99,6 +100,7 @@ var ItemsService = class ItemsService {
|
|
|
99
100
|
* update tree
|
|
100
101
|
*/
|
|
101
102
|
const primaryKey = await transaction(this.knex, async (trx) => {
|
|
103
|
+
const previousSeatCount = await captureSeatCount(trx, opts.userIntegrityCheckFlags);
|
|
102
104
|
const payloadAfterHooks = opts.emitEvents !== false ? await emitter_default.emitFilter(this.eventScope === "items" ? ["items.create", `${this.collection}.items.create`] : `${this.eventScope}.create`, payload, { collection: this.collection }, {
|
|
103
105
|
database: trx,
|
|
104
106
|
schema: this.schema,
|
|
@@ -159,7 +161,8 @@ var ItemsService = class ItemsService {
|
|
|
159
161
|
if (userIntegrityCheckFlags) if (opts.onRequireUserIntegrityCheck) opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
|
|
160
162
|
else await validateUserCountIntegrity({
|
|
161
163
|
flags: userIntegrityCheckFlags,
|
|
162
|
-
knex: trx
|
|
164
|
+
knex: trx,
|
|
165
|
+
previousSeatCount
|
|
163
166
|
});
|
|
164
167
|
if (opts.skipTracking !== true && this.accountability && this.schema.collections[this.collection].accountability !== null) {
|
|
165
168
|
const { ActivityService } = await import("./activity.js");
|
|
@@ -232,7 +235,9 @@ var ItemsService = class ItemsService {
|
|
|
232
235
|
*/
|
|
233
236
|
async createMany(data, opts = {}) {
|
|
234
237
|
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
|
|
238
|
+
if (this.collection === "directus_users") opts.userIntegrityCheckFlags = (opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None) | UserIntegrityCheckFlag.UserLimits;
|
|
235
239
|
const { primaryKeys, nestedActionEvents } = await transaction(this.knex, async (knex) => {
|
|
240
|
+
const previousSeatCount = await captureSeatCount(knex, opts.userIntegrityCheckFlags);
|
|
236
241
|
const service = this.fork({ knex });
|
|
237
242
|
let userIntegrityCheckFlags = opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None;
|
|
238
243
|
const primaryKeys$1 = [];
|
|
@@ -255,7 +260,8 @@ var ItemsService = class ItemsService {
|
|
|
255
260
|
if (userIntegrityCheckFlags) if (opts.onRequireUserIntegrityCheck) opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
|
|
256
261
|
else await validateUserCountIntegrity({
|
|
257
262
|
flags: userIntegrityCheckFlags,
|
|
258
|
-
knex
|
|
263
|
+
knex,
|
|
264
|
+
previousSeatCount
|
|
259
265
|
});
|
|
260
266
|
return {
|
|
261
267
|
primaryKeys: primaryKeys$1,
|
|
@@ -271,6 +277,7 @@ var ItemsService = class ItemsService {
|
|
|
271
277
|
* Get items by query.
|
|
272
278
|
*/
|
|
273
279
|
async readByQuery(query, opts) {
|
|
280
|
+
if (query.version && !isPublishedVersionKey(query.version)) return await handleVersion(this, opts?.key ?? null, query, opts);
|
|
274
281
|
const updatedQuery = opts?.emitEvents !== false ? await emitter_default.emitFilter(this.eventScope === "items" ? ["items.query", `${this.collection}.items.query`] : `${this.eventScope}.query`, query, { collection: this.collection }, {
|
|
275
282
|
database: this.knex,
|
|
276
283
|
schema: this.schema,
|
|
@@ -325,9 +332,10 @@ var ItemsService = class ItemsService {
|
|
|
325
332
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
326
333
|
validateKeys(this.schema, this.collection, primaryKeyField, key);
|
|
327
334
|
const queryWithKey = assign({}, query, { filter: assign({}, query.filter, { [primaryKeyField]: { _eq: key } }) });
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
335
|
+
const results = await this.readByQuery(queryWithKey, {
|
|
336
|
+
...opts,
|
|
337
|
+
key
|
|
338
|
+
});
|
|
331
339
|
if (results.length === 0) throw new ForbiddenError();
|
|
332
340
|
return results[0];
|
|
333
341
|
}
|
|
@@ -373,6 +381,7 @@ var ItemsService = class ItemsService {
|
|
|
373
381
|
const keys = [];
|
|
374
382
|
try {
|
|
375
383
|
await transaction(this.knex, async (knex) => {
|
|
384
|
+
const previousSeatCount = await captureSeatCount(knex, opts.userIntegrityCheckFlags);
|
|
376
385
|
const service = this.fork({ knex });
|
|
377
386
|
let userIntegrityCheckFlags = opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None;
|
|
378
387
|
for (const index in data) {
|
|
@@ -390,7 +399,8 @@ var ItemsService = class ItemsService {
|
|
|
390
399
|
if (userIntegrityCheckFlags) if (opts.onRequireUserIntegrityCheck) opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
|
|
391
400
|
else await validateUserCountIntegrity({
|
|
392
401
|
flags: userIntegrityCheckFlags,
|
|
393
|
-
knex
|
|
402
|
+
knex,
|
|
403
|
+
previousSeatCount
|
|
394
404
|
});
|
|
395
405
|
});
|
|
396
406
|
} finally {
|
|
@@ -404,6 +414,7 @@ var ItemsService = class ItemsService {
|
|
|
404
414
|
async updateMany(keys, data, opts = {}) {
|
|
405
415
|
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
|
|
406
416
|
if (!opts.bypassLimits) opts.mutationTracker.trackMutations(keys.length);
|
|
417
|
+
if (this.collection === "directus_users" && data["status"] === "active") opts.userIntegrityCheckFlags = (opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None) | UserIntegrityCheckFlag.UserLimits;
|
|
407
418
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
408
419
|
validateKeys(this.schema, this.collection, primaryKeyField, keys);
|
|
409
420
|
const fields = Object.keys(this.schema.collections[this.collection].fields);
|
|
@@ -441,6 +452,7 @@ var ItemsService = class ItemsService {
|
|
|
441
452
|
}) : payloadAfterHooks;
|
|
442
453
|
if (opts.preMutationError) throw opts.preMutationError;
|
|
443
454
|
await transaction(this.knex, async (trx) => {
|
|
455
|
+
const previousSeatCount = await captureSeatCount(trx, opts.userIntegrityCheckFlags);
|
|
444
456
|
const payloadService = new PayloadService(this.collection, {
|
|
445
457
|
accountability: this.accountability,
|
|
446
458
|
knex: trx,
|
|
@@ -470,7 +482,8 @@ var ItemsService = class ItemsService {
|
|
|
470
482
|
if (userIntegrityCheckFlags) if (opts?.onRequireUserIntegrityCheck) opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
|
|
471
483
|
else await validateUserCountIntegrity({
|
|
472
484
|
flags: userIntegrityCheckFlags,
|
|
473
|
-
knex: trx
|
|
485
|
+
knex: trx,
|
|
486
|
+
previousSeatCount
|
|
474
487
|
});
|
|
475
488
|
if (opts.skipTracking !== true && this.accountability && this.schema.collections[this.collection].accountability !== null) {
|
|
476
489
|
const { ActivityService } = await import("./activity.js");
|
|
@@ -622,11 +635,13 @@ var ItemsService = class ItemsService {
|
|
|
622
635
|
});
|
|
623
636
|
if (opts.preMutationError) throw opts.preMutationError;
|
|
624
637
|
await transaction(this.knex, async (trx) => {
|
|
638
|
+
const previousSeatCount = await captureSeatCount(trx, opts.userIntegrityCheckFlags);
|
|
625
639
|
await trx(this.collection).whereIn(primaryKeyField, keysAfterHooks).delete();
|
|
626
640
|
if (opts.userIntegrityCheckFlags) if (opts.onRequireUserIntegrityCheck) opts.onRequireUserIntegrityCheck(opts.userIntegrityCheckFlags);
|
|
627
641
|
else await validateUserCountIntegrity({
|
|
628
642
|
flags: opts.userIntegrityCheckFlags,
|
|
629
|
-
knex: trx
|
|
643
|
+
knex: trx,
|
|
644
|
+
previousSeatCount
|
|
630
645
|
});
|
|
631
646
|
if (opts.skipTracking !== true && this.accountability && this.schema.collections[this.collection].accountability !== null) {
|
|
632
647
|
const { ActivityService } = await import("./activity.js");
|
|
@@ -670,12 +685,15 @@ var ItemsService = class ItemsService {
|
|
|
670
685
|
async readSingleton(query, opts) {
|
|
671
686
|
query = clone(query);
|
|
672
687
|
query.limit = 1;
|
|
673
|
-
|
|
674
|
-
if (query.version && query.version !== "main") {
|
|
688
|
+
if (query.version && !isPublishedVersionKey(query.version)) {
|
|
675
689
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
676
690
|
const key = (await this.knex.select(primaryKeyField).from(this.collection).first())?.[primaryKeyField];
|
|
677
|
-
|
|
678
|
-
|
|
691
|
+
opts = {
|
|
692
|
+
...opts,
|
|
693
|
+
key
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const record = (await this.readByQuery(query, opts))[0];
|
|
679
697
|
if (!record) {
|
|
680
698
|
let fields = Object.entries(this.schema.collections[this.collection].fields);
|
|
681
699
|
const defaults = {};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { useLogger } from "../../logger/index.js";
|
|
2
|
+
import { getAxios } from "../../request/index.js";
|
|
3
|
+
import { OAuthError } from "./types/error.js";
|
|
4
|
+
import { CimdEgressError, createCimdLookup } from "./utils/cimd-egress.js";
|
|
5
|
+
import { validateRedirectUri } from "./utils/redirect.js";
|
|
6
|
+
import { useEnv } from "@directus/env";
|
|
7
|
+
import { isIP } from "node:net";
|
|
8
|
+
import { performance } from "node:perf_hooks";
|
|
9
|
+
|
|
10
|
+
//#region src/services/mcp-oauth/cimd.ts
|
|
11
|
+
const MIN_TTL_MS = 3e5;
|
|
12
|
+
const MAX_TTL_MS = 864e5;
|
|
13
|
+
const DEFAULT_TTL_MS = 36e5;
|
|
14
|
+
const MAX_CLIENT_NAME_LENGTH = 200;
|
|
15
|
+
const MAX_URL_LENGTH = 255;
|
|
16
|
+
const DEFAULT_BLOCKED_TLDS = [
|
|
17
|
+
"test",
|
|
18
|
+
"localhost",
|
|
19
|
+
"invalid",
|
|
20
|
+
"example",
|
|
21
|
+
"local",
|
|
22
|
+
"onion"
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Forbidden shared-secret auth methods per draft-ietf-oauth-client-id-metadata-document-01 Section 4.1:
|
|
26
|
+
* "the token_endpoint_auth_method property MUST NOT include client_secret_post, client_secret_basic,
|
|
27
|
+
* client_secret_jwt, or any other method based around a shared symmetric secret."
|
|
28
|
+
*/
|
|
29
|
+
const FORBIDDEN_AUTH_METHODS = new Set([
|
|
30
|
+
"client_secret_basic",
|
|
31
|
+
"client_secret_post",
|
|
32
|
+
"client_secret_jwt"
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Detect whether a client_id is a CIMD URL or a DCR-registered ID.
|
|
36
|
+
* Returns 'cimd' for valid CIMD URLs, 'dcr' for anything else that should go to DB lookup,
|
|
37
|
+
* or null if it looks like a CIMD URL but fails validation.
|
|
38
|
+
*/
|
|
39
|
+
function detectClientIdType(clientId) {
|
|
40
|
+
if (clientId.startsWith("https://") || useEnv()["MCP_OAUTH_CIMD_ALLOW_HTTP"] && clientId.startsWith("http://")) return isValidCimdClientId(clientId) ? "cimd" : null;
|
|
41
|
+
return "dcr";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Strict CIMD client_id URL validation with debug logging on each rejection.
|
|
45
|
+
*/
|
|
46
|
+
function isValidCimdClientId(input) {
|
|
47
|
+
const logger = useLogger();
|
|
48
|
+
const env = useEnv();
|
|
49
|
+
let url;
|
|
50
|
+
try {
|
|
51
|
+
url = new URL(input);
|
|
52
|
+
} catch {
|
|
53
|
+
logger.debug({
|
|
54
|
+
client_id: input,
|
|
55
|
+
reason: "unparseable URL"
|
|
56
|
+
}, "CIMD client_id rejected");
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const allowHttp = env["MCP_OAUTH_CIMD_ALLOW_HTTP"];
|
|
60
|
+
if (url.protocol !== "https:" && !(allowHttp && url.protocol === "http:")) {
|
|
61
|
+
logger.debug({
|
|
62
|
+
client_id: input,
|
|
63
|
+
reason: "not HTTPS"
|
|
64
|
+
}, "CIMD client_id rejected");
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (url.pathname === "/") {
|
|
68
|
+
logger.debug({
|
|
69
|
+
client_id: input,
|
|
70
|
+
reason: "root path"
|
|
71
|
+
}, "CIMD client_id rejected");
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (url.search) {
|
|
75
|
+
logger.debug({
|
|
76
|
+
client_id: input,
|
|
77
|
+
reason: "query string present"
|
|
78
|
+
}, "CIMD client_id rejected");
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (url.hash) {
|
|
82
|
+
logger.debug({
|
|
83
|
+
client_id: input,
|
|
84
|
+
reason: "fragment present"
|
|
85
|
+
}, "CIMD client_id rejected");
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (url.username || url.password) {
|
|
89
|
+
logger.debug({
|
|
90
|
+
client_id: input,
|
|
91
|
+
reason: "credentials in URL"
|
|
92
|
+
}, "CIMD client_id rejected");
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (/(?:^|\/)\.\.?(?:\/|$)/.test(url.pathname)) {
|
|
96
|
+
logger.debug({
|
|
97
|
+
client_id: input,
|
|
98
|
+
reason: "dot segments in path"
|
|
99
|
+
}, "CIMD client_id rejected");
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const hostname = url.hostname.replace(/\.$/, "").toLowerCase();
|
|
103
|
+
if (isIP(hostname.replace(/^\[|\]$/g, ""))) {
|
|
104
|
+
logger.debug({
|
|
105
|
+
client_id: input,
|
|
106
|
+
reason: "IP address hostname"
|
|
107
|
+
}, "CIMD client_id rejected");
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const envTlds = env["MCP_OAUTH_CIMD_BLOCKED_TLDS"];
|
|
111
|
+
const suffixes = (envTlds.length > 0 ? envTlds : DEFAULT_BLOCKED_TLDS).map((t) => `.${t.toLowerCase()}`);
|
|
112
|
+
for (const suffix of suffixes) if (hostname === suffix.slice(1) || hostname.endsWith(suffix)) {
|
|
113
|
+
logger.debug({
|
|
114
|
+
client_id: input,
|
|
115
|
+
reason: `blocked TLD: ${suffix}`
|
|
116
|
+
}, "CIMD client_id rejected");
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (input.length > MAX_URL_LENGTH) {
|
|
120
|
+
logger.debug({
|
|
121
|
+
client_id: input,
|
|
122
|
+
reason: "URL too long"
|
|
123
|
+
}, "CIMD client_id rejected");
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (url.href !== input) {
|
|
127
|
+
logger.debug({
|
|
128
|
+
client_id: input,
|
|
129
|
+
reason: `non-canonical (${url.href})`
|
|
130
|
+
}, "CIMD client_id rejected");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/** Read allowed domains from env, filtering empty strings. */
|
|
136
|
+
function getAllowedDomains() {
|
|
137
|
+
return useEnv()["MCP_OAUTH_CIMD_ALLOWED_DOMAINS"].filter((s) => s !== "");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Parse Cache-Control and Expires headers per RFC 9111.
|
|
141
|
+
* Returns TTL in milliseconds, clamped to [5min, 24h]. Default: 1h.
|
|
142
|
+
*/
|
|
143
|
+
function resolveCacheTtl(headers) {
|
|
144
|
+
const cc = headers["cache-control"];
|
|
145
|
+
if (cc) {
|
|
146
|
+
const directives = cc.split(",").map((d) => d.trim());
|
|
147
|
+
for (const directive of directives) {
|
|
148
|
+
const eqIdx = directive.indexOf("=");
|
|
149
|
+
const name = (eqIdx === -1 ? directive : directive.slice(0, eqIdx)).toLowerCase();
|
|
150
|
+
if (name === "no-store" && eqIdx === -1) return 0;
|
|
151
|
+
if (name === "no-cache" && eqIdx === -1) return 0;
|
|
152
|
+
}
|
|
153
|
+
for (const directive of directives) {
|
|
154
|
+
const eqIdx = directive.indexOf("=");
|
|
155
|
+
if (eqIdx === -1) continue;
|
|
156
|
+
if (directive.slice(0, eqIdx).toLowerCase() === "max-age") {
|
|
157
|
+
const seconds = parseInt(directive.slice(eqIdx + 1), 10);
|
|
158
|
+
if (!isNaN(seconds)) {
|
|
159
|
+
const ms = seconds * 1e3;
|
|
160
|
+
return Math.min(MAX_TTL_MS, Math.max(MIN_TTL_MS, ms));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const expires = headers["expires"];
|
|
166
|
+
if (expires) {
|
|
167
|
+
const expiresMs = new Date(expires).getTime();
|
|
168
|
+
if (!isNaN(expiresMs)) {
|
|
169
|
+
const delta = expiresMs - Date.now();
|
|
170
|
+
if (delta <= 0) return 0;
|
|
171
|
+
return Math.min(MAX_TTL_MS, Math.max(MIN_TTL_MS, delta));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return DEFAULT_TTL_MS;
|
|
175
|
+
}
|
|
176
|
+
/** Validate a URI is HTTPS. Used for optional metadata fields (client_uri, logo_uri, etc.). */
|
|
177
|
+
function validateOptionalHttpsUri(value, field) {
|
|
178
|
+
if (typeof value !== "string") throw new OAuthError(400, "invalid_client_metadata", `${field} must be a string`);
|
|
179
|
+
try {
|
|
180
|
+
if (new URL(value).protocol !== "https:") throw new OAuthError(400, "invalid_client_metadata", `${field} must use HTTPS`);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err instanceof OAuthError) throw err;
|
|
183
|
+
throw new OAuthError(400, "invalid_client_metadata", `${field} is not a valid URL`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const CONTENT_TYPE_JSON_RE = /^application\/(?:json|[\w.-]+\+json)(?:\s*;|$)/i;
|
|
187
|
+
/**
|
|
188
|
+
* Fetch and validate a CIMD metadata document.
|
|
189
|
+
* When `etag` is provided, sends If-None-Match and accepts 304.
|
|
190
|
+
*/
|
|
191
|
+
async function fetchCimdMetadata(clientId, etag) {
|
|
192
|
+
const logger = useLogger();
|
|
193
|
+
const axios = await getAxios();
|
|
194
|
+
let url;
|
|
195
|
+
try {
|
|
196
|
+
url = new URL(clientId);
|
|
197
|
+
} catch {
|
|
198
|
+
logger.debug({
|
|
199
|
+
client_id: clientId,
|
|
200
|
+
reason: "unparseable URL"
|
|
201
|
+
}, "CIMD metadata fetch rejected");
|
|
202
|
+
throw new OAuthError(400, "invalid_client_metadata", "Failed to fetch client metadata document");
|
|
203
|
+
}
|
|
204
|
+
if (isIP(url.hostname.replace(/^\[|\]$/g, ""))) {
|
|
205
|
+
logger.debug({
|
|
206
|
+
client_id: clientId,
|
|
207
|
+
reason: "IP address hostname"
|
|
208
|
+
}, "CIMD metadata fetch rejected");
|
|
209
|
+
throw new OAuthError(400, "invalid_client_metadata", "Failed to fetch client metadata document");
|
|
210
|
+
}
|
|
211
|
+
const headers = {};
|
|
212
|
+
if (etag) headers["If-None-Match"] = etag;
|
|
213
|
+
let response;
|
|
214
|
+
try {
|
|
215
|
+
const requestConfig = {
|
|
216
|
+
headers,
|
|
217
|
+
lookup: createCimdLookup({ deadlineAt: performance.now() + 3e3 }),
|
|
218
|
+
maxRedirects: 0,
|
|
219
|
+
maxContentLength: 5120,
|
|
220
|
+
proxy: false,
|
|
221
|
+
timeout: 3e3,
|
|
222
|
+
responseType: "json",
|
|
223
|
+
validateStatus: (s) => s === 200 || (etag ? s === 304 : false)
|
|
224
|
+
};
|
|
225
|
+
response = await axios.get(clientId, requestConfig);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (err instanceof CimdEgressError) {
|
|
228
|
+
const reason = err.reason;
|
|
229
|
+
logger.debug({
|
|
230
|
+
client_id: clientId,
|
|
231
|
+
...typeof reason === "string" ? { reason } : {}
|
|
232
|
+
}, "CIMD metadata fetch rejected");
|
|
233
|
+
throw new OAuthError(400, "invalid_client_metadata", "Failed to fetch client metadata document");
|
|
234
|
+
}
|
|
235
|
+
logger.debug({
|
|
236
|
+
client_id: clientId,
|
|
237
|
+
error: err?.message
|
|
238
|
+
}, "CIMD metadata fetch failed");
|
|
239
|
+
throw new OAuthError(400, "invalid_client_metadata", "Failed to fetch client metadata document");
|
|
240
|
+
}
|
|
241
|
+
if (response.status === 304) {
|
|
242
|
+
const respHeaders = response.headers;
|
|
243
|
+
return {
|
|
244
|
+
notModified: true,
|
|
245
|
+
ttlMs: !!respHeaders["cache-control"] ? resolveCacheTtl(respHeaders) : null
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const contentType = response.headers["content-type"];
|
|
249
|
+
if (!contentType || !CONTENT_TYPE_JSON_RE.test(contentType)) throw new OAuthError(400, "invalid_client_metadata", "Client metadata document must be JSON");
|
|
250
|
+
const doc = response.data;
|
|
251
|
+
if (!doc || typeof doc !== "object") throw new OAuthError(400, "invalid_client_metadata", "Client metadata document must be a JSON object");
|
|
252
|
+
if (doc.client_id !== clientId) {
|
|
253
|
+
logger.debug({
|
|
254
|
+
client_id: clientId,
|
|
255
|
+
doc_client_id: doc.client_id
|
|
256
|
+
}, "CIMD client_id mismatch");
|
|
257
|
+
throw new OAuthError(400, "invalid_client_metadata", "client_id in document does not match fetch URL");
|
|
258
|
+
}
|
|
259
|
+
if (!doc.client_name || typeof doc.client_name !== "string" || doc.client_name.trim().length === 0) throw new OAuthError(400, "invalid_client_metadata", "client_name is required");
|
|
260
|
+
if (doc.client_name.length > MAX_CLIENT_NAME_LENGTH) throw new OAuthError(400, "invalid_client_metadata", `client_name must not exceed ${MAX_CLIENT_NAME_LENGTH} characters`);
|
|
261
|
+
if (!Array.isArray(doc.redirect_uris) || doc.redirect_uris.length === 0) throw new OAuthError(400, "invalid_client_metadata", "redirect_uris is required and must be a non-empty array");
|
|
262
|
+
for (const uri of doc.redirect_uris) try {
|
|
263
|
+
validateRedirectUri(uri);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err instanceof OAuthError) throw err;
|
|
266
|
+
throw new OAuthError(400, "invalid_redirect_uri", "Invalid redirect URI in metadata document");
|
|
267
|
+
}
|
|
268
|
+
if ("client_secret" in doc) throw new OAuthError(400, "invalid_client_metadata", "CIMD documents must not contain client_secret");
|
|
269
|
+
if ("client_secret_expires_at" in doc) throw new OAuthError(400, "invalid_client_metadata", "CIMD documents must not contain client_secret_expires_at");
|
|
270
|
+
if (doc.grant_types !== void 0 && !Array.isArray(doc.grant_types)) throw new OAuthError(400, "invalid_client_metadata", "grant_types must be an array");
|
|
271
|
+
const grantTypes = doc.grant_types ?? ["authorization_code"];
|
|
272
|
+
if (!grantTypes.includes("authorization_code")) throw new OAuthError(400, "invalid_client_metadata", "grant_types must include authorization_code");
|
|
273
|
+
if (doc.response_types !== void 0) {
|
|
274
|
+
if (!Array.isArray(doc.response_types) || doc.response_types.length !== 1 || doc.response_types[0] !== "code") throw new OAuthError(400, "invalid_client_metadata", "response_types must be [\"code\"] if present");
|
|
275
|
+
}
|
|
276
|
+
if (doc.token_endpoint_auth_method !== void 0 && typeof doc.token_endpoint_auth_method !== "string") throw new OAuthError(400, "invalid_client_metadata", "token_endpoint_auth_method must be a string");
|
|
277
|
+
const authMethod = doc.token_endpoint_auth_method ?? "none";
|
|
278
|
+
if (FORBIDDEN_AUTH_METHODS.has(authMethod)) throw new OAuthError(400, "invalid_client_metadata", "Shared-secret authentication methods are forbidden for CIMD");
|
|
279
|
+
if (authMethod !== "none") throw new OAuthError(400, "invalid_client_metadata", `Unsupported token_endpoint_auth_method: ${authMethod}`);
|
|
280
|
+
for (const field of [
|
|
281
|
+
"client_uri",
|
|
282
|
+
"logo_uri",
|
|
283
|
+
"tos_uri",
|
|
284
|
+
"policy_uri"
|
|
285
|
+
]) if (doc[field] !== void 0) validateOptionalHttpsUri(doc[field], field);
|
|
286
|
+
const responseHeaders = response.headers;
|
|
287
|
+
return {
|
|
288
|
+
notModified: false,
|
|
289
|
+
metadata: {
|
|
290
|
+
client_id: doc.client_id,
|
|
291
|
+
client_name: doc.client_name,
|
|
292
|
+
redirect_uris: doc.redirect_uris,
|
|
293
|
+
grant_types: grantTypes,
|
|
294
|
+
response_types: doc.response_types,
|
|
295
|
+
token_endpoint_auth_method: authMethod,
|
|
296
|
+
...doc.client_uri && { client_uri: doc.client_uri },
|
|
297
|
+
...doc.logo_uri && { logo_uri: doc.logo_uri },
|
|
298
|
+
...doc.tos_uri && { tos_uri: doc.tos_uri },
|
|
299
|
+
...doc.policy_uri && { policy_uri: doc.policy_uri }
|
|
300
|
+
},
|
|
301
|
+
etag: responseHeaders["etag"] ?? null,
|
|
302
|
+
ttlMs: resolveCacheTtl(responseHeaders)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
export { detectClientIdType, fetchCimdMetadata, getAllowedDomains, isValidCimdClientId, resolveCacheTtl };
|