@directus/api 33.0.0 → 33.1.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 (116) hide show
  1. package/dist/ai/chat/controllers/chat.post.js +19 -4
  2. package/dist/ai/chat/lib/create-ui-stream.d.ts +7 -6
  3. package/dist/ai/chat/lib/create-ui-stream.js +28 -25
  4. package/dist/ai/chat/middleware/load-settings.js +31 -7
  5. package/dist/ai/chat/models/chat-request.d.ts +135 -2
  6. package/dist/ai/chat/models/chat-request.js +56 -2
  7. package/dist/ai/chat/models/providers.d.ts +16 -2
  8. package/dist/ai/chat/models/providers.js +16 -2
  9. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
  10. package/dist/ai/chat/utils/format-context.d.ts +5 -0
  11. package/dist/ai/chat/utils/format-context.js +122 -0
  12. package/dist/ai/mcp/server.d.ts +27 -1
  13. package/dist/ai/providers/index.d.ts +3 -0
  14. package/dist/ai/providers/index.js +3 -0
  15. package/dist/ai/providers/options.d.ts +14 -0
  16. package/dist/ai/providers/options.js +26 -0
  17. package/dist/ai/providers/registry.d.ts +6 -0
  18. package/dist/ai/providers/registry.js +65 -0
  19. package/dist/ai/providers/types.d.ts +34 -0
  20. package/dist/ai/providers/types.js +1 -0
  21. package/dist/ai/tools/items/index.js +4 -1
  22. package/dist/ai/tools/items/prompt.md +7 -9
  23. package/dist/ai/tools/schema.js +1 -1
  24. package/dist/app.js +4 -0
  25. package/dist/auth/drivers/ldap.d.ts +1 -1
  26. package/dist/auth/drivers/ldap.js +142 -137
  27. package/dist/cache.d.ts +12 -0
  28. package/dist/cache.js +25 -1
  29. package/dist/cli/utils/create-env/env-stub.liquid +3 -0
  30. package/dist/controllers/deployment.d.ts +2 -0
  31. package/dist/controllers/deployment.js +481 -0
  32. package/dist/controllers/fields.js +6 -4
  33. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  34. package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
  35. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
  36. package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
  37. package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
  38. package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
  39. package/dist/database/migrations/20260204A-add-deployment.js +32 -0
  40. package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
  41. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
  43. package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
  44. package/dist/deployment/deployment.d.ts +94 -0
  45. package/dist/deployment/deployment.js +29 -0
  46. package/dist/deployment/drivers/index.d.ts +1 -0
  47. package/dist/deployment/drivers/index.js +1 -0
  48. package/dist/deployment/drivers/vercel.d.ts +32 -0
  49. package/dist/deployment/drivers/vercel.js +208 -0
  50. package/dist/deployment/index.d.ts +2 -0
  51. package/dist/deployment/index.js +2 -0
  52. package/dist/deployment.d.ts +24 -0
  53. package/dist/deployment.js +39 -0
  54. package/dist/middleware/respond.js +27 -14
  55. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
  56. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +1 -1
  57. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +19 -8
  58. package/dist/server.js +2 -1
  59. package/dist/services/deployment-projects.d.ts +20 -0
  60. package/dist/services/deployment-projects.js +34 -0
  61. package/dist/services/deployment-runs.d.ts +13 -0
  62. package/dist/services/deployment-runs.js +6 -0
  63. package/dist/services/deployment.d.ts +40 -0
  64. package/dist/services/deployment.js +202 -0
  65. package/dist/services/graphql/resolvers/system-admin.js +2 -3
  66. package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
  67. package/dist/services/index.d.ts +3 -0
  68. package/dist/services/index.js +3 -0
  69. package/dist/services/server.js +1 -0
  70. package/dist/services/specifications.js +2 -2
  71. package/dist/services/versions.js +1 -1
  72. package/dist/telemetry/lib/get-report.js +2 -0
  73. package/dist/telemetry/types/report.d.ts +8 -0
  74. package/dist/telemetry/utils/get-settings.d.ts +2 -0
  75. package/dist/telemetry/utils/get-settings.js +5 -0
  76. package/dist/utils/deep-map-response.d.ts +1 -1
  77. package/dist/utils/deep-map-response.js +1 -1
  78. package/dist/utils/get-column-path.js +1 -1
  79. package/dist/utils/get-service.js +7 -1
  80. package/dist/utils/is-field-allowed.d.ts +4 -0
  81. package/dist/utils/is-field-allowed.js +9 -0
  82. package/dist/utils/versioning/handle-version.js +1 -1
  83. package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
  84. package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
  85. package/dist/websocket/collab/collab.d.ts +63 -0
  86. package/dist/websocket/collab/collab.js +481 -0
  87. package/dist/websocket/collab/constants.d.ts +1 -0
  88. package/dist/websocket/collab/constants.js +13 -0
  89. package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
  90. package/dist/websocket/collab/filter-to-fields.js +11 -0
  91. package/dist/websocket/collab/messenger.d.ts +43 -0
  92. package/dist/websocket/collab/messenger.js +225 -0
  93. package/dist/websocket/collab/payload-permissions.d.ts +18 -0
  94. package/dist/websocket/collab/payload-permissions.js +158 -0
  95. package/dist/websocket/collab/permissions-cache.d.ts +52 -0
  96. package/dist/websocket/collab/permissions-cache.js +204 -0
  97. package/dist/websocket/collab/room.d.ts +125 -0
  98. package/dist/websocket/collab/room.js +593 -0
  99. package/dist/websocket/collab/store.d.ts +7 -0
  100. package/dist/websocket/collab/store.js +33 -0
  101. package/dist/websocket/collab/types.d.ts +21 -0
  102. package/dist/websocket/collab/types.js +1 -0
  103. package/dist/websocket/collab/verify-permissions.d.ts +11 -0
  104. package/dist/websocket/collab/verify-permissions.js +100 -0
  105. package/dist/websocket/handlers/index.d.ts +2 -0
  106. package/dist/websocket/handlers/index.js +9 -0
  107. package/dist/websocket/utils/items.d.ts +2 -2
  108. package/dist/websocket/utils/message.d.ts +1 -1
  109. package/dist/websocket/utils/message.js +2 -2
  110. package/package.json +32 -30
  111. package/dist/utils/get-relation-info.d.ts +0 -6
  112. package/dist/utils/get-relation-info.js +0 -43
  113. package/dist/utils/get-relation-type.d.ts +0 -6
  114. package/dist/utils/get-relation-type.js +0 -18
  115. package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
  116. package/dist/utils/versioning/deep-map-with-schema.js +0 -81
