@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.
- 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 +45 -1
- 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/schema.js +2 -2
- 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 +3 -3
- package/dist/extensions/lib/sandbox/register/operation.js +1 -1
- package/dist/extensions/lib/sync/sync.js +2 -2
- package/dist/extensions/manager.js +5 -5
- package/dist/flows.js +12 -10
- 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 +41 -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/schema.js +2 -2
- 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 +138 -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-diff.js +7 -3
- 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 -33
- package/dist/controllers/mcp.js +0 -31
- 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 };
|