@directus/api 35.1.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.
Files changed (192) hide show
  1. package/dist/ai/chat/models/chat-request.js +48 -48
  2. package/dist/ai/chat/models/object-request.js +6 -6
  3. package/dist/ai/chat/models/providers.js +14 -14
  4. package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
  5. package/dist/ai/mcp/server.js +44 -6
  6. package/dist/ai/mcp/utils.js +31 -0
  7. package/dist/ai/tools/assets/index.js +3 -3
  8. package/dist/ai/tools/collections/index.js +18 -18
  9. package/dist/ai/tools/fields/index.js +18 -18
  10. package/dist/ai/tools/files/index.js +18 -18
  11. package/dist/ai/tools/flows/index.js +16 -16
  12. package/dist/ai/tools/folders/index.js +18 -18
  13. package/dist/ai/tools/items/index.js +17 -17
  14. package/dist/ai/tools/operations/index.js +16 -16
  15. package/dist/ai/tools/relations/index.js +22 -22
  16. package/dist/ai/tools/schema/index.js +3 -3
  17. package/dist/ai/tools/schema.js +159 -159
  18. package/dist/ai/tools/system/index.js +3 -3
  19. package/dist/ai/tools/trigger-flow/index.js +3 -3
  20. package/dist/app.js +33 -9
  21. package/dist/auth/drivers/ldap.js +3 -1
  22. package/dist/auth/drivers/local.js +2 -0
  23. package/dist/auth/drivers/oauth2.js +3 -1
  24. package/dist/auth/drivers/openid.js +3 -1
  25. package/dist/auth/drivers/saml.js +2 -0
  26. package/dist/auth/utils/check-local-disabled.js +16 -0
  27. package/dist/auth/utils/check-sso-enabled.js +14 -0
  28. package/dist/auth.js +8 -5
  29. package/dist/cli/commands/bootstrap/index.js +3 -0
  30. package/dist/cli/commands/cache/clear.js +6 -1
  31. package/dist/cli/commands/roles/create.js +4 -1
  32. package/dist/cli/commands/users/create.js +3 -0
  33. package/dist/constants.js +8 -1
  34. package/dist/controllers/access.js +1 -1
  35. package/dist/controllers/activity.js +2 -1
  36. package/dist/controllers/assets.js +45 -1
  37. package/dist/controllers/auth.js +13 -5
  38. package/dist/controllers/collections.js +1 -1
  39. package/dist/controllers/comments.js +1 -1
  40. package/dist/controllers/dashboards.js +1 -1
  41. package/dist/controllers/fields.js +1 -1
  42. package/dist/controllers/files.js +3 -1
  43. package/dist/controllers/flows.js +6 -5
  44. package/dist/controllers/folders.js +1 -1
  45. package/dist/controllers/graphql.js +2 -0
  46. package/dist/controllers/items.js +3 -1
  47. package/dist/controllers/license.js +119 -0
  48. package/dist/controllers/mcp/index.js +38 -0
  49. package/dist/controllers/mcp/oauth-clients.js +68 -0
  50. package/dist/controllers/mcp/oauth-consent-page.js +316 -0
  51. package/dist/controllers/mcp/oauth.js +381 -0
  52. package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
  53. package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
  54. package/dist/controllers/notifications.js +1 -1
  55. package/dist/controllers/operations.js +1 -1
  56. package/dist/controllers/panels.js +1 -1
  57. package/dist/controllers/permissions.js +1 -1
  58. package/dist/controllers/policies.js +1 -1
  59. package/dist/controllers/presets.js +1 -1
  60. package/dist/controllers/revisions.js +3 -2
  61. package/dist/controllers/roles.js +1 -1
  62. package/dist/controllers/schema.js +2 -2
  63. package/dist/controllers/server.js +38 -9
  64. package/dist/controllers/shares.js +1 -1
  65. package/dist/controllers/translations.js +1 -1
  66. package/dist/controllers/users.js +1 -1
  67. package/dist/controllers/utils.js +2 -2
  68. package/dist/controllers/versions.js +12 -5
  69. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
  70. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
  71. package/dist/database/helpers/fn/dialects/mysql.js +7 -12
  72. package/dist/database/helpers/fn/dialects/oracle.js +3 -4
  73. package/dist/database/helpers/fn/dialects/postgres.js +4 -26
  74. package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
  75. package/dist/database/helpers/fn/json/parse-function.js +14 -6
  76. package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
  77. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
  78. package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
  79. package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
  80. package/dist/database/migrations/20260507A-add-licensing.js +22 -0
  81. package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
  82. package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
  83. package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
  84. package/dist/database/run-ast/lib/apply-query/index.js +4 -1
  85. package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
  86. package/dist/database/run-ast/lib/get-db-query.js +21 -9
  87. package/dist/database/run-ast/lib/parse-current-level.js +2 -1
  88. package/dist/database/run-ast/run-ast.js +2 -1
  89. package/dist/database/run-ast/utils/get-column.js +2 -1
  90. package/dist/extensions/lib/installation/manager.js +3 -3
  91. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  92. package/dist/extensions/lib/sync/sync.js +2 -2
  93. package/dist/extensions/manager.js +5 -5
  94. package/dist/flows.js +12 -10
  95. package/dist/license/entitlements/lib/collections.js +37 -0
  96. package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
  97. package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
  98. package/dist/license/entitlements/lib/flows.js +29 -0
  99. package/dist/license/entitlements/lib/seats.js +103 -0
  100. package/dist/license/entitlements/lib/sso-enabled.js +45 -0
  101. package/dist/license/entitlements/manager.js +256 -0
  102. package/dist/license/index.js +4 -0
  103. package/dist/license/manager.js +505 -0
  104. package/dist/license/utils/compute-license-status.js +27 -0
  105. package/dist/license/utils/get-core-grace-expires-at.js +38 -0
  106. package/dist/license/utils/get-license-key.js +23 -0
  107. package/dist/license/utils/get-license-token.js +23 -0
  108. package/dist/license/utils/handle-license-error.js +41 -0
  109. package/dist/license/utils/is-in-core-grace-period.js +11 -0
  110. package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
  111. package/dist/license/utils/use-rpc.js +33 -0
  112. package/dist/middleware/cache.js +4 -1
  113. package/dist/middleware/error-handler.js +11 -0
  114. package/dist/middleware/extract-token.js +11 -2
  115. package/dist/middleware/is-admin.js +16 -0
  116. package/dist/middleware/is-locked.js +16 -0
  117. package/dist/middleware/mcp-oauth-guard.js +23 -0
  118. package/dist/middleware/request-counter.js +5 -2
  119. package/dist/packages/types/dist/index.js +117 -122
  120. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
  121. package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
  122. package/dist/request/is-denied-ip.js +2 -0
  123. package/dist/schedules/license.js +31 -0
  124. package/dist/schedules/oauth-cleanup.js +26 -0
  125. package/dist/schedules/retention.js +1 -1
  126. package/dist/schedules/telemetry.js +4 -1
  127. package/dist/schedules/tus.js +1 -1
  128. package/dist/schedules/utils/duration-to-cron.js +36 -0
  129. package/dist/services/activity.js +15 -0
  130. package/dist/services/authentication.js +12 -5
  131. package/dist/services/collections.js +41 -10
  132. package/dist/services/fields.js +6 -6
  133. package/dist/services/flows.js +12 -0
  134. package/dist/services/graphql/resolvers/system-admin.js +2 -2
  135. package/dist/services/graphql/resolvers/system-global.js +1 -1
  136. package/dist/services/graphql/resolvers/system.js +34 -18
  137. package/dist/services/graphql/schema/get-types.js +23 -2
  138. package/dist/services/graphql/schema/parse-query.js +8 -0
  139. package/dist/services/graphql/schema/read.js +12 -0
  140. package/dist/services/graphql/types/json-filter.js +30 -0
  141. package/dist/services/index.js +6 -6
  142. package/dist/services/items.js +32 -14
  143. package/dist/services/mcp-oauth/cimd.js +307 -0
  144. package/dist/services/mcp-oauth/index.js +1185 -0
  145. package/dist/services/mcp-oauth/types/error.js +22 -0
  146. package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
  147. package/dist/services/mcp-oauth/utils/domain.js +21 -0
  148. package/dist/services/mcp-oauth/utils/loopback.js +11 -0
  149. package/dist/services/mcp-oauth/utils/redirect.js +84 -0
  150. package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
  151. package/dist/services/payload.js +2 -1
  152. package/dist/services/permissions.js +31 -9
  153. package/dist/services/revisions.js +15 -0
  154. package/dist/services/schema.js +2 -2
  155. package/dist/services/server.js +21 -4
  156. package/dist/services/settings.js +37 -3
  157. package/dist/services/users.js +13 -6
  158. package/dist/services/utils.js +6 -1
  159. package/dist/services/versions.js +138 -69
  160. package/dist/utils/calculate-field-depth.js +1 -0
  161. package/dist/utils/deep-freeze.js +24 -0
  162. package/dist/utils/extract-function-name.js +13 -0
  163. package/dist/utils/generate-translations.js +5 -5
  164. package/dist/utils/get-accountability-for-token.js +13 -1
  165. package/dist/utils/get-cache-key.js +1 -1
  166. package/dist/utils/get-history-filter-query.js +22 -0
  167. package/dist/utils/get-schema.js +2 -2
  168. package/dist/utils/get-service.js +3 -3
  169. package/dist/utils/is-admin.js +9 -0
  170. package/dist/utils/parse-oauth-scope.js +12 -0
  171. package/dist/utils/sanitize-query.js +1 -1
  172. package/dist/utils/split-field-path.js +29 -0
  173. package/dist/utils/transaction.js +2 -2
  174. package/dist/utils/translations-validation.js +2 -2
  175. package/dist/utils/validate-diff.js +7 -3
  176. package/dist/utils/validate-query.js +35 -4
  177. package/dist/utils/validate-user-count-integrity.js +28 -5
  178. package/dist/utils/verify-session-jwt.js +5 -2
  179. package/dist/utils/versioning/handle-version.js +130 -48
  180. package/dist/utils/versioning/remove-circular.js +17 -0
  181. package/dist/websocket/authenticate.js +2 -1
  182. package/dist/websocket/collab/collab.js +1 -1
  183. package/dist/websocket/collab/room.js +1 -1
  184. package/dist/websocket/controllers/base.js +12 -0
  185. package/dist/websocket/controllers/graphql.js +1 -1
  186. package/dist/websocket/handlers/subscribe.js +1 -1
  187. package/dist/websocket/messages.js +64 -64
  188. package/dist/websocket/utils/items.js +2 -2
  189. package/license +90 -80
  190. package/package.json +33 -33
  191. package/dist/controllers/mcp.js +0 -31
  192. package/dist/utils/job-queue.js +0 -24
