@nocobase/cli 2.1.0-alpha.26 → 2.1.0-alpha.28
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/README.md +24 -0
- package/README.zh-CN.md +4 -0
- package/dist/commands/app/down.js +2 -3
- package/dist/commands/app/logs.js +2 -2
- package/dist/commands/app/upgrade.js +114 -128
- package/dist/commands/config/delete.js +30 -0
- package/dist/commands/config/get.js +29 -0
- package/dist/commands/config/index.js +20 -0
- package/dist/commands/config/list.js +29 -0
- package/dist/commands/config/set.js +35 -0
- package/dist/commands/db/check.js +238 -0
- package/dist/commands/db/logs.js +2 -2
- package/dist/commands/db/shared.js +6 -5
- package/dist/commands/env/info.js +6 -2
- package/dist/commands/env/shared.js +1 -1
- package/dist/commands/init.js +0 -1
- package/dist/commands/install.js +87 -35
- package/dist/commands/license/activate.js +357 -0
- package/dist/commands/license/env.js +94 -0
- package/dist/commands/license/generate-id.js +107 -0
- package/dist/commands/license/id.js +52 -0
- package/dist/commands/license/index.js +20 -0
- package/dist/commands/license/plugins/clean.js +98 -0
- package/dist/commands/license/plugins/index.js +20 -0
- package/dist/commands/license/plugins/list.js +50 -0
- package/dist/commands/license/plugins/shared.js +325 -0
- package/dist/commands/license/plugins/sync.js +267 -0
- package/dist/commands/license/shared.js +414 -0
- package/dist/commands/license/status.js +50 -0
- package/dist/lib/api-client.js +74 -3
- package/dist/lib/app-managed-resources.js +10 -6
- package/dist/lib/app-runtime.js +29 -11
- package/dist/lib/auth-store.js +36 -68
- package/dist/lib/build-config.js +8 -0
- package/dist/lib/builtin-db.js +86 -0
- package/dist/lib/cli-config.js +176 -0
- package/dist/lib/cli-home.js +6 -21
- package/dist/lib/db-connection-check.js +178 -0
- package/dist/lib/env-config.js +7 -0
- package/dist/lib/generated-command.js +23 -3
- package/dist/lib/plugin-storage.js +127 -0
- package/dist/lib/prompt-validators.js +4 -4
- package/dist/lib/run-npm.js +53 -0
- package/dist/lib/runtime-env-vars.js +32 -0
- package/dist/lib/runtime-generator.js +89 -10
- package/dist/lib/self-manager.js +57 -2
- package/dist/lib/skills-manager.js +2 -2
- package/dist/lib/startup-update.js +85 -7
- package/dist/locale/en-US.json +16 -13
- package/dist/locale/zh-CN.json +16 -13
- package/nocobase-ctl.config.json +82 -0
- package/package.json +16 -4
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { Command, Flags } from '@oclif/core';
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { licenseEnvFlag, licenseJsonFlag, licensePkgUrlFlag, requireLicenseRuntime } from '../shared.js';
|
|
14
|
+
import { syncLicensedPlugins } from './shared.js';
|
|
15
|
+
import { resolvePluginStoragePath } from '../../../lib/plugin-storage.js';
|
|
16
|
+
import { commandOutput } from '../../../lib/run-npm.js';
|
|
17
|
+
import { startTask, stopTask, succeedTask, updateTask } from '../../../lib/ui.js';
|
|
18
|
+
const SYNC_LOADING_DELAY_MS = 1200;
|
|
19
|
+
const SYNC_LOADING_UPDATE_MS = 5000;
|
|
20
|
+
const LOCAL_CLI_PACKAGE_JSON_PATH = 'node_modules/@nocobase/cli/package.json';
|
|
21
|
+
const DOCKER_CLI_PACKAGE_JSON_PATH = '/opt/nb/node_modules/@nocobase/cli/package.json';
|
|
22
|
+
const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
|
|
23
|
+
const DEFAULT_DOCKER_VERSION = 'alpha';
|
|
24
|
+
function formatActionLabel(action) {
|
|
25
|
+
switch (action) {
|
|
26
|
+
case 'installed':
|
|
27
|
+
return pc.green('installed');
|
|
28
|
+
case 'updated':
|
|
29
|
+
return pc.cyan('updated');
|
|
30
|
+
case 'removed':
|
|
31
|
+
return pc.yellow('removed');
|
|
32
|
+
case 'skipped':
|
|
33
|
+
return pc.dim('skipped');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function trimValue(value) {
|
|
37
|
+
const text = String(value ?? '').trim();
|
|
38
|
+
return text || undefined;
|
|
39
|
+
}
|
|
40
|
+
function normalizeDockerPlatform(value) {
|
|
41
|
+
const text = trimValue(value);
|
|
42
|
+
if (!text || text === 'auto') {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (text === 'linux/amd64' || text === 'linux/arm64') {
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function normalizePluginRegistryVersion(version) {
|
|
51
|
+
const normalized = version.trim();
|
|
52
|
+
const rcMatch = normalized.match(/^(.+)-rc\.\d{8,}$/);
|
|
53
|
+
if (rcMatch) {
|
|
54
|
+
return rcMatch[1];
|
|
55
|
+
}
|
|
56
|
+
const betaMatch = normalized.match(/^(.+-beta\.\d+)\.\d{8,}$/);
|
|
57
|
+
if (betaMatch) {
|
|
58
|
+
return betaMatch[1];
|
|
59
|
+
}
|
|
60
|
+
const alphaMatch = normalized.match(/^(.+-alpha\.\d+)\.\d{8,}$/);
|
|
61
|
+
if (alphaMatch) {
|
|
62
|
+
return alphaMatch[1];
|
|
63
|
+
}
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
async function parseVersionFromPackageJson(content, sourceLabel) {
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(content);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error(`Failed to parse ${sourceLabel}.`);
|
|
73
|
+
}
|
|
74
|
+
const version = trimValue(parsed.version);
|
|
75
|
+
if (!version) {
|
|
76
|
+
throw new Error(`Missing version in ${sourceLabel}.`);
|
|
77
|
+
}
|
|
78
|
+
return version;
|
|
79
|
+
}
|
|
80
|
+
async function resolveLocalAppVersion(runtime) {
|
|
81
|
+
const packageJsonPath = path.join(runtime.projectRoot, LOCAL_CLI_PACKAGE_JSON_PATH);
|
|
82
|
+
let content;
|
|
83
|
+
try {
|
|
84
|
+
content = await readFile(packageJsonPath, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new Error(`Missing ${LOCAL_CLI_PACKAGE_JSON_PATH} for env "${runtime.envName}" at ${packageJsonPath}.`);
|
|
88
|
+
}
|
|
89
|
+
return await parseVersionFromPackageJson(content, packageJsonPath);
|
|
90
|
+
}
|
|
91
|
+
async function resolveDockerAppVersion(runtime) {
|
|
92
|
+
const config = runtime.env.config ?? {};
|
|
93
|
+
const imageRef = `${trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY}:${trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION}`;
|
|
94
|
+
const args = [
|
|
95
|
+
'run',
|
|
96
|
+
'--rm',
|
|
97
|
+
'--network',
|
|
98
|
+
runtime.workspaceName,
|
|
99
|
+
];
|
|
100
|
+
const dockerPlatform = normalizeDockerPlatform(config.dockerPlatform);
|
|
101
|
+
if (dockerPlatform) {
|
|
102
|
+
args.push('--platform', dockerPlatform);
|
|
103
|
+
}
|
|
104
|
+
args.push('--entrypoint', 'node', imageRef, '-p', `JSON.stringify(require(${JSON.stringify(DOCKER_CLI_PACKAGE_JSON_PATH)}).version)`);
|
|
105
|
+
let output;
|
|
106
|
+
try {
|
|
107
|
+
output = await commandOutput('docker', args, {
|
|
108
|
+
errorName: 'docker run',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
throw new Error(`Missing ${DOCKER_CLI_PACKAGE_JSON_PATH} for env "${runtime.envName}" inside Docker image ${imageRef}. ${message}`);
|
|
114
|
+
}
|
|
115
|
+
const version = trimValue(output.replace(/^"+|"+$/g, ''));
|
|
116
|
+
if (!version) {
|
|
117
|
+
throw new Error(`Missing version in ${DOCKER_CLI_PACKAGE_JSON_PATH} for env "${runtime.envName}" inside Docker image ${imageRef}.`);
|
|
118
|
+
}
|
|
119
|
+
return version;
|
|
120
|
+
}
|
|
121
|
+
async function resolveManagedAppVersion(runtime) {
|
|
122
|
+
if (runtime.kind === 'local') {
|
|
123
|
+
return await resolveLocalAppVersion(runtime);
|
|
124
|
+
}
|
|
125
|
+
if (runtime.kind === 'docker') {
|
|
126
|
+
return await resolveDockerAppVersion(runtime);
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Env "${runtime.envName}" does not support automatic app version detection.`);
|
|
129
|
+
}
|
|
130
|
+
export default class LicensePluginsSync extends Command {
|
|
131
|
+
static summary = 'Synchronize commercial plugins for the selected env';
|
|
132
|
+
static description = 'Synchronize the commercial plugins allowed by the current saved license key.';
|
|
133
|
+
static examples = [
|
|
134
|
+
'<%= config.bin %> <%= command.id %>',
|
|
135
|
+
'<%= config.bin %> <%= command.id %> --env app1',
|
|
136
|
+
'<%= config.bin %> <%= command.id %> --env app1 --dry-run',
|
|
137
|
+
'<%= config.bin %> <%= command.id %> --env app1 --json',
|
|
138
|
+
];
|
|
139
|
+
static flags = {
|
|
140
|
+
env: licenseEnvFlag,
|
|
141
|
+
json: licenseJsonFlag,
|
|
142
|
+
'pkg-url': licensePkgUrlFlag,
|
|
143
|
+
'dry-run': Flags.boolean({
|
|
144
|
+
description: 'Preview plugin changes without installing, upgrading, or removing anything',
|
|
145
|
+
default: false,
|
|
146
|
+
}),
|
|
147
|
+
version: Flags.string({
|
|
148
|
+
description: 'Registry version or dist-tag to synchronize. Defaults to the current workspace version.',
|
|
149
|
+
}),
|
|
150
|
+
verbose: Flags.boolean({
|
|
151
|
+
char: 'V',
|
|
152
|
+
description: 'Show detailed per-plugin sync logs',
|
|
153
|
+
default: false,
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
async run() {
|
|
157
|
+
const { flags } = await this.parse(LicensePluginsSync);
|
|
158
|
+
const runtime = await requireLicenseRuntime(flags.env);
|
|
159
|
+
const version = trimValue(flags.version) || await resolveManagedAppVersion(runtime);
|
|
160
|
+
const registryVersion = normalizePluginRegistryVersion(version);
|
|
161
|
+
const shouldStreamLogs = !flags.json && Boolean(flags.verbose);
|
|
162
|
+
const pluginStoragePath = resolvePluginStoragePath(runtime.env.storagePath);
|
|
163
|
+
const shouldShowLoading = !flags.json && !flags.verbose;
|
|
164
|
+
if (!flags.json) {
|
|
165
|
+
this.log(pc.bold(flags['dry-run']
|
|
166
|
+
? `Commercial plugin sync preview for env "${runtime.envName}"`
|
|
167
|
+
: `Commercial plugin sync for env "${runtime.envName}"`));
|
|
168
|
+
this.log(pc.dim(`App version: ${version}`));
|
|
169
|
+
if (registryVersion !== version) {
|
|
170
|
+
this.log(pc.dim(`Download version: ${registryVersion}`));
|
|
171
|
+
}
|
|
172
|
+
this.log(pc.dim(`Plugin storage path: ${pluginStoragePath}`));
|
|
173
|
+
}
|
|
174
|
+
let loadingStarted = false;
|
|
175
|
+
let loadingTimer;
|
|
176
|
+
let updateTimer;
|
|
177
|
+
let elapsedSeconds = 0;
|
|
178
|
+
if (shouldShowLoading) {
|
|
179
|
+
loadingTimer = setTimeout(() => {
|
|
180
|
+
loadingStarted = true;
|
|
181
|
+
elapsedSeconds = Math.floor(SYNC_LOADING_DELAY_MS / 1000);
|
|
182
|
+
startTask(flags['dry-run']
|
|
183
|
+
? 'Preparing commercial plugin sync preview. Please wait...'
|
|
184
|
+
: 'Synchronizing commercial plugins. Please wait...');
|
|
185
|
+
updateTimer = setInterval(() => {
|
|
186
|
+
elapsedSeconds += Math.floor(SYNC_LOADING_UPDATE_MS / 1000);
|
|
187
|
+
updateTask(flags['dry-run']
|
|
188
|
+
? `Preparing commercial plugin sync preview. Still working... (${elapsedSeconds}s elapsed)`
|
|
189
|
+
: `Synchronizing commercial plugins. Still working... (${elapsedSeconds}s elapsed)`);
|
|
190
|
+
}, SYNC_LOADING_UPDATE_MS);
|
|
191
|
+
}, SYNC_LOADING_DELAY_MS);
|
|
192
|
+
}
|
|
193
|
+
let result;
|
|
194
|
+
try {
|
|
195
|
+
result = await syncLicensedPlugins(runtime, {
|
|
196
|
+
pkgUrl: flags['pkg-url'],
|
|
197
|
+
version: registryVersion,
|
|
198
|
+
dryRun: Boolean(flags['dry-run']),
|
|
199
|
+
onProgress: shouldStreamLogs
|
|
200
|
+
? async (detail) => {
|
|
201
|
+
this.log(`${formatActionLabel(detail.action)} ${pc.bold(detail.packageName)}`);
|
|
202
|
+
this.log(pc.dim(` output: ${detail.outputDir}`));
|
|
203
|
+
if (detail.warning) {
|
|
204
|
+
this.log(pc.yellow(` warning: ${detail.warning}`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
: undefined,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
if (loadingTimer) {
|
|
212
|
+
clearTimeout(loadingTimer);
|
|
213
|
+
}
|
|
214
|
+
if (updateTimer) {
|
|
215
|
+
clearInterval(updateTimer);
|
|
216
|
+
}
|
|
217
|
+
if (loadingStarted) {
|
|
218
|
+
stopTask();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const payload = {
|
|
222
|
+
ok: true,
|
|
223
|
+
env: runtime.envName,
|
|
224
|
+
kind: runtime.kind,
|
|
225
|
+
dryRun: Boolean(flags['dry-run']),
|
|
226
|
+
version,
|
|
227
|
+
registryVersion,
|
|
228
|
+
...result,
|
|
229
|
+
};
|
|
230
|
+
if (flags.json) {
|
|
231
|
+
this.log(JSON.stringify(payload, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (loadingStarted) {
|
|
235
|
+
succeedTask(flags['dry-run']
|
|
236
|
+
? 'Commercial plugin sync preview is ready.'
|
|
237
|
+
: 'Commercial plugin sync completed.');
|
|
238
|
+
}
|
|
239
|
+
if (!flags.verbose) {
|
|
240
|
+
const changes = [];
|
|
241
|
+
if (result.installed.length > 0) {
|
|
242
|
+
changes.push(pc.green(`${result.installed.length} installed`));
|
|
243
|
+
}
|
|
244
|
+
if (result.updated.length > 0) {
|
|
245
|
+
changes.push(pc.cyan(`${result.updated.length} updated`));
|
|
246
|
+
}
|
|
247
|
+
if (result.removed.length > 0) {
|
|
248
|
+
changes.push(pc.yellow(`${result.removed.length} removed`));
|
|
249
|
+
}
|
|
250
|
+
if (result.skipped.length > 0) {
|
|
251
|
+
changes.push(pc.dim(`${result.skipped.length} skipped`));
|
|
252
|
+
}
|
|
253
|
+
if (changes.length === 0) {
|
|
254
|
+
changes.push(pc.dim('no plugin changes'));
|
|
255
|
+
}
|
|
256
|
+
this.log(`Result: ${changes.join(', ')}`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.log(`Summary: ${result.installed.length} installed, ${result.updated.length} updated, ${result.removed.length} removed, ${result.skipped.length} skipped, ${result.warnings.length} warnings`);
|
|
260
|
+
}
|
|
261
|
+
if (result.warnings.length > 0 && !flags.verbose) {
|
|
262
|
+
for (const warning of result.warnings) {
|
|
263
|
+
this.log(pc.yellow(`Warning: ${warning}`));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { Flags } from '@oclif/core';
|
|
10
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { getEnvAsync, getInstanceIdAsync, keyDecrypt } from '@nocobase/license-kit';
|
|
13
|
+
import _ from 'lodash';
|
|
14
|
+
import { checkExternalDbConnection, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
|
|
15
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
|
|
16
|
+
import { buildRuntimeEnvVars } from '../../lib/runtime-env-vars.js';
|
|
17
|
+
import { resolveLicensePkgUrlFromConfig } from '../../lib/cli-config.js';
|
|
18
|
+
import { commandOutput } from '../../lib/run-npm.js';
|
|
19
|
+
import { appUrl } from '../env/shared.js';
|
|
20
|
+
export const licenseEnvFlag = Flags.string({
|
|
21
|
+
char: 'e',
|
|
22
|
+
description: 'CLI env name (from `nb env` / `nb init`). Defaults to the current env when omitted',
|
|
23
|
+
});
|
|
24
|
+
export const licenseJsonFlag = Flags.boolean({
|
|
25
|
+
description: 'Output the result as JSON',
|
|
26
|
+
default: false,
|
|
27
|
+
});
|
|
28
|
+
const DEFAULT_LICENSE_PKG_URL = 'https://pkg.nocobase.com/';
|
|
29
|
+
const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
|
|
30
|
+
const DEFAULT_DOCKER_VERSION = 'alpha';
|
|
31
|
+
export const licensePkgUrlFlag = Flags.string({
|
|
32
|
+
description: 'Commercial package service base URL',
|
|
33
|
+
hidden: true,
|
|
34
|
+
});
|
|
35
|
+
export async function requireLicenseRuntime(envName) {
|
|
36
|
+
const runtime = await resolveManagedAppRuntime(envName);
|
|
37
|
+
if (!runtime) {
|
|
38
|
+
throw new Error(formatMissingManagedAppEnvMessage(envName));
|
|
39
|
+
}
|
|
40
|
+
return runtime;
|
|
41
|
+
}
|
|
42
|
+
export function resolveLicenseDir(runtime) {
|
|
43
|
+
return path.resolve(runtime.env.storagePath, '.license');
|
|
44
|
+
}
|
|
45
|
+
export function resolveInstanceIdFile(runtime) {
|
|
46
|
+
return path.resolve(resolveLicenseDir(runtime), 'instance-id');
|
|
47
|
+
}
|
|
48
|
+
export function resolveLicenseKeyFile(runtime) {
|
|
49
|
+
return path.resolve(resolveLicenseDir(runtime), 'license-key');
|
|
50
|
+
}
|
|
51
|
+
export async function readSavedInstanceId(runtime) {
|
|
52
|
+
try {
|
|
53
|
+
const value = await readFile(resolveInstanceIdFile(runtime), 'utf8');
|
|
54
|
+
const normalized = value.trim();
|
|
55
|
+
return normalized || undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function trimValue(value) {
|
|
62
|
+
const text = String(value ?? '').trim();
|
|
63
|
+
return text || undefined;
|
|
64
|
+
}
|
|
65
|
+
function normalizeDockerPlatform(value) {
|
|
66
|
+
const text = trimValue(value);
|
|
67
|
+
if (!text || text === 'auto') {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (text === 'linux/amd64' || text === 'linux/arm64') {
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function resolveDockerLicenseImageRef(runtime) {
|
|
76
|
+
const config = runtime.env.config ?? {};
|
|
77
|
+
return `${trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY}:${trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION}`;
|
|
78
|
+
}
|
|
79
|
+
function buildDockerLicenseDbFlagArgs(envVars) {
|
|
80
|
+
return [
|
|
81
|
+
'--db-dialect',
|
|
82
|
+
String(envVars.DB_DIALECT ?? ''),
|
|
83
|
+
'--db-host',
|
|
84
|
+
String(envVars.DB_HOST ?? ''),
|
|
85
|
+
'--db-port',
|
|
86
|
+
String(envVars.DB_PORT ?? ''),
|
|
87
|
+
'--db-database',
|
|
88
|
+
String(envVars.DB_DATABASE ?? ''),
|
|
89
|
+
'--db-user',
|
|
90
|
+
String(envVars.DB_USER ?? ''),
|
|
91
|
+
'--db-password',
|
|
92
|
+
String(envVars.DB_PASSWORD ?? ''),
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
async function runDockerLicenseJsonCommand(runtime, commandArgs) {
|
|
96
|
+
const args = [
|
|
97
|
+
'run',
|
|
98
|
+
'--rm',
|
|
99
|
+
'--network',
|
|
100
|
+
runtime.dockerNetworkName || runtime.workspaceName,
|
|
101
|
+
];
|
|
102
|
+
const dockerPlatform = normalizeDockerPlatform(runtime.env.config?.dockerPlatform);
|
|
103
|
+
if (dockerPlatform) {
|
|
104
|
+
args.push('--platform', dockerPlatform);
|
|
105
|
+
}
|
|
106
|
+
args.push('--entrypoint', 'nb', resolveDockerLicenseImageRef(runtime), ...commandArgs, '--json');
|
|
107
|
+
const output = await commandOutput('docker', args, {
|
|
108
|
+
errorName: 'docker run',
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(output);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error(`Failed to parse Docker license command response: ${output}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function buildLicenseDbConfigFromEnvVars(envVars) {
|
|
118
|
+
return {
|
|
119
|
+
builtinDb: false,
|
|
120
|
+
dbDialect: trimValue(envVars.DB_DIALECT),
|
|
121
|
+
dbHost: trimValue(envVars.DB_HOST),
|
|
122
|
+
dbPort: trimValue(envVars.DB_PORT),
|
|
123
|
+
dbDatabase: trimValue(envVars.DB_DATABASE),
|
|
124
|
+
dbUser: trimValue(envVars.DB_USER),
|
|
125
|
+
dbPassword: envVars.DB_PASSWORD,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export async function validateLicenseDbConnectionFromEnvVars(envVars) {
|
|
129
|
+
const connectionConfig = readExternalDbConnectionConfig(buildLicenseDbConfigFromEnvVars(envVars));
|
|
130
|
+
if (!connectionConfig) {
|
|
131
|
+
throw new Error('Unsupported or incomplete database settings for instance ID generation.');
|
|
132
|
+
}
|
|
133
|
+
const validationError = await checkExternalDbConnection(connectionConfig);
|
|
134
|
+
if (validationError) {
|
|
135
|
+
throw new Error(validationError);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function withLicenseEnvVars(nextEnv, task) {
|
|
139
|
+
const previous = {};
|
|
140
|
+
for (const [key, value] of Object.entries(nextEnv)) {
|
|
141
|
+
previous[key] = process.env[key];
|
|
142
|
+
process.env[key] = value;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return await task();
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
for (const key of Object.keys(nextEnv)) {
|
|
149
|
+
const value = previous[key];
|
|
150
|
+
if (value === undefined) {
|
|
151
|
+
delete process.env[key];
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
process.env[key] = value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function withLicenseEnv(runtime, task) {
|
|
160
|
+
return await withLicenseEnvVars(await buildRuntimeEnvVars(runtime), task);
|
|
161
|
+
}
|
|
162
|
+
export async function getCurrentLicenseEnv(runtime) {
|
|
163
|
+
if (runtime.kind === 'docker') {
|
|
164
|
+
const envVars = await buildRuntimeEnvVars(runtime);
|
|
165
|
+
const payload = await runDockerLicenseJsonCommand(runtime, [
|
|
166
|
+
'license',
|
|
167
|
+
'env',
|
|
168
|
+
...buildDockerLicenseDbFlagArgs(envVars),
|
|
169
|
+
]);
|
|
170
|
+
return payload?.env;
|
|
171
|
+
}
|
|
172
|
+
return await withLicenseEnv(runtime, async () => await getEnvAsync());
|
|
173
|
+
}
|
|
174
|
+
export async function generateInstanceIdFromEnvVars(envVars) {
|
|
175
|
+
const instanceId = String(await withLicenseEnvVars(envVars, async () => await getInstanceIdAsync())).trim();
|
|
176
|
+
if (!instanceId) {
|
|
177
|
+
throw new Error('Generated instance ID is empty.');
|
|
178
|
+
}
|
|
179
|
+
return instanceId;
|
|
180
|
+
}
|
|
181
|
+
export async function generateValidatedInstanceIdFromEnvVars(envVars) {
|
|
182
|
+
await validateLicenseDbConnectionFromEnvVars(envVars);
|
|
183
|
+
return await generateInstanceIdFromEnvVars(envVars);
|
|
184
|
+
}
|
|
185
|
+
async function generateInstanceIdForDockerRuntime(runtime) {
|
|
186
|
+
const envVars = await buildRuntimeEnvVars(runtime);
|
|
187
|
+
const payload = await runDockerLicenseJsonCommand(runtime, [
|
|
188
|
+
'license',
|
|
189
|
+
'generate-id',
|
|
190
|
+
...buildDockerLicenseDbFlagArgs(envVars),
|
|
191
|
+
]);
|
|
192
|
+
const instanceId = trimValue(payload.instanceId);
|
|
193
|
+
if (!instanceId) {
|
|
194
|
+
throw new Error('Docker instance ID generation did not return an instance ID.');
|
|
195
|
+
}
|
|
196
|
+
return instanceId;
|
|
197
|
+
}
|
|
198
|
+
export async function generateInstanceIdForRuntime(runtime) {
|
|
199
|
+
if (runtime.kind === 'docker') {
|
|
200
|
+
return await generateInstanceIdForDockerRuntime(runtime);
|
|
201
|
+
}
|
|
202
|
+
if (runtime.kind === 'local') {
|
|
203
|
+
return await generateValidatedInstanceIdFromEnvVars(await buildRuntimeEnvVars(runtime));
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`Env "${runtime.envName}" does not support automatic instance ID generation.`);
|
|
206
|
+
}
|
|
207
|
+
export async function saveInstanceId(runtime, instanceId) {
|
|
208
|
+
const normalized = String(instanceId ?? '').trim();
|
|
209
|
+
if (!normalized) {
|
|
210
|
+
throw new Error('Generated instance ID is empty.');
|
|
211
|
+
}
|
|
212
|
+
await mkdir(resolveLicenseDir(runtime), { recursive: true });
|
|
213
|
+
await writeFile(resolveInstanceIdFile(runtime), `${normalized}\n`);
|
|
214
|
+
return normalized;
|
|
215
|
+
}
|
|
216
|
+
export async function generateAndSaveInstanceId(runtime) {
|
|
217
|
+
const instanceId = await generateInstanceIdForRuntime(runtime);
|
|
218
|
+
return await saveInstanceId(runtime, instanceId);
|
|
219
|
+
}
|
|
220
|
+
export async function ensureInstanceId(runtime, options = {}) {
|
|
221
|
+
if (!options.force) {
|
|
222
|
+
const saved = await readSavedInstanceId(runtime);
|
|
223
|
+
if (saved) {
|
|
224
|
+
return saved;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return await generateAndSaveInstanceId(runtime);
|
|
228
|
+
}
|
|
229
|
+
export function parseLicenseKey(key) {
|
|
230
|
+
try {
|
|
231
|
+
return JSON.parse(keyDecrypt(key));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
throw new Error('invalid');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export async function saveLicenseKey(runtime, key) {
|
|
238
|
+
await mkdir(resolveLicenseDir(runtime), { recursive: true });
|
|
239
|
+
const filePath = resolveLicenseKeyFile(runtime);
|
|
240
|
+
await writeFile(filePath, key.trim());
|
|
241
|
+
return filePath;
|
|
242
|
+
}
|
|
243
|
+
export async function readSavedLicenseKey(runtime) {
|
|
244
|
+
try {
|
|
245
|
+
const value = await readFile(resolveLicenseKeyFile(runtime), 'utf8');
|
|
246
|
+
const normalized = value.trim();
|
|
247
|
+
return normalized || undefined;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function matchSingleDomain(licenseDomain, currentDomain) {
|
|
254
|
+
let hostname = '';
|
|
255
|
+
let port = '';
|
|
256
|
+
try {
|
|
257
|
+
const url = new URL(currentDomain);
|
|
258
|
+
hostname = url.hostname;
|
|
259
|
+
port = url.port ? `:${url.port}` : '';
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const fullDomain = hostname + port;
|
|
265
|
+
if (!licenseDomain.includes('*')) {
|
|
266
|
+
return fullDomain === licenseDomain;
|
|
267
|
+
}
|
|
268
|
+
const base = licenseDomain.replace('*', '');
|
|
269
|
+
return fullDomain.endsWith(base);
|
|
270
|
+
}
|
|
271
|
+
export function isDomainMatch(currentDomain, keyData) {
|
|
272
|
+
if (!keyData?.licenseKey?.domain || !currentDomain) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const licenseDomains = String(keyData.licenseKey.domain)
|
|
276
|
+
.split(',')
|
|
277
|
+
.map((value) => value.trim())
|
|
278
|
+
.filter(Boolean);
|
|
279
|
+
return licenseDomains.some((licenseDomain) => matchSingleDomain(licenseDomain, currentDomain));
|
|
280
|
+
}
|
|
281
|
+
export function isDbMatch(env, keyData) {
|
|
282
|
+
const currentDb = env?.db;
|
|
283
|
+
const licenseDb = keyData?.instanceData?.db;
|
|
284
|
+
if (!currentDb || !licenseDb) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
if (currentDb?.id && licenseDb?.id) {
|
|
288
|
+
return currentDb.id === licenseDb.id;
|
|
289
|
+
}
|
|
290
|
+
return _.isEqual(_.omit(currentDb, ['id']), _.omit(licenseDb, ['id']));
|
|
291
|
+
}
|
|
292
|
+
export function isSysMatch(env, keyData) {
|
|
293
|
+
const instance = keyData?.instanceData;
|
|
294
|
+
if (!env || !instance) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const normalize = (item) => ({
|
|
298
|
+
sys: item?.sys ?? null,
|
|
299
|
+
osVer: item?.osVer ?? null,
|
|
300
|
+
});
|
|
301
|
+
return _.isEqual(normalize(env), normalize(instance));
|
|
302
|
+
}
|
|
303
|
+
export async function getLicenseStatus(keyData) {
|
|
304
|
+
if (!keyData) {
|
|
305
|
+
return 'invalid';
|
|
306
|
+
}
|
|
307
|
+
if (keyData.licenseKey?.licenseStatus === 'invalid') {
|
|
308
|
+
return 'invalid';
|
|
309
|
+
}
|
|
310
|
+
const domain = String(keyData.service?.domain ?? '').trim();
|
|
311
|
+
const accessKeyId = String(keyData.accessKeyId ?? '').trim();
|
|
312
|
+
const accessKeySecret = String(keyData.accessKeySecret ?? '').trim();
|
|
313
|
+
if (!domain || !accessKeyId || !accessKeySecret) {
|
|
314
|
+
return 'active';
|
|
315
|
+
}
|
|
316
|
+
const controller = new AbortController();
|
|
317
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
318
|
+
try {
|
|
319
|
+
const response = await fetch(`${domain.replace(/\/$/, '')}/api/license_keys:getKeyStatus`, {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: {
|
|
322
|
+
'Content-Type': 'application/json',
|
|
323
|
+
...(keyData.service?.headers ?? {}),
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
access_key_id: accessKeyId,
|
|
327
|
+
access_key_secret: accessKeySecret,
|
|
328
|
+
}),
|
|
329
|
+
signal: controller.signal,
|
|
330
|
+
});
|
|
331
|
+
const payload = await response.json();
|
|
332
|
+
return payload?.data?.status === 'active' ? 'active' : 'invalid';
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return 'active';
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
clearTimeout(timeout);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export async function validateLicenseKey(runtime, key) {
|
|
342
|
+
let keyData;
|
|
343
|
+
let keyStatus;
|
|
344
|
+
try {
|
|
345
|
+
keyData = parseLicenseKey(key);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
keyStatus = 'invalid';
|
|
349
|
+
}
|
|
350
|
+
const currentEnv = await getCurrentLicenseEnv(runtime);
|
|
351
|
+
const currentDomain = appUrl(runtime);
|
|
352
|
+
const dbMatch = isDbMatch(currentEnv, keyData);
|
|
353
|
+
const sysMatch = isSysMatch(currentEnv, keyData);
|
|
354
|
+
const envMatch = dbMatch && sysMatch;
|
|
355
|
+
const domainMatch = isDomainMatch(currentDomain, keyData);
|
|
356
|
+
const licenseStatus = await getLicenseStatus(keyData);
|
|
357
|
+
return {
|
|
358
|
+
current: {
|
|
359
|
+
env: currentEnv,
|
|
360
|
+
domain: currentDomain ? new URL(currentDomain).host : '',
|
|
361
|
+
},
|
|
362
|
+
keyData,
|
|
363
|
+
keyStatus,
|
|
364
|
+
dbMatch,
|
|
365
|
+
sysMatch,
|
|
366
|
+
envMatch,
|
|
367
|
+
domainMatch,
|
|
368
|
+
licenseStatus,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
export function redactLicenseKey(value) {
|
|
372
|
+
const text = String(value ?? '').trim();
|
|
373
|
+
if (!text) {
|
|
374
|
+
return '';
|
|
375
|
+
}
|
|
376
|
+
if (text.length <= 8) {
|
|
377
|
+
return '*'.repeat(text.length);
|
|
378
|
+
}
|
|
379
|
+
return `${text.slice(0, 4)}...${text.slice(-4)}`;
|
|
380
|
+
}
|
|
381
|
+
export async function resolveLicenseServiceUrl(value) {
|
|
382
|
+
return (await resolveLicensePkgUrl(value)).replace(/\/+$/, '');
|
|
383
|
+
}
|
|
384
|
+
export async function resolveLicensePkgUrl(value) {
|
|
385
|
+
const normalized = String(value ?? '').trim() || await resolveLicensePkgUrlFromConfig();
|
|
386
|
+
return normalized.replace(/\/+$/, '') + '/';
|
|
387
|
+
}
|
|
388
|
+
function shouldRedactOutputKey(key) {
|
|
389
|
+
return /accesskeyid|accesskeysecret|secret|token|password|authorization/i.test(key);
|
|
390
|
+
}
|
|
391
|
+
function redactOutputValue(value) {
|
|
392
|
+
const text = String(value ?? '').trim();
|
|
393
|
+
if (!text) {
|
|
394
|
+
return '';
|
|
395
|
+
}
|
|
396
|
+
if (text.length <= 8) {
|
|
397
|
+
return '*'.repeat(text.length);
|
|
398
|
+
}
|
|
399
|
+
return `${text.slice(0, 2)}***${text.slice(-2)}`;
|
|
400
|
+
}
|
|
401
|
+
export function sanitizeLicenseOutput(value) {
|
|
402
|
+
if (Array.isArray(value)) {
|
|
403
|
+
return value.map((item) => sanitizeLicenseOutput(item));
|
|
404
|
+
}
|
|
405
|
+
if (value && typeof value === 'object') {
|
|
406
|
+
return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
|
|
407
|
+
key,
|
|
408
|
+
shouldRedactOutputKey(key)
|
|
409
|
+
? redactOutputValue(String(nestedValue ?? ''))
|
|
410
|
+
: sanitizeLicenseOutput(nestedValue),
|
|
411
|
+
]));
|
|
412
|
+
}
|
|
413
|
+
return value;
|
|
414
|
+
}
|