@@ -2,7 +2,7 @@ import { useEnv } from '@directus/env';
2
2
  import { ErrorCode, InvalidCredentialsError, InvalidPayloadError, InvalidProviderConfigError, InvalidProviderError, isDirectusError, ServiceUnavailableError, UnexpectedResponseError, } from '@directus/errors';
3
3
  import { Router } from 'express';
4
4
  import Joi from 'joi';
5
- import ldap from 'ldapjs';
5
+ import { Client, InappropriateAuthError, InsufficientAccessError, InvalidCredentialsError as LdapInvalidCredentialsError, } from 'ldapts';
6
6
  import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
7
7
  import getDatabase from '../../database/index.js';
8
8
  import emitter from '../../emitter.js';
@@ -34,127 +34,101 @@ export class LDAPAuthDriver extends AuthDriver {
34
34
  throw new InvalidProviderConfigError({ provider });
35
35
  }
36
36
  const clientConfig = typeof config['client'] === 'object' ? config['client'] : {};
37
- this.bindClient = ldap.createClient({ url: clientUrl, reconnect: true, ...clientConfig });
38
- this.bindClient.on('error', (err) => {
39
- logger.warn(err);
37
+ this.bindClient = new Client({
38
+ url: clientUrl,
39
+ ...clientConfig,
40
40
  });
41
41
  this.config = config;
42
42
  }
43
43
  async validateBindClient() {
44
44
  const logger = useLogger();
45
45
  const { bindDn, bindPassword, provider } = this.config;
46
- return new Promise((resolve, reject) => {
47
- // Healthcheck bind user
48
- this.bindClient.search(bindDn, {}, (err, res) => {
49
- if (err) {
50
- reject(handleError(err));
51
- return;
52
- }
53
- res.on('searchEntry', () => {
54
- resolve();
55
- });
56
- res.on('error', () => {
57
- // Attempt to rebind on search error
58
- this.bindClient.bind(bindDn, bindPassword, (err) => {
59
- if (err) {
60
- const error = handleError(err);
61
- if (isDirectusError(error, ErrorCode.InvalidCredentials)) {
62
- logger.warn('Invalid bind user');
63
- reject(new InvalidProviderConfigError({ provider }));
64
- }
65
- else {
66
- reject(error);
67
- }
68
- }
69
- else {
70
- resolve();
71
- }
72
- });
73
- });
74
- res.on('end', (result) => {
75
- // Handle edge case where authenticated bind user cannot read their own DN
76
- // Status `0` is success
77
- if (result?.status !== 0) {
78
- logger.warn('[LDAP] Failed to find bind user record');
79
- reject(new UnexpectedResponseError());
80
- }
81
- });
46
+ try {
47
+ // Attempt to bind with the configured credentials
48
+ await this.bindClient.bind(bindDn, bindPassword);
49
+ // Healthcheck: verify bind user can read their own DN
50
+ const { searchEntries } = await this.bindClient.search(bindDn, {
51
+ scope: 'base',
82
52
  });
83
- });
53
+ if (searchEntries.length === 0) {
54
+ logger.warn('[LDAP] Failed to find bind user record');
55
+ throw new UnexpectedResponseError();
56
+ }
57
+ }
58
+ catch (err) {
59
+ const error = handleError(err);
60
+ if (isDirectusError(error, ErrorCode.InvalidCredentials)) {
61
+ logger.warn('Invalid bind user');
62
+ throw new InvalidProviderConfigError({ provider });
63
+ }
64
+ throw error;
65
+ }
84
66
  }
85
67
  async fetchUserInfo(baseDn, filter, scope) {
86
68
  let { firstNameAttribute, lastNameAttribute, mailAttribute } = this.config;
87
69
  firstNameAttribute ??= 'givenName';
88
70
  lastNameAttribute ??= 'sn';
89
71
  mailAttribute ??= 'mail';
90
- return new Promise((resolve, reject) => {
91
- // Search for the user in LDAP by filter
92
- this.bindClient.search(baseDn, {
93
- filter,
94
- scope,
72
+ try {
73
+ const searchOptions = {
95
74
  attributes: ['uid', firstNameAttribute, lastNameAttribute, mailAttribute, 'userAccountControl'],
96
- }, (err, res) => {
97
- if (err) {
98
- reject(handleError(err));
99
- return;
100
- }
101
- res.on('searchEntry', ({ object }) => {
102
- const user = {
103
- dn: object['dn'],
104
- userAccountControl: Number(getEntryValue(object['userAccountControl']) ?? 0),
105
- };
106
- const firstName = getEntryValue(object[firstNameAttribute]);
107
- if (firstName)
108
- user.firstName = firstName;
109
- const lastName = getEntryValue(object[lastNameAttribute]);
110
- if (lastName)
111
- user.lastName = lastName;
112
- const email = getEntryValue(object[mailAttribute]);
113
- if (email)
114
- user.email = email;
115
- const uid = getEntryValue(object['uid']);
116
- if (uid)
117
- user.uid = uid;
118
- resolve(user);
119
- });
120
- res.on('error', (err) => {
121
- reject(handleError(err));
122
- });
123
- res.on('end', () => {
124
- resolve(undefined);
125
- });
126
- });
127
- });
75
+ };
76
+ if (filter !== undefined)
77
+ searchOptions.filter = filter;
78
+ if (scope !== undefined)
79
+ searchOptions.scope = scope;
80
+ const { searchEntries } = await this.bindClient.search(baseDn, searchOptions);
81
+ if (searchEntries.length === 0) {
82
+ return undefined;
83
+ }
84
+ const entry = searchEntries[0];
85
+ const user = {
86
+ dn: entry['dn'],
87
+ userAccountControl: Number(getEntryValue(entry['userAccountControl']) ?? 0),
88
+ };
89
+ const firstName = getEntryValue(entry[firstNameAttribute]);
90
+ if (firstName)
91
+ user.firstName = firstName;
92
+ const lastName = getEntryValue(entry[lastNameAttribute]);
93
+ if (lastName)
94
+ user.lastName = lastName;
95
+ const email = getEntryValue(entry[mailAttribute]);
96
+ if (email)
97
+ user.email = email;
98
+ const uid = getEntryValue(entry['uid']);
99
+ if (uid)
100
+ user.uid = uid;
101
+ return user;
102
+ }
103
+ catch (err) {
104
+ throw handleError(err);
105
+ }
128
106
  }
129
107
  async fetchUserGroups(baseDn, filter, scope) {
130
- return new Promise((resolve, reject) => {
131
- let userGroups = [];
132
- // Search for the user info in LDAP by group attribute
133
- this.bindClient.search(baseDn, {
134
- filter,
135
- scope,
108
+ try {
109
+ const searchOptions = {
136
110
  attributes: ['cn'],
137
- }, (err, res) => {
138
- if (err) {
139
- reject(handleError(err));
140
- return;
111
+ };
112
+ if (filter !== undefined)
113
+ searchOptions.filter = filter;
114
+ if (scope !== undefined)
115
+ searchOptions.scope = scope;
116
+ const { searchEntries } = await this.bindClient.search(baseDn, searchOptions);
117
+ const userGroups = [];
118
+ for (const entry of searchEntries) {
119
+ const cn = entry['cn'];
120
+ if (Array.isArray(cn)) {
121
+ userGroups.push(...cn.map((v) => String(v)));
141
122
  }
142
- res.on('searchEntry', ({ object }) => {
143
- if (typeof object['cn'] === 'object') {
144
- userGroups = [...userGroups, ...object['cn']];
145
- }
146
- else if (object['cn']) {
147
- userGroups.push(object['cn']);
148
- }
149
- });
150
- res.on('error', (err) => {
151
- reject(handleError(err));
152
- });
153
- res.on('end', () => {
154
- resolve(userGroups);
155
- });
156
- });
157
- });
123
+ else if (cn) {
124
+ userGroups.push(String(cn));
125
+ }
126
+ }
127
+ return userGroups;
128
+ }
129
+ catch (err) {
130
+ throw handleError(err);
131
+ }
158
132
  }
159
133
  async fetchUserId(userDn) {
160
134
  const user = await this.knex
@@ -171,19 +145,15 @@ export class LDAPAuthDriver extends AuthDriver {
171
145
  const logger = useLogger();
172
146
  await this.validateBindClient();
173
147
  const { userDn, userScope, userAttribute, groupDn, groupScope, groupAttribute, defaultRoleId, syncUserInfo } = this.config;
174
- const userInfo = await this.fetchUserInfo(userDn, new ldap.EqualityFilter({
175
- attribute: userAttribute ?? 'cn',
176
- value: payload['identifier'],
177
- }), userScope ?? 'one');
148
+ const userInfo = await this.fetchUserInfo(userDn, `(${validateLDAPAttribute(userAttribute ?? 'cn')}=${escapeFilterValue(payload['identifier'])})`, userScope ?? 'one');
178
149
  if (!userInfo?.dn) {
179
150
  throw new InvalidCredentialsError();
180
151
  }
181
152
  let userRole;
182
153
  if (groupDn) {
183
- const userGroups = await this.fetchUserGroups(groupDn, new ldap.EqualityFilter({
184
- attribute: groupAttribute ?? 'member',
185
- value: groupAttribute?.toLowerCase() === 'memberuid' && userInfo.uid ? userInfo.uid : userInfo.dn,
186
- }), groupScope ?? 'one');
154
+ const groupAttr = groupAttribute ?? 'member';
155
+ const memberValue = groupAttr.toLowerCase() === 'memberuid' && userInfo.uid ? userInfo.uid : userInfo.dn;
156
+ const userGroups = await this.fetchUserGroups(groupDn, `(${validateLDAPAttribute(groupAttr)}=${escapeFilterValue(memberValue)})`, groupScope ?? 'one');
187
157
  if (userGroups.length) {
188
158
  userRole = await this.knex
189
159
  .select('id')
@@ -253,51 +223,86 @@ export class LDAPAuthDriver extends AuthDriver {
253
223
  if (!user.external_identifier || !password) {
254
224
  throw new InvalidCredentialsError();
255
225
  }
256
- return new Promise((resolve, reject) => {
257
- const clientConfig = typeof this.config['client'] === 'object' ? this.config['client'] : {};
258
- const client = ldap.createClient({
259
- url: this.config['clientUrl'],
260
- ...clientConfig,
261
- reconnect: false,
262
- });
263
- client.on('error', (err) => {
264
- reject(handleError(err));
265
- });
266
- client.bind(user.external_identifier, password, (err) => {
267
- if (err) {
268
- reject(handleError(err));
269
- }
270
- else {
271
- resolve();
272
- }
273
- client.destroy();
274
- });
226
+ const clientConfig = typeof this.config['client'] === 'object' ? this.config['client'] : {};
227
+ const client = new Client({
228
+ url: this.config['clientUrl'],
229
+ ...clientConfig,
275
230
  });
231
+ try {
232
+ await client.bind(user.external_identifier, password);
233
+ }
234
+ catch (err) {
235
+ throw handleError(err);
236
+ }
237
+ finally {
238
+ await client.unbind().catch(() => {
239
+ // Ignore unbind errors
240
+ });
241
+ }
276
242
  }
277
243
  async login(user, payload) {
278
244
  await this.verify(user, payload['password']);
279
245
  }
280
246
  async refresh(user) {
281
247
  await this.validateBindClient();
282
- const userInfo = await this.fetchUserInfo(user.external_identifier);
248
+ // Use scope 'base' to search the specific DN entry
249
+ const userInfo = await this.fetchUserInfo(user.external_identifier, undefined, 'base');
283
250
  if (userInfo?.userAccountControl && userInfo.userAccountControl & INVALID_ACCOUNT_FLAGS) {
284
251
  throw new InvalidCredentialsError();
285
252
  }
286
253
  }
287
254
  }
288
255
  const handleError = (e) => {
289
- if (e instanceof ldap.InappropriateAuthenticationError ||
290
- e instanceof ldap.InvalidCredentialsError ||
291
- e instanceof ldap.InsufficientAccessRightsError) {
256
+ if (e instanceof InappropriateAuthError ||
257
+ e instanceof LdapInvalidCredentialsError ||
258
+ e instanceof InsufficientAccessError) {
292
259
  return new InvalidCredentialsError();
293
260
  }
261
+ if (e instanceof Error) {
262
+ return new ServiceUnavailableError({
263
+ service: 'ldap',
264
+ reason: `Service returned unexpected error: ${e.message}`,
265
+ });
266
+ }
294
267
  return new ServiceUnavailableError({
295
268
  service: 'ldap',
296
- reason: `Service returned unexpected error: ${e.message}`,
269
+ reason: 'Service returned unexpected error',
297
270
  });
298
271
  };
299
272
  const getEntryValue = (value) => {
300
- return typeof value === 'object' ? value[0] : value;
273
+ if (value === undefined)
274
+ return undefined;
275
+ if (Buffer.isBuffer(value)) {
276
+ return value.toString();
277
+ }
278
+ if (Array.isArray(value)) {
279
+ const first = value[0];
280
+ if (Buffer.isBuffer(first)) {
281
+ return first.toString();
282
+ }
283
+ return first;
284
+ }
285
+ return value;
286
+ };
287
+ /**
288
+ * Escape special characters in LDAP filter values according to RFC 4515
289
+ */
290
+ const escapeFilterValue = (value) => {
291
+ return value
292
+ .replace(/\\/g, '\\5c')
293
+ .replace(/\*/g, '\\2a')
294
+ .replace(/\(/g, '\\28')
295
+ .replace(/\)/g, '\\29')
296
+ .replace(/\0/g, '\\00');
297
+ };
298
+ /**
299
+ * Validate LDAP attribute name according to RFC 4512
300
+ */
301
+ const validateLDAPAttribute = (name) => {
302
+ if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(name) === false) {
303
+ throw new Error(`Invalid LDAP attribute name: "${name}"`);
304
+ }
305
+ return name;
301
306
  };
302
307
  export function createLDAPAuthRouter(provider) {
303
308
  const router = Router();
package/dist/cache.d.ts CHANGED
@@ -3,6 +3,7 @@ import Keyv from 'keyv';
3
3
  export declare function getCache(): {
4
4
  cache: Keyv | null;
5
5
  systemCache: Keyv;
6
+ deploymentCache: Keyv;
6
7
  localSchemaCache: Keyv;
7
8
  lockCache: Keyv;
8
9
  };
@@ -17,3 +18,14 @@ export declare function setMemorySchemaCache(schema: SchemaOverview): void;
17
18
  export declare function getMemorySchemaCache(): Readonly<SchemaOverview> | undefined;
18
19
  export declare function setCacheValue(cache: Keyv, key: string, value: Record<string, any> | Record<string, any>[], ttl?: number): Promise<void>;
19
20
  export declare function getCacheValue(cache: Keyv, key: string): Promise<any>;
21
+ /**
22
+ * Store a value in cache with its expiration timestamp for TTL tracking
23
+ */
24
+ export declare function setCacheValueWithExpiry(cache: Keyv, key: string, value: Record<string, any> | Record<string, any>[], ttl: number): Promise<void>;
25
+ /**
26
+ * Get a cached value along with its remaining TTL
27
+ */
28
+ export declare function getCacheValueWithTTL(cache: Keyv, key: string): Promise<{
29
+ data: any;
30
+ remainingTTL: number;
31
+ } | undefined>;
package/dist/cache.js CHANGED
@@ -15,6 +15,7 @@ const env = useEnv();
15
15
  const require = createRequire(import.meta.url);
16
16
  let cache = null;
17
17
  let systemCache = null;
18
+ let deploymentCache = null;
18
19
  let lockCache = null;
19
20
  let messengerSubscribed = false;
20
21
  let localSchemaCache = null;
@@ -40,6 +41,11 @@ export function getCache() {
40
41
  systemCache = getKeyvInstance(env['CACHE_STORE'], getMilliseconds(env['CACHE_SYSTEM_TTL']), '_system');
41
42
  systemCache.on('error', (err) => logger.warn(err, `[system-cache] ${err}`));
42
43
  }
44
+ if (deploymentCache === null) {
45
+ const ttl = getMilliseconds(env['CACHE_DEPLOYMENT_TTL']) || 5000; // Default 5s
46
+ deploymentCache = getKeyvInstance(env['CACHE_STORE'], ttl, '_deployment');
47
+ deploymentCache.on('error', (err) => logger.warn(err, `[deployment-cache] ${err}`));
48
+ }
43
49
  if (localSchemaCache === null) {
44
50
  localSchemaCache = getKeyvInstance('memory', getMilliseconds(env['CACHE_SYSTEM_TTL']), '_schema');
45
51
  localSchemaCache.on('error', (err) => logger.warn(err, `[schema-cache] ${err}`));
@@ -48,7 +54,7 @@ export function getCache() {
48
54
  lockCache = getKeyvInstance(env['CACHE_STORE'], undefined, '_lock');
49
55
  lockCache.on('error', (err) => logger.warn(err, `[lock-cache] ${err}`));
50
56
  }
51
- return { cache, systemCache, localSchemaCache, lockCache };
57
+ return { cache, systemCache, deploymentCache, localSchemaCache, lockCache };
52
58
  }
53
59
  export async function flushCaches(forced) {
54
60
  const { cache } = getCache();
@@ -107,6 +113,24 @@ export async function getCacheValue(cache, key) {
107
113
  const decompressed = await decompress(value);
108
114
  return decompressed;
109
115
  }
116
+ /**
117
+ * Store a value in cache with its expiration timestamp for TTL tracking
118
+ */
119
+ export async function setCacheValueWithExpiry(cache, key, value, ttl) {
120
+ await setCacheValue(cache, key, value, ttl);
121
+ await setCacheValue(cache, `${key}__expires_at`, { exp: Date.now() + ttl }, ttl);
122
+ }
123
+ /**
124
+ * Get a cached value along with its remaining TTL
125
+ */
126
+ export async function getCacheValueWithTTL(cache, key) {
127
+ const value = await getCacheValue(cache, key);
128
+ if (!value)
129
+ return undefined;
130
+ const expiryData = await getCacheValue(cache, `${key}__expires_at`);
131
+ const remainingTTL = expiryData?.exp ? Math.max(0, expiryData.exp - Date.now()) : 0;
132
+ return { data: value, remainingTTL };
133
+ }
110
134
  function getKeyvInstance(store, ttl, namespaceSuffix) {
111
135
  switch (store) {
112
136
  case 'redis':
@@ -148,6 +148,9 @@ CACHE_ENABLED=false
148
148
  # How long the cache is persisted ["5m"]
149
149
  # CACHE_TTL="30m"
150
150
 
151
+ # How long deployment provider data is cached ["5s"]
152
+ # CACHE_DEPLOYMENT_TTL="5s"
153
+
151
154
  # How to scope the cache data ["system-cache"]
152
155
  # CACHE_NAMESPACE="system-cache"
153
156
 
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;