@@ -0,0 +1,505 @@
1
+ import { useLogger } from "../logger/index.js";
2
+ import { clearCache } from "../permissions/cache.js";
3
+ import schedule, { stopLicenseCheck } from "../schedules/license.js";
4
+ import { useStore } from "../utils/store.js";
5
+ import { getSchema } from "../utils/get-schema.js";
6
+ import { getActiveCollections } from "./entitlements/lib/collections.js";
7
+ import { getActiveFlows } from "./entitlements/lib/flows.js";
8
+ import { getActiveSeats } from "./entitlements/lib/seats.js";
9
+ import { EntitlementManager, getEntitlementManager } from "./entitlements/manager.js";
10
+ import { SettingsService } from "../services/settings.js";
11
+ import { UsersService } from "../services/users.js";
12
+ import "../services/index.js";
13
+ import { computeLicenseStatus } from "./utils/compute-license-status.js";
14
+ import { getLicenseKey } from "./utils/get-license-key.js";
15
+ import { getLicenseToken } from "./utils/get-license-token.js";
16
+ import { handleLicenseError } from "./utils/handle-license-error.js";
17
+ import { useRPC } from "./utils/use-rpc.js";
18
+ import { useEnv } from "@directus/env";
19
+ import { ForbiddenError, InvalidPayloadError } from "@directus/errors";
20
+ import { CORE_LICENSE, COUNTABLE_ENTITLEMENT_KEYS, LicenseServerError, ResolveInput, activateKey, billingPortal, deactivateKey, deleteAddon, previewKey, readAddons, refreshLicense, updateAddonQuantity, updateKey, verifyLicense } from "@directus/license";
21
+
22
+ //#region src/license/manager.ts
23
+ const env = useEnv();
24
+ const logger = useLogger();
25
+ const LICENSE_CHANNEL = `license`;
26
+ let licenseCache;
27
+ let licenseManager;
28
+ function getLicenseManager() {
29
+ if (licenseManager) return licenseManager;
30
+ licenseManager = new LicenseManager();
31
+ return licenseManager;
32
+ }
33
+ var LicenseManager = class {
34
+ licenseKey = null;
35
+ licenseToken = null;
36
+ /** Where the key or token comes from */
37
+ source = null;
38
+ initialized = false;
39
+ rpc = useRPC(this, LICENSE_CHANNEL);
40
+ store = useStore(String(env["LICENSE_NAMESPACE"]));
41
+ /**
42
+ * Initialize license state based on the following state permutations.
43
+ *
44
+ * | envKey | envToken | dbKey | dbToken | diff | Outcome | id |
45
+ * | :----: | :------: | :---: | :-----: | :--: | -------------------------------------------- | ---- |
46
+ * | ✓ | ✓ | * | * | * | **Error** — both env vars set, process exits | A |
47
+ * | ✓ | - | ✓ | * | ✓ | update | B |
48
+ * | ✓ | - | ✓ | * | - | verify, refresh | C |
49
+ * | ✓ | - | - | * | - | activate | D |
50
+ * | - | ✓ | * | * | - | verify offline token, cleanup DB | E |
51
+ * | - | - | ✓ | ✓ | - | verify token + refresh | F |
52
+ * | - | - | ✓ | - | - | activate | G |
53
+ * | - | - | - | ✓ | - | cleanup and CORE_LICENSE | H |
54
+ * | - | - | - | - | - | CORE_LICENSE | I |
55
+ */
56
+ async initialize() {
57
+ const existingStore = this.store;
58
+ getEntitlementManager();
59
+ try {
60
+ await this.store(async (store) => {
61
+ this.store = (cb) => {
62
+ return cb(store);
63
+ };
64
+ const envKey = env["LICENSE_KEY"];
65
+ const envToken = env["LICENSE_TOKEN"];
66
+ if (envKey && envToken) {
67
+ logger.fatal("LICENSE_KEY and LICENSE_TOKEN cannot both be set. Provide one or the other.");
68
+ process.exit(1);
69
+ }
70
+ const settingsService = new SettingsService({ schema: await getSchema() });
71
+ const { license_key: dbKey, license_token: dbToken } = await settingsService.readSingleton({ fields: ["license_key", "license_token"] });
72
+ if (envKey) try {
73
+ this.source = "env";
74
+ if (!dbKey) await this.activate(envKey);
75
+ else if (envKey !== dbKey) await this.update(envKey, { oldKey: dbKey });
76
+ else await this.refresh({
77
+ key: envKey,
78
+ token: dbToken ?? null
79
+ });
80
+ } catch (error) {
81
+ logger.fatal("Unable to validate the LICENSE_KEY, please check the key and try again.");
82
+ logger.fatal(error);
83
+ process.exit(1);
84
+ }
85
+ else if (envToken) try {
86
+ this.source = "env";
87
+ await this.refresh({ token: envToken });
88
+ if (dbKey || dbToken) await settingsService.upsertSingleton({
89
+ license_key: null,
90
+ license_token: null
91
+ });
92
+ } catch (error) {
93
+ logger.fatal("Unable to validate the LICENSE_TOKEN, please check the token and try again.");
94
+ logger.fatal(error);
95
+ process.exit(1);
96
+ }
97
+ else if (dbKey) try {
98
+ this.source = "settings";
99
+ if (dbToken) await this.refresh({
100
+ key: dbKey,
101
+ token: dbToken
102
+ });
103
+ else await this.activate(dbKey);
104
+ } catch (error) {
105
+ logger.error("Unable to validate the license key from the database, downgrading to core tier.");
106
+ logger.error(error);
107
+ await this.syncLicense({ kind: "downgrade" });
108
+ }
109
+ else if (dbToken) await this.syncLicense({ kind: "downgrade" });
110
+ else await this.syncLicense();
111
+ this.initialized = true;
112
+ });
113
+ } finally {
114
+ this.store = existingStore;
115
+ }
116
+ }
117
+ async getLicense(options) {
118
+ if (licenseCache) return licenseCache;
119
+ const { token } = await getLicenseToken(options);
120
+ if (!token) {
121
+ this.source = null;
122
+ licenseCache = CORE_LICENSE;
123
+ } else {
124
+ licenseCache = await this.verify(token);
125
+ if (!licenseCache) {
126
+ this.source = null;
127
+ licenseCache = CORE_LICENSE;
128
+ }
129
+ }
130
+ return licenseCache;
131
+ }
132
+ async getStatus() {
133
+ return computeLicenseStatus(this.source === null ? null : await this.getLicense());
134
+ }
135
+ async getDowngradeReason() {
136
+ return await this.store(async (store) => store.get("invalidStatus")) ?? null;
137
+ }
138
+ getSource() {
139
+ return this.source;
140
+ }
141
+ /**
142
+ * Throw if the current license cannot have its key changed (activate / update / deactivate).
143
+ *
144
+ * License management is only allowed for setting-based licenses
145
+ */
146
+ assertCanManageLicense() {
147
+ if (this.initialized && this.source !== "settings") throw new ForbiddenError({ reason: `You cannot manage license for the current license.` });
148
+ }
149
+ /**
150
+ * Throw if the current license cannot have its entitlements changed (e.g. adding addons).
151
+ *
152
+ * Addons are supported for all licenses except core and offline.
153
+ */
154
+ assertCanManageAddons() {
155
+ if (this.source === null || this.licenseKey === null) throw new ForbiddenError({ reason: `You cannot manage addons for the current license.` });
156
+ }
157
+ async isLocked() {
158
+ return await this.getStatus() === "locked";
159
+ }
160
+ /**
161
+ * Check a license meta/info without activating it
162
+ */
163
+ async preview(key) {
164
+ try {
165
+ return await previewKey({ license_key: key });
166
+ } catch (err) {
167
+ handleLicenseError(err);
168
+ }
169
+ }
170
+ /**
171
+ * Activates a new license
172
+ */
173
+ async activate(key) {
174
+ if (this.source !== null) this.assertCanManageLicense();
175
+ if (this.licenseKey) throw new ForbiddenError({ reason: "A license was already activated" });
176
+ const settingsService = new SettingsService({ schema: await getSchema() });
177
+ const { project_id } = await settingsService.readSingleton({ fields: ["project_id"] });
178
+ try {
179
+ const { token, new_project_id } = await activateKey({
180
+ license_key: key,
181
+ project_id,
182
+ public_url: env["PUBLIC_URL"]
183
+ });
184
+ await settingsService.upsertSingleton({
185
+ license_key: key,
186
+ license_token: token,
187
+ project_id: new_project_id ?? project_id
188
+ });
189
+ if (this.initialized) this.source = "settings";
190
+ await this.syncLicense();
191
+ if (this.initialized) await schedule();
192
+ } catch (err) {
193
+ if (err instanceof LicenseServerError) handleLicenseError(err);
194
+ throw err;
195
+ }
196
+ }
197
+ async deactivate(key) {
198
+ this.assertCanManageLicense();
199
+ const currentKey = key ?? this.licenseKey;
200
+ if (!currentKey) throw new InvalidPayloadError({ reason: "\"key\" has to be defined in order to deactivate" });
201
+ const { project_id } = await new SettingsService({ schema: await getSchema() }).readSingleton({ fields: ["project_id"] });
202
+ try {
203
+ await deactivateKey({
204
+ license_key: currentKey,
205
+ project_id,
206
+ public_url: env["PUBLIC_URL"]
207
+ });
208
+ await this.syncLicense({ kind: "downgrade" });
209
+ } catch (err) {
210
+ if (err instanceof LicenseServerError) handleLicenseError(err);
211
+ throw err;
212
+ }
213
+ }
214
+ /**
215
+ * Update from an existing key to a new key
216
+ */
217
+ async update(newKey, options) {
218
+ this.assertCanManageLicense();
219
+ const currentKey = options?.oldKey ?? this.licenseKey;
220
+ if (!currentKey) throw new InvalidPayloadError({ reason: "A current license must be provided in order to update" });
221
+ const settingsService = new SettingsService({ schema: await getSchema() });
222
+ const { project_id } = await settingsService.readSingleton({ fields: ["project_id"] });
223
+ try {
224
+ const { token } = await updateKey({
225
+ license_key: currentKey,
226
+ project_id,
227
+ public_url: env["PUBLIC_URL"]
228
+ }, { license_key: newKey });
229
+ await settingsService.upsertSingleton({
230
+ license_key: newKey,
231
+ license_token: token,
232
+ project_id
233
+ });
234
+ await this.syncLicense();
235
+ } catch (err) {
236
+ if (err instanceof LicenseServerError) handleLicenseError(err);
237
+ throw err;
238
+ }
239
+ }
240
+ async verify(token) {
241
+ try {
242
+ return await verifyLicense(token);
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ /**
248
+ * Verify a license token. On failure, downgrade and mark status 'expired'.
249
+ */
250
+ async refresh(options) {
251
+ const key = options?.key ?? this.licenseKey;
252
+ const token = options?.token ?? this.licenseToken;
253
+ let license = null;
254
+ if (token) {
255
+ license = await this.verify(token);
256
+ if (!license) {
257
+ await this.syncLicense({
258
+ kind: "downgrade",
259
+ reason: "expired"
260
+ });
261
+ return;
262
+ }
263
+ }
264
+ if (license?.meta.offline === false) {
265
+ if (!key) throw new InvalidPayloadError({ reason: "A \"key\" is required" });
266
+ const entitlementManager = getEntitlementManager();
267
+ const settingsService = new SettingsService({ schema: await getSchema() });
268
+ const { project_id } = await settingsService.readSingleton({ fields: ["project_id"] });
269
+ const refreshPayload = { usage_metrics: {
270
+ seats: await entitlementManager.getUsage("seats"),
271
+ collections: await entitlementManager.getUsage("collections"),
272
+ flows: await entitlementManager.getUsage("flows")
273
+ } };
274
+ try {
275
+ const { token: token$1 } = await refreshLicense({
276
+ license_key: key,
277
+ project_id,
278
+ public_url: env["PUBLIC_URL"]
279
+ }, refreshPayload);
280
+ await settingsService.upsertSingleton({ license_token: token$1 });
281
+ } catch (err) {
282
+ logger.error(err);
283
+ if (err instanceof LicenseServerError) {
284
+ if (err.code === "LICENSE_EXPIRED") await this.syncLicense({
285
+ kind: "downgrade",
286
+ reason: "expired"
287
+ });
288
+ else if (err.code === "LICENSE_CANCELED") await this.syncLicense({
289
+ kind: "downgrade",
290
+ reason: "canceled"
291
+ });
292
+ else if (err.code === "LICENSE_SUSPENDED") await this.syncLicense({
293
+ kind: "downgrade",
294
+ reason: "suspended"
295
+ });
296
+ }
297
+ }
298
+ }
299
+ await this.syncLicense();
300
+ }
301
+ async billingPortalUrl() {
302
+ this.assertCanManageAddons();
303
+ const { project_id } = await new SettingsService({ schema: await getSchema() }).readSingleton({ fields: ["project_id"] });
304
+ try {
305
+ const { url } = await billingPortal({
306
+ license_key: this.licenseKey,
307
+ project_id,
308
+ public_url: env["PUBLIC_URL"]
309
+ });
310
+ return url;
311
+ } catch (err) {
312
+ handleLicenseError(err);
313
+ }
314
+ }
315
+ async availableAddons() {
316
+ this.assertCanManageAddons();
317
+ const { project_id } = await new SettingsService({ schema: await getSchema() }).readSingleton({ fields: ["project_id"] });
318
+ try {
319
+ return (await readAddons({
320
+ license_key: this.licenseKey,
321
+ project_id,
322
+ public_url: env["PUBLIC_URL"]
323
+ })).available_addons.map((addon) => ({
324
+ id: addon.id,
325
+ name: addon.name,
326
+ description: addon.description,
327
+ icon: addon.icon,
328
+ unit_price: addon.unit_price,
329
+ billing_interval: addon.billing_interval,
330
+ upgrade_required: addon.upgrade_required,
331
+ pricing_summary: addon.pricing_summary,
332
+ min_quantity: addon.min_quantity,
333
+ max_quantity: addon.max_quantity,
334
+ active_quantity: addon.active_quantity,
335
+ scheduled_quantity: addon.scheduled_quantity
336
+ }));
337
+ } catch (err) {
338
+ handleLicenseError(err);
339
+ }
340
+ }
341
+ async setAddonQuantity(options) {
342
+ this.assertCanManageAddons();
343
+ const settingsService = new SettingsService({ schema: await getSchema() });
344
+ const { project_id } = await settingsService.readSingleton({ fields: ["project_id"] });
345
+ const entitlementManager = getEntitlementManager();
346
+ try {
347
+ const { token } = await updateAddonQuantity({
348
+ license_key: this.licenseKey,
349
+ project_id,
350
+ public_url: env["PUBLIC_URL"]
351
+ }, {
352
+ addons: [{
353
+ addon_id: options.addonId,
354
+ quantity: options.quantity
355
+ }],
356
+ usage_metrics: {
357
+ seats: await entitlementManager.getUsage("seats"),
358
+ collections: await entitlementManager.getUsage("collections"),
359
+ flows: await entitlementManager.getUsage("flows")
360
+ }
361
+ });
362
+ await settingsService.upsertSingleton({ license_token: token });
363
+ await this.syncLicense();
364
+ } catch (err) {
365
+ if (err instanceof LicenseServerError) handleLicenseError(err);
366
+ throw err;
367
+ }
368
+ }
369
+ async removeAddon(addonId) {
370
+ this.assertCanManageAddons();
371
+ const { project_id } = await new SettingsService({ schema: await getSchema() }).readSingleton({ fields: ["project_id"] });
372
+ try {
373
+ await deleteAddon({
374
+ license_key: this.licenseKey,
375
+ project_id,
376
+ public_url: env["PUBLIC_URL"]
377
+ }, { addon_ids: [addonId] });
378
+ } catch (err) {
379
+ handleLicenseError(err);
380
+ }
381
+ }
382
+ /**
383
+ * Retrieve entitlements that are pending resolution
384
+ *
385
+ * If no entitlements to resolve, an empty array will be returned
386
+ */
387
+ async pendingResolution(options) {
388
+ const schema = await getSchema();
389
+ const pendingResolution = [];
390
+ let entitlements;
391
+ if (options.licenseKey) entitlements = (await this.preview(options.licenseKey)).entitlements;
392
+ else if (options.licenseKey === null) entitlements = null;
393
+ else entitlements = (await this.getLicense()).entitlements;
394
+ const entitlementManager = new EntitlementManager();
395
+ entitlementManager.setEntitlements(entitlements);
396
+ const candidateGetters = {
397
+ seats: getActiveSeats,
398
+ collections: getActiveCollections,
399
+ flows: getActiveFlows
400
+ };
401
+ for (const check of COUNTABLE_ENTITLEMENT_KEYS) {
402
+ const resolution = await entitlementManager.check(check);
403
+ if (resolution.allowed === false) {
404
+ const candidates = await candidateGetters[check]({ adminId: options.adminId });
405
+ pendingResolution.push({
406
+ key: check,
407
+ kind: "limit",
408
+ limit: resolution.hardLimit,
409
+ usage: resolution.usage,
410
+ candidates
411
+ });
412
+ }
413
+ }
414
+ if ((await entitlementManager.check("sso_enabled")).valid === false) {
415
+ const adminUser = await new UsersService({ schema }).readOne(options.adminId, { fields: ["email", "password"] });
416
+ const blockers = [];
417
+ if (adminUser["email"] === null) blockers.push("ADMIN_MISSING_EMAIL");
418
+ if (adminUser["password"] === null) blockers.push("ADMIN_MISSING_PASSWORD");
419
+ pendingResolution.push({
420
+ key: "sso_enabled",
421
+ kind: "feature_gate",
422
+ blockers
423
+ });
424
+ }
425
+ if ((await entitlementManager.check("custom_llms_enabled")).valid === false) pendingResolution.push({
426
+ key: "custom_llms_enabled",
427
+ kind: "feature_gate"
428
+ });
429
+ if ((await entitlementManager.check("custom_permission_rules_enabled")).valid === false) pendingResolution.push({
430
+ key: "custom_permission_rules_enabled",
431
+ kind: "feature_gate"
432
+ });
433
+ return pendingResolution;
434
+ }
435
+ /**
436
+ * Apply a resolution strategy
437
+ *
438
+ * Allows partial resolution
439
+ */
440
+ async applyResolution(resolution, ctx) {
441
+ const entitlementManager = getEntitlementManager();
442
+ const cachesToClear = [];
443
+ if (resolution.collections && resolution.collections.length > 0) {
444
+ await entitlementManager.resolve("collections", resolution.collections, { accountability: ctx?.accountability });
445
+ cachesToClear.push("collections");
446
+ }
447
+ if (resolution.seats && resolution.seats.length > 0) {
448
+ await entitlementManager.resolve("seats", resolution.seats, { accountability: ctx?.accountability });
449
+ cachesToClear.push("seats");
450
+ }
451
+ if (resolution.flows && resolution.flows.length > 0) {
452
+ await entitlementManager.resolve("flows", resolution.flows, { accountability: ctx?.accountability });
453
+ cachesToClear.push("flows");
454
+ }
455
+ /**
456
+ * Set all sso users to disabled and optional set the current admin email and password
457
+ */
458
+ if (resolution.sso_enabled) {
459
+ await entitlementManager.resolve("sso_enabled", resolution.sso_enabled, { accountability: ctx?.accountability });
460
+ if (!cachesToClear.includes("seats")) cachesToClear.push("seats");
461
+ cachesToClear.push("sso_enabled");
462
+ }
463
+ if (cachesToClear.length > 0) await entitlementManager.clearCache(...cachesToClear);
464
+ if (await entitlementManager.checkAll()) await this.syncLicense({ kind: "clear-status" });
465
+ }
466
+ /**
467
+ * Apply a state transition and propagate to all instances.
468
+ *
469
+ * - { kind: 'downgrade', reason? }: clear key + token, drop to core, propagate.
470
+ * - { kind: 'clear-token' }: clear only the token; key survives for re-activation. Marker preserved (server's verdict still applies). Propagates.
471
+ * - { kind: 'clear-status' }: clear the invalidStatus marker only. Redis-only, does NOT propagate.
472
+ */
473
+ async syncLicense(options) {
474
+ if (options?.kind !== "downgrade" || options?.kind === "downgrade" && options.reason === void 0) {
475
+ await this.store(async (store) => store.delete("invalidStatus"));
476
+ if (options?.kind === "clear-status") return;
477
+ }
478
+ if (options?.kind === "downgrade") {
479
+ await new SettingsService({ schema: await getSchema() }).upsertSingleton({
480
+ license_key: null,
481
+ license_token: null
482
+ });
483
+ this.source = null;
484
+ await stopLicenseCheck();
485
+ if (options.reason) await this.store(async (store) => store.set("invalidStatus", options.reason));
486
+ } else if (options?.kind === "clear-token") await new SettingsService({ schema: await getSchema() }).upsertSingleton({ license_token: null });
487
+ await clearCache();
488
+ await this.syncState({ source: this.source });
489
+ await this.rpc.syncState({ source: this.source });
490
+ }
491
+ async syncState(options) {
492
+ const { key } = await getLicenseKey();
493
+ const { token } = await getLicenseToken();
494
+ this.licenseKey = key;
495
+ this.licenseToken = token;
496
+ this.initialized = true;
497
+ if (options && "source" in options) this.source = options.source;
498
+ licenseCache = null;
499
+ const license = await this.getLicense();
500
+ getEntitlementManager().setEntitlements(license.entitlements);
501
+ }
502
+ };
503
+
504
+ //#endregion
505
+ export { LicenseManager, getLicenseManager };
@@ -0,0 +1,27 @@
1
+ import { getEntitlementManager } from "../entitlements/manager.js";
2
+ import { isInCoreGracePeriod } from "./is-in-core-grace-period.js";
3
+ import "../index.js";
4
+
5
+ //#region src/license/utils/compute-license-status.ts
6
+ /**
7
+ * Compute the operational license status.
8
+ */
9
+ async function computeLicenseStatus(license) {
10
+ const entitlementManager = getEntitlementManager().fork(license?.entitlements ?? null);
11
+ if (!license) {
12
+ const isWithinLimits = await entitlementManager.checkAll();
13
+ const isWithinCoreGracePeriod = await isInCoreGracePeriod();
14
+ if (isWithinLimits === false && isWithinCoreGracePeriod) return "grace";
15
+ if (isWithinLimits === false) return "locked";
16
+ return "active";
17
+ }
18
+ if (await entitlementManager.checkAll() === false) return "locked";
19
+ const now = Math.floor(Date.now() / 1e3);
20
+ const expires = license.meta.expires_at ?? license.meta.renews_at ?? -1;
21
+ if (expires === -1 || now < expires) return "active";
22
+ if (expires < now && expires + license.meta.grace_period > now) return "grace";
23
+ throw new Error("License is expired beyond grace period");
24
+ }
25
+
26
+ //#endregion
27
+ export { computeLicenseStatus };
@@ -0,0 +1,38 @@
1
+ import { ItemsService } from "../../services/items.js";
2
+ import { getSchema } from "../../utils/get-schema.js";
3
+
4
+ //#region src/license/utils/get-core-grace-expires-at.ts
5
+ const V12_MIGRATION_VERSION = "20260507A";
6
+ const CLEAN_INSTALL_MS = 1440 * 60 * 1e3;
7
+ const GRACE_PERIOD_MS = 720 * 60 * 60 * 1e3;
8
+ const _cache = { migrations: void 0 };
9
+ /**
10
+ * V12 migration timestamp in seconds, used as the Core "expires_at" during the upgrade grace.
11
+ * Returns `null` when no grace applies.
12
+ */
13
+ async function getCoreGraceExpiresAt() {
14
+ if (!_cache.migrations) {
15
+ const itemsService = new ItemsService("directus_migrations", { schema: await getSchema() });
16
+ const [oldest, v12] = await Promise.all([itemsService.readByQuery({
17
+ fields: ["timestamp"],
18
+ sort: ["timestamp"],
19
+ limit: 1
20
+ }).then((r) => r[0]), itemsService.readByQuery({
21
+ fields: ["timestamp"],
22
+ filter: { version: { _eq: V12_MIGRATION_VERSION } },
23
+ limit: 1
24
+ }).then((r) => r[0])]);
25
+ _cache.migrations = {
26
+ oldest,
27
+ v12
28
+ };
29
+ }
30
+ if (!_cache.migrations.oldest || !_cache.migrations.v12) return null;
31
+ const start = new Date(_cache.migrations.oldest["timestamp"]).getTime();
32
+ const upgrade = new Date(_cache.migrations.v12["timestamp"]).getTime();
33
+ if (upgrade - start < CLEAN_INSTALL_MS) return null;
34
+ return Math.floor(upgrade / 1e3);
35
+ }
36
+
37
+ //#endregion
38
+ export { GRACE_PERIOD_MS, _cache, getCoreGraceExpiresAt };
@@ -0,0 +1,23 @@
1
+ import { getSchema } from "../../utils/get-schema.js";
2
+ import { SettingsService } from "../../services/settings.js";
3
+ import { useEnv } from "@directus/env";
4
+
5
+ //#region src/license/utils/get-license-key.ts
6
+ async function getLicenseKey(options) {
7
+ const env = useEnv();
8
+ if (env["LICENSE_KEY"]) return {
9
+ source: "env",
10
+ key: String(env["LICENSE_KEY"])
11
+ };
12
+ const { license_key } = await new SettingsService({
13
+ schema: await getSchema(options),
14
+ ...options
15
+ }).readSingleton({ fields: ["license_key"] });
16
+ return {
17
+ source: license_key ? "settings" : null,
18
+ key: license_key ?? null
19
+ };
20
+ }
21
+
22
+ //#endregion
23
+ export { getLicenseKey };
@@ -0,0 +1,23 @@
1
+ import { getSchema } from "../../utils/get-schema.js";
2
+ import { SettingsService } from "../../services/settings.js";
3
+ import { useEnv } from "@directus/env";
4
+
5
+ //#region src/license/utils/get-license-token.ts
6
+ async function getLicenseToken(options) {
7
+ const env = useEnv();
8
+ if (env["LICENSE_TOKEN"]) return {
9
+ source: "env",
10
+ token: String(env["LICENSE_TOKEN"])
11
+ };
12
+ const { license_token } = await new SettingsService({
13
+ schema: await getSchema(options),
14
+ ...options
15
+ }).readSingleton({ fields: ["license_token"] });
16
+ return {
17
+ source: license_token ? "settings" : null,
18
+ token: license_token ?? null
19
+ };
20
+ }
21
+
22
+ //#endregion
23
+ export { getLicenseToken };
@@ -0,0 +1,41 @@
1
+ import { ForbiddenError, HitRateLimitError, InvalidPayloadError, LicenseInvalidError, ServiceUnavailableError } from "@directus/errors";
2
+ import { LicenseServerError } from "@directus/license";
3
+
4
+ //#region src/license/utils/handle-license-error.ts
5
+ function handleLicenseError(error) {
6
+ if (error instanceof LicenseServerError) {
7
+ const reason = error.message;
8
+ switch (error.code) {
9
+ case "INVALID_PAYLOAD":
10
+ case "LIMIT_OVERFLOW": throw new InvalidPayloadError({ reason });
11
+ case "INVALID_CREDENTIALS":
12
+ case "LICENSE_EXPIRED":
13
+ case "LICENSE_CANCELED":
14
+ case "LICENSE_SUSPENDED":
15
+ case "NOT_FOUND": throw new LicenseInvalidError();
16
+ case "FORBIDDEN":
17
+ case "BINDING_MISMATCH":
18
+ case "SUBSCRIPTION_PAST_DUE":
19
+ case "NO_PAYMENT_METHOD":
20
+ case "ACTIVATION_LIMIT_EXCEEDED":
21
+ case "ADDON_NOT_ALLOWED":
22
+ case "ROUTE_NOT_FOUND":
23
+ case "BILLING_LINKAGE_MISSING": throw new ForbiddenError({ reason });
24
+ case "REQUESTS_EXCEEDED": {
25
+ const limit = typeof error.extensions["limit"] === "number" ? error.extensions["limit"] : 0;
26
+ const retryAfterSeconds = typeof error.extensions["retry_after"] === "number" ? error.extensions["retry_after"] : 1;
27
+ throw new HitRateLimitError({
28
+ limit,
29
+ reset: new Date(Date.now() + retryAfterSeconds * 1e3)
30
+ });
31
+ }
32
+ }
33
+ }
34
+ throw new ServiceUnavailableError({
35
+ service: "license",
36
+ reason: error instanceof Error ? error.message : "An unknown error occurred"
37
+ });
38
+ }
39
+
40
+ //#endregion
41
+ export { handleLicenseError };
@@ -0,0 +1,11 @@
1
+ import { GRACE_PERIOD_MS, getCoreGraceExpiresAt } from "./get-core-grace-expires-at.js";
2
+
3
+ //#region src/license/utils/is-in-core-grace-period.ts
4
+ async function isInCoreGracePeriod() {
5
+ const expiresAtSec = await getCoreGraceExpiresAt();
6
+ if (expiresAtSec === null) return false;
7
+ return Date.now() - expiresAtSec * 1e3 < GRACE_PERIOD_MS;
8
+ }
9
+
10
+ //#endregion
11
+ export { isInCoreGracePeriod };