@directus/api 35.2.0 → 36.0.0-rc.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/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 +33 -9
- 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 -9
- 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/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 +34 -18
- package/dist/services/graphql/schema/get-types.js +23 -2
- 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 +21 -4
- package/dist/services/settings.js +37 -3
- package/dist/services/users.js +13 -6
- package/dist/services/utils.js +6 -1
- package/dist/services/versions.js +137 -69
- package/dist/utils/calculate-field-depth.js +1 -0
- 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/parse-oauth-scope.js +12 -0
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/split-field-path.js +29 -0
- 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 +130 -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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import database_default from "../../../database/index.js";
|
|
2
|
+
import { fetchAccessRoles } from "../../../utils/fetch-user-count/fetch-access-roles.js";
|
|
3
|
+
import { AccessService } from "../../../services/access.js";
|
|
4
|
+
import { getSchema } from "../../../utils/get-schema.js";
|
|
5
|
+
import { UsersService } from "../../../services/users.js";
|
|
6
|
+
import "../../../services/index.js";
|
|
7
|
+
import { toBoolean } from "@directus/utils";
|
|
8
|
+
import { USER_INACTIVE_LICENSE_STATUS } from "@directus/constants";
|
|
9
|
+
|
|
10
|
+
//#region src/license/entitlements/lib/seats.ts
|
|
11
|
+
async function getActiveSeats(opts) {
|
|
12
|
+
const knex = opts?.knex ?? database_default();
|
|
13
|
+
const schema = await getSchema({ database: knex });
|
|
14
|
+
const accessRows = await new AccessService({
|
|
15
|
+
schema,
|
|
16
|
+
knex
|
|
17
|
+
}).readByQuery({
|
|
18
|
+
fields: [
|
|
19
|
+
"role",
|
|
20
|
+
"user.id",
|
|
21
|
+
"user.status",
|
|
22
|
+
"user.role",
|
|
23
|
+
"policy.app_access",
|
|
24
|
+
"policy.admin_access"
|
|
25
|
+
],
|
|
26
|
+
limit: -1
|
|
27
|
+
});
|
|
28
|
+
const adminRoles = /* @__PURE__ */ new Set();
|
|
29
|
+
const appRoles = /* @__PURE__ */ new Set();
|
|
30
|
+
const adminUsers = /* @__PURE__ */ new Set();
|
|
31
|
+
const appUsers = /* @__PURE__ */ new Set();
|
|
32
|
+
for (const accessRow of accessRows) {
|
|
33
|
+
const isAdmin = toBoolean(accessRow["policy"]?.["admin_access"]);
|
|
34
|
+
const isApp = !isAdmin && toBoolean(accessRow["policy"]?.["app_access"]);
|
|
35
|
+
if (!isAdmin && !isApp) continue;
|
|
36
|
+
if (accessRow["user"] && accessRow["user"].status === "active") {
|
|
37
|
+
if (isAdmin) adminUsers.add(accessRow["user"].id);
|
|
38
|
+
else if (adminUsers.has(accessRow["user"].id) === false && adminRoles.has(accessRow["user"]?.role) === false) appUsers.add(accessRow["user"].id);
|
|
39
|
+
}
|
|
40
|
+
if (accessRow["role"]) if (isAdmin) adminRoles.add(accessRow["role"]);
|
|
41
|
+
else appRoles.add(accessRow["role"]);
|
|
42
|
+
}
|
|
43
|
+
const { adminRoles: allAdminRoles, appRoles: allAppRoles } = await fetchAccessRoles({
|
|
44
|
+
adminRoles,
|
|
45
|
+
appRoles
|
|
46
|
+
}, { knex });
|
|
47
|
+
const usersService = new UsersService({
|
|
48
|
+
schema,
|
|
49
|
+
knex
|
|
50
|
+
});
|
|
51
|
+
const adminFilters = [{ _or: [{ id: { _in: Array.from(adminUsers) } }, { role: { _in: Array.from(allAdminRoles) } }] }, { status: { _eq: "active" } }];
|
|
52
|
+
const appFilters = [{ _or: [{ id: {
|
|
53
|
+
_in: Array.from(appUsers),
|
|
54
|
+
_nin: Array.from(adminUsers)
|
|
55
|
+
} }, { role: {
|
|
56
|
+
_in: Array.from(allAppRoles),
|
|
57
|
+
_nin: Array.from(allAdminRoles)
|
|
58
|
+
} }] }, { status: { _eq: "active" } }];
|
|
59
|
+
if (opts?.adminId) {
|
|
60
|
+
adminFilters.push({ id: { _neq: opts.adminId } });
|
|
61
|
+
appFilters.push({ id: { _neq: opts.adminId } });
|
|
62
|
+
}
|
|
63
|
+
const adminCandidates = await usersService.readByQuery({
|
|
64
|
+
fields: [
|
|
65
|
+
"id",
|
|
66
|
+
"first_name",
|
|
67
|
+
"last_name",
|
|
68
|
+
"avatar",
|
|
69
|
+
"email"
|
|
70
|
+
],
|
|
71
|
+
filter: { _and: adminFilters },
|
|
72
|
+
limit: -1
|
|
73
|
+
});
|
|
74
|
+
return [...await usersService.readByQuery({
|
|
75
|
+
fields: [
|
|
76
|
+
"id",
|
|
77
|
+
"first_name",
|
|
78
|
+
"last_name",
|
|
79
|
+
"avatar",
|
|
80
|
+
"email"
|
|
81
|
+
],
|
|
82
|
+
filter: { _and: appFilters },
|
|
83
|
+
limit: -1
|
|
84
|
+
}), ...adminCandidates.map((admin) => ({
|
|
85
|
+
...admin,
|
|
86
|
+
admin: true
|
|
87
|
+
}))];
|
|
88
|
+
}
|
|
89
|
+
async function countActiveSeats(opts) {
|
|
90
|
+
return (await getActiveSeats(opts)).length;
|
|
91
|
+
}
|
|
92
|
+
async function resolveSeats(seats, ctx) {
|
|
93
|
+
if (!ctx?.accountability?.user) return;
|
|
94
|
+
const usersService = new UsersService({
|
|
95
|
+
schema: await getSchema(),
|
|
96
|
+
accountability: ctx.accountability
|
|
97
|
+
});
|
|
98
|
+
const users = seats.filter((user_id) => user_id !== ctx.accountability.user);
|
|
99
|
+
await Promise.allSettled(users.map((user_id) => usersService.updateOne(user_id, { status: USER_INACTIVE_LICENSE_STATUS })));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { countActiveSeats, getActiveSeats, resolveSeats };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DEFAULT_AUTH_PROVIDER } from "../../../constants.js";
|
|
2
|
+
import database_default from "../../../database/index.js";
|
|
3
|
+
import { getSchema } from "../../../utils/get-schema.js";
|
|
4
|
+
import { UsersService } from "../../../services/users.js";
|
|
5
|
+
import "../../../services/index.js";
|
|
6
|
+
import { USER_INACTIVE_LICENSE_STATUS } from "@directus/constants";
|
|
7
|
+
|
|
8
|
+
//#region src/license/entitlements/lib/sso-enabled.ts
|
|
9
|
+
/**
|
|
10
|
+
* Counting the current amount of users with sso enabled
|
|
11
|
+
*/
|
|
12
|
+
async function checkUsersSSO(opts) {
|
|
13
|
+
const knex = opts?.knex ?? database_default();
|
|
14
|
+
return (await new UsersService({
|
|
15
|
+
schema: await getSchema({ database: knex }),
|
|
16
|
+
knex
|
|
17
|
+
}).readByQuery({
|
|
18
|
+
fields: ["id"],
|
|
19
|
+
filter: {
|
|
20
|
+
provider: { _neq: DEFAULT_AUTH_PROVIDER },
|
|
21
|
+
status: { _eq: "active" }
|
|
22
|
+
}
|
|
23
|
+
})).length === 0;
|
|
24
|
+
}
|
|
25
|
+
async function resolveSSOUsers(resolution, ctx) {
|
|
26
|
+
if (!ctx?.accountability?.user) return;
|
|
27
|
+
const adminId = ctx.accountability.user;
|
|
28
|
+
const usersService = new UsersService({
|
|
29
|
+
schema: await getSchema(),
|
|
30
|
+
accountability: ctx?.accountability
|
|
31
|
+
});
|
|
32
|
+
await usersService.updateByQuery({ filter: { _and: [{ provider: {
|
|
33
|
+
_neq: DEFAULT_AUTH_PROVIDER,
|
|
34
|
+
_nnull: true
|
|
35
|
+
} }, { id: { _neq: adminId } }] } }, { status: USER_INACTIVE_LICENSE_STATUS });
|
|
36
|
+
if (typeof resolution === "object" && Object.keys(resolution.admin).length) {
|
|
37
|
+
const payload = { provider: DEFAULT_AUTH_PROVIDER };
|
|
38
|
+
if (resolution.admin.email?.length) payload["email"] = resolution.admin.email;
|
|
39
|
+
if (resolution.admin.password?.length) payload["password"] = resolution.admin.password;
|
|
40
|
+
await usersService.updateOne(adminId, payload);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { checkUsersSSO, resolveSSOUsers };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useBus } from "../../bus/lib/use-bus.js";
|
|
2
|
+
import "../../bus/index.js";
|
|
3
|
+
import { countActiveCollections, resolveCollections } from "./lib/collections.js";
|
|
4
|
+
import { checkCustomLLM } from "./lib/custom-llms-enabled.js";
|
|
5
|
+
import { checkCustomPermissionRules } from "./lib/custom-permission-rules-enabled.js";
|
|
6
|
+
import { countActiveFlows, resolveFlows } from "./lib/flows.js";
|
|
7
|
+
import { countActiveSeats, resolveSeats } from "./lib/seats.js";
|
|
8
|
+
import { checkUsersSSO, resolveSSOUsers } from "./lib/sso-enabled.js";
|
|
9
|
+
import { LimitExceededError, ResourceRestrictedError } from "@directus/errors";
|
|
10
|
+
import { CORE_LICENSE, COUNTABLE_ENTITLEMENT_KEYS, FEATURE_FLAG_ENTITLEMENT_KEYS } from "@directus/license";
|
|
11
|
+
|
|
12
|
+
//#region src/license/entitlements/manager.ts
|
|
13
|
+
const BUS_CHANNEL = "entitlements.invalidate";
|
|
14
|
+
let entitlementManager;
|
|
15
|
+
function getEntitlementManager() {
|
|
16
|
+
if (!entitlementManager) entitlementManager = new EntitlementManager();
|
|
17
|
+
return entitlementManager;
|
|
18
|
+
}
|
|
19
|
+
var EntitlementManager = class EntitlementManager {
|
|
20
|
+
entitlements = CORE_LICENSE["entitlements"];
|
|
21
|
+
counterSources = /* @__PURE__ */ new Map();
|
|
22
|
+
validatorSources = /* @__PURE__ */ new Map();
|
|
23
|
+
resolverSources = /* @__PURE__ */ new Map();
|
|
24
|
+
cache = /* @__PURE__ */ new Map();
|
|
25
|
+
initialized = false;
|
|
26
|
+
constructor() {
|
|
27
|
+
this.registerHandlers();
|
|
28
|
+
}
|
|
29
|
+
registerHandlers() {
|
|
30
|
+
this.registerCounter("collections", countActiveCollections);
|
|
31
|
+
this.registerCounter("seats", countActiveSeats);
|
|
32
|
+
this.registerCounter("flows", countActiveFlows);
|
|
33
|
+
this.registerValidator("sso_enabled", checkUsersSSO);
|
|
34
|
+
this.registerValidator("custom_llms_enabled", checkCustomLLM);
|
|
35
|
+
this.registerValidator("custom_permission_rules_enabled", checkCustomPermissionRules);
|
|
36
|
+
this.registerResolver("collections", resolveCollections);
|
|
37
|
+
this.registerResolver("seats", resolveSeats);
|
|
38
|
+
this.registerResolver("flows", resolveFlows);
|
|
39
|
+
this.registerResolver("sso_enabled", resolveSSOUsers);
|
|
40
|
+
}
|
|
41
|
+
initialize() {
|
|
42
|
+
if (this.initialized) return;
|
|
43
|
+
this.initialized = true;
|
|
44
|
+
useBus().subscribe(BUS_CHANNEL, async (msg) => {
|
|
45
|
+
this.clearCacheNoPublish(...msg?.keys ?? []);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Replace the active license. Pass `null` to reset to the core license.
|
|
50
|
+
*/
|
|
51
|
+
setEntitlements(entitlements) {
|
|
52
|
+
this.entitlements = entitlements ?? CORE_LICENSE["entitlements"];
|
|
53
|
+
this.clearCache();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create a manager that uses a different entitlement set while sharing
|
|
57
|
+
* this instance's cache and registered sources. Used to preview how a
|
|
58
|
+
* license change would affect the current user. Intended for read-only
|
|
59
|
+
* checks. Mutating methods (`setEntitlements`, `clearCache`) on the
|
|
60
|
+
* fork will affect the shared cache.
|
|
61
|
+
*/
|
|
62
|
+
fork(entitlements) {
|
|
63
|
+
const forked = Object.create(EntitlementManager.prototype);
|
|
64
|
+
forked.entitlements = entitlements ?? CORE_LICENSE["entitlements"];
|
|
65
|
+
forked.counterSources = this.counterSources;
|
|
66
|
+
forked.validatorSources = this.validatorSources;
|
|
67
|
+
forked.resolverSources = this.resolverSources;
|
|
68
|
+
forked.cache = this.cache;
|
|
69
|
+
return forked;
|
|
70
|
+
}
|
|
71
|
+
clearCacheNoPublish(...keys) {
|
|
72
|
+
if (keys.length === 0) this.cache.clear();
|
|
73
|
+
else for (const key of keys) this.cache.delete(key);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Drop cached usage/validity locally and notify other nodes. Pass specific
|
|
77
|
+
* keys to clear only those entries; call with no args to clear everything.
|
|
78
|
+
* Used by mutation paths (services) and by the manual cache-clear endpoint
|
|
79
|
+
* and CLI command.
|
|
80
|
+
*/
|
|
81
|
+
async clearCache(...keys) {
|
|
82
|
+
this.clearCacheNoPublish(...keys);
|
|
83
|
+
if (this.initialized) await useBus().publish(BUS_CHANNEL, { keys });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns a cached value by key
|
|
87
|
+
*/
|
|
88
|
+
getCached(key) {
|
|
89
|
+
return this.cache.get(key);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns whether a feature flag is enabled, applying `override` when
|
|
93
|
+
* present and falling back to `default` otherwise.
|
|
94
|
+
*/
|
|
95
|
+
isEntitled(key) {
|
|
96
|
+
const entitlement = this.entitlements[key];
|
|
97
|
+
return entitlement.override ?? entitlement.default;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Wire up a validator function for a feature flag entitlement.
|
|
101
|
+
*/
|
|
102
|
+
registerValidator(key, validator) {
|
|
103
|
+
if (this.validatorSources.has(key)) throw new Error(`Validator was already registered for entitlement "${String(key)}"`);
|
|
104
|
+
this.validatorSources.set(key, validator);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the validity of a feature flag by invoking its registered
|
|
108
|
+
* validator. Throws if no validator has been registered for `key`.
|
|
109
|
+
*/
|
|
110
|
+
async isValid(key, opts) {
|
|
111
|
+
const validator = this.validatorSources.get(key);
|
|
112
|
+
if (!validator) throw new Error(`No validator registered for entitlement "${String(key)}"`);
|
|
113
|
+
if (opts?.knex?.isTransaction) return await validator(opts);
|
|
114
|
+
let cached = this.cache.get(key);
|
|
115
|
+
if (typeof cached !== "boolean") {
|
|
116
|
+
cached = await validator(opts);
|
|
117
|
+
this.cache.set(key, cached);
|
|
118
|
+
}
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Returns the resolved values of app-only entitlements as a single bundle
|
|
123
|
+
* for exposure to the client. The package does not enforce these — the app
|
|
124
|
+
* uses them to adapt its UI (production indicator, powered-by branding).
|
|
125
|
+
*/
|
|
126
|
+
getAppEntitlements() {
|
|
127
|
+
const { production_enabled, display_powered_by, ai_translations_enabled } = this.entitlements;
|
|
128
|
+
return {
|
|
129
|
+
production_enabled: production_enabled.override ?? production_enabled.default,
|
|
130
|
+
ai_translations_enabled: ai_translations_enabled.override ?? ai_translations_enabled.default,
|
|
131
|
+
display_powered_by
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Returns the effective hard limit (`limit + overage + addon`) for a numeric
|
|
136
|
+
* entitlement with `-1` denoting unlimited
|
|
137
|
+
*/
|
|
138
|
+
getEntitlementLimit(key) {
|
|
139
|
+
const { limit, overage, addon } = this.entitlements[key];
|
|
140
|
+
if (limit === -1 || overage === -1 || addon === -1) return -1;
|
|
141
|
+
return limit + (overage ?? 0) + (addon ?? 0);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Wire up a usage counter function for a countable entitlement.
|
|
145
|
+
*/
|
|
146
|
+
registerCounter(key, source) {
|
|
147
|
+
if (this.counterSources.has(key)) throw new Error(`Counter was already registered for entitlement "${String(key)}"`);
|
|
148
|
+
this.counterSources.set(key, source);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Wire up a resolver function for an entitlement.
|
|
152
|
+
*/
|
|
153
|
+
registerResolver(key, source) {
|
|
154
|
+
if (this.resolverSources.has(key)) throw new Error(`Resolver was already registered for entitlement "${String(key)}"`);
|
|
155
|
+
this.resolverSources.set(key, source);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Resolve current usage for a countable entitlement by invoking the
|
|
159
|
+
* registered source. Throws if no source has been registered for `key`.
|
|
160
|
+
*/
|
|
161
|
+
async getUsage(key, opts) {
|
|
162
|
+
const source = this.counterSources.get(key);
|
|
163
|
+
if (!source) throw new Error(`No usage source registered for entitlement "${String(key)}"`);
|
|
164
|
+
if (opts?.knex?.isTransaction) return await source(opts);
|
|
165
|
+
let cached = this.cache.get(key);
|
|
166
|
+
if (typeof cached !== "number") {
|
|
167
|
+
cached = await source(opts);
|
|
168
|
+
this.cache.set(key, cached);
|
|
169
|
+
}
|
|
170
|
+
return cached;
|
|
171
|
+
}
|
|
172
|
+
async check(key, opts) {
|
|
173
|
+
if (this.isCountableKey(key)) {
|
|
174
|
+
const hardLimit = this.getEntitlementLimit(key);
|
|
175
|
+
if (hardLimit === -1) return {
|
|
176
|
+
allowed: true,
|
|
177
|
+
hardLimit: -1,
|
|
178
|
+
usage: 0,
|
|
179
|
+
remaining: null
|
|
180
|
+
};
|
|
181
|
+
const usage = await this.getUsage(key, { knex: opts?.knex });
|
|
182
|
+
const adding = opts?.adding ?? 0;
|
|
183
|
+
const removing = opts?.removing ?? 0;
|
|
184
|
+
return {
|
|
185
|
+
allowed: usage + adding - removing <= hardLimit,
|
|
186
|
+
hardLimit,
|
|
187
|
+
usage,
|
|
188
|
+
remaining: hardLimit - usage
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const entitled = this.isEntitled(key);
|
|
192
|
+
if (!entitled) return {
|
|
193
|
+
valid: await this.isValid(key, { knex: opts?.knex }),
|
|
194
|
+
entitled
|
|
195
|
+
};
|
|
196
|
+
else return {
|
|
197
|
+
valid: true,
|
|
198
|
+
entitled
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async assert(key, opts) {
|
|
202
|
+
if (this.isCountableKey(key)) {
|
|
203
|
+
const hardLimit = this.getEntitlementLimit(key);
|
|
204
|
+
if (hardLimit === -1) return;
|
|
205
|
+
const adding = opts?.adding ?? 0;
|
|
206
|
+
const removing = opts?.removing ?? 0;
|
|
207
|
+
if (await this.getUsage(key, { knex: opts?.knex }) + adding - removing > hardLimit) throw new LimitExceededError({ category: key });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!this.isEntitled(key) && !await this.isValid(key, { knex: opts?.knex })) throw new ResourceRestrictedError({ category: key });
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Checks all entitlements and returns true if all are within the limits
|
|
214
|
+
*/
|
|
215
|
+
async checkAll(opts) {
|
|
216
|
+
for (const key of COUNTABLE_ENTITLEMENT_KEYS) {
|
|
217
|
+
if (!this.counterSources.has(key)) continue;
|
|
218
|
+
const { allowed } = await this.check(key, opts);
|
|
219
|
+
if (!allowed) return false;
|
|
220
|
+
}
|
|
221
|
+
for (const key of FEATURE_FLAG_ENTITLEMENT_KEYS) {
|
|
222
|
+
if (!this.validatorSources.has(key) || !this.resolverSources.has(key)) continue;
|
|
223
|
+
const { valid } = await this.check(key, opts);
|
|
224
|
+
if (!valid) return false;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Asserts all entitlements and throws if a limit is breached
|
|
230
|
+
*/
|
|
231
|
+
async assertAll(opts) {
|
|
232
|
+
for (const key of COUNTABLE_ENTITLEMENT_KEYS) {
|
|
233
|
+
if (!this.counterSources.has(key)) continue;
|
|
234
|
+
await this.assert(key, opts);
|
|
235
|
+
}
|
|
236
|
+
for (const key of FEATURE_FLAG_ENTITLEMENT_KEYS) {
|
|
237
|
+
if (!this.validatorSources.has(key)) continue;
|
|
238
|
+
await this.assert(key, opts);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Apply a resolution payload to an entitlement by invoking its registered
|
|
243
|
+
* resolver
|
|
244
|
+
*/
|
|
245
|
+
async resolve(key, input, ctx) {
|
|
246
|
+
const source = this.resolverSources.get(key);
|
|
247
|
+
if (!source) throw new Error(`No resolver registered for entitlement "${String(key)}"`);
|
|
248
|
+
await source(input, ctx);
|
|
249
|
+
}
|
|
250
|
+
isCountableKey(key) {
|
|
251
|
+
return COUNTABLE_ENTITLEMENT_KEYS.includes(key);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
export { EntitlementManager, getEntitlementManager };
|