@zhin.js/gateway 1.1.1 → 1.2.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/README.md +4 -4
- package/lib/config/configuration-chat-policy.js +2 -2
- package/lib/config/configuration-redaction.d.ts +2 -8
- package/lib/config/configuration-redaction.js +15 -68
- package/lib/config/console-schema.js +4 -3
- package/lib/config/environment-files.d.ts +1 -0
- package/lib/config/environment-files.js +4 -11
- package/lib/config/literal-secrets.d.ts +34 -0
- package/lib/config/literal-secrets.js +136 -0
- package/lib/config.d.ts +1 -0
- package/lib/config.js +1 -0
- package/lib/database/config.js +31 -0
- package/lib/database/console-role.d.ts +35 -0
- package/lib/database/console-role.js +108 -0
- package/lib/database/gateway-store.d.ts +43 -0
- package/lib/database/gateway-store.js +90 -0
- package/lib/database/index.d.ts +2 -0
- package/lib/database/index.js +2 -0
- package/lib/database/project-database.d.ts +45 -0
- package/lib/database/project-database.js +212 -6
- package/lib/http/console-rpc.d.ts +9 -1
- package/lib/http/gateway-console.d.ts +15 -8
- package/lib/http/gateway-console.js +128 -45
- package/lib/http/gateway-events.d.ts +5 -0
- package/lib/http/gateway-events.js +16 -3
- package/lib/http/gateway-http-server.js +11 -7
- package/lib/http/plugin-switch-rpc.d.ts +19 -0
- package/lib/http/plugin-switch-rpc.js +218 -0
- package/lib/http/start-gateway.js +15 -0
- package/lib/iam/index.d.ts +4 -3
- package/lib/iam/index.js +4 -3
- package/lib/iam/install.d.ts +17 -11
- package/lib/iam/install.js +39 -18
- package/lib/iam/legacy-import.d.ts +151 -0
- package/lib/iam/legacy-import.js +600 -0
- package/lib/iam/local-admin.d.ts +2 -0
- package/lib/iam/local-admin.js +6 -1
- package/lib/iam/routes.js +10 -2
- package/lib/iam/rpc.js +1 -1
- package/lib/iam/service.d.ts +31 -0
- package/lib/iam/service.js +49 -4
- package/lib/iam/setup-service.d.ts +2 -0
- package/lib/iam/setup-service.js +11 -4
- package/lib/iam/tables.d.ts +6 -6
- package/lib/iam/tables.js +6 -8
- package/lib/local/handler.js +1 -1
- package/lib/local/protocol.d.ts +6 -0
- package/lib/local/protocol.js +6 -0
- package/lib/packages/authorization.d.ts +2 -1
- package/lib/packages/authorization.js +20 -1
- package/lib/packages/rpc.js +5 -0
- package/lib/packages/service.d.ts +18 -0
- package/lib/packages/service.js +66 -3
- package/lib/plugin-management/config-explanation.js +3 -3
- package/lib/plugin-management/workflow.d.ts +44 -0
- package/lib/plugin-management/workflow.js +109 -3
- package/package.json +7 -7
|
@@ -9,7 +9,7 @@ import { featureId, pluginVerificationToken, rootPluginId, readPluginConfigurati
|
|
|
9
9
|
import { ConsoleConfigurationStore } from '../config/configuration.js';
|
|
10
10
|
import { explainConfigFields } from './config-explanation.js';
|
|
11
11
|
import { resolveCommand } from '../onboarding/package-manager.js';
|
|
12
|
-
import { readPluginLifecycleState, resolvePluginLifecycleFile } from '@zhin.js/runtime';
|
|
12
|
+
import { inferConventionFeatureReferences, readPluginLifecycleState, resolvePluginLifecycleFile } from '@zhin.js/runtime';
|
|
13
13
|
const execute = promisify(execFile);
|
|
14
14
|
// Narrow local view of the `@zhin.js/adapter` index projection: the gateway must not import IM layers (ADR 0054 §0).
|
|
15
15
|
const adapterFeatureId = featureId('zhin.adapter');
|
|
@@ -28,6 +28,21 @@ export class PluginDeclarationError extends Error {
|
|
|
28
28
|
this.missingEnv = Object.freeze([...missingEnv]);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/** An enable / disable refused before anything was written (invalid target or configuration). */
|
|
32
|
+
export class PluginEnablementError extends Error {
|
|
33
|
+
code;
|
|
34
|
+
errors;
|
|
35
|
+
missingEnv;
|
|
36
|
+
constructor(code, message, errors = [], missingEnv = []) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'PluginEnablementError';
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.errors = Object.freeze(errors.map(error => Object.freeze({ ...error })));
|
|
41
|
+
this.missingEnv = Object.freeze([...missingEnv]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Keys that never name a plugin instance (Host sections and the declaration protocol). */
|
|
45
|
+
const RESERVED_INSTANCE_KEYS = new Set([...HOST_CONFIG_KEYS, 'configVersion', 'plugins', 'disable_plugins', 'plugin', 'root']);
|
|
31
46
|
/** One installation authority for CLI and Console. No generation or package parser of its own. */
|
|
32
47
|
export class PluginManagementWorkflow {
|
|
33
48
|
#root;
|
|
@@ -133,7 +148,9 @@ export class PluginManagementWorkflow {
|
|
|
133
148
|
try {
|
|
134
149
|
if (pkg.packageJson.zhin.type !== 'plugin')
|
|
135
150
|
throw new Error(`${packageName} is a Feature package, not a Plugin`);
|
|
136
|
-
|
|
151
|
+
// An entry-less Plugin has no entry file; its manifest alone makes it a Plugin.
|
|
152
|
+
if (pkg.packageJson.zhin.entry !== undefined)
|
|
153
|
+
await access(join(pkg.root, pkg.packageJson.zhin.entry));
|
|
137
154
|
return { installed: true, plugin: true, version: pkg.packageJson.version ?? null, error: undefined, pkg };
|
|
138
155
|
}
|
|
139
156
|
catch (error) {
|
|
@@ -164,7 +181,9 @@ export class PluginManagementWorkflow {
|
|
|
164
181
|
const installedPackage = await resolver.loadPackage(directory);
|
|
165
182
|
if (installedPackage.packageJson.zhin.type !== 'plugin')
|
|
166
183
|
throw new Error(`${packageName} is a Feature package, not a Plugin`);
|
|
167
|
-
|
|
184
|
+
const entry = installedPackage.packageJson.zhin.entry;
|
|
185
|
+
if (entry !== undefined)
|
|
186
|
+
await access(join(installedPackage.root, entry));
|
|
168
187
|
const declarationMissing = resolutionError instanceof PackageResolutionError && resolutionError.message.includes('does not declare');
|
|
169
188
|
return { installed: true, plugin: true, version, error: declarationMissing ? errorMessage(resolutionError) : undefined, pkg: installedPackage };
|
|
170
189
|
}
|
|
@@ -561,6 +580,90 @@ export class PluginManagementWorkflow {
|
|
|
561
580
|
return Object.freeze({ packageName: plan.packageName, instanceKey: plan.instanceKey, restartRequired: state.restartRequired, state });
|
|
562
581
|
}, request);
|
|
563
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Validates enabling / disabling one declared instance (an instance key, or a package name
|
|
585
|
+
* declared exactly once) without writing. Enabling needs an installed plugin package whose
|
|
586
|
+
* dependencies and configuration pass; disabling also accepts a declared package that is no
|
|
587
|
+
* longer installed, so a broken instance can be taken out of the next generation.
|
|
588
|
+
*/
|
|
589
|
+
async planSetEnabled(request, enabled) {
|
|
590
|
+
if (typeof request !== 'string' || !request.trim() || request.length > 256)
|
|
591
|
+
throw new PluginEnablementError('invalid-request', '请提供插件包名或 instanceKey');
|
|
592
|
+
if (typeof enabled !== 'boolean')
|
|
593
|
+
throw new PluginEnablementError('invalid-request', 'enabled 必须是布尔值');
|
|
594
|
+
const key = request.trim().replace(/^root\//u, '');
|
|
595
|
+
if (RESERVED_INSTANCE_KEYS.has(key))
|
|
596
|
+
throw new PluginEnablementError('reserved', `${key} 是保留配置键,不是插件实例`);
|
|
597
|
+
const { project } = await this.#project();
|
|
598
|
+
if (project.name !== undefined && key === project.name)
|
|
599
|
+
throw new PluginEnablementError('root', '项目根插件始终加载,不能启用或停用');
|
|
600
|
+
let target;
|
|
601
|
+
let declared;
|
|
602
|
+
try {
|
|
603
|
+
const entries = await this.#declaredEntries();
|
|
604
|
+
target = await this.#resolveTarget(key, entries);
|
|
605
|
+
declared = entries.some(entry => entry.package === target.packageName && entry.instanceKey === target.instanceKey);
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
throw new PluginEnablementError('invalid-request', errorMessage(error), [], []);
|
|
609
|
+
}
|
|
610
|
+
if (!declared)
|
|
611
|
+
throw new PluginEnablementError('not-declared', `插件实例 ${target.instanceKey}(${target.packageName})未在配置中声明;先运行 zhin install ${target.packageName}`);
|
|
612
|
+
const state = await this.inspect(target.instanceKey);
|
|
613
|
+
if (enabled && (!state.installed || !state.plugin)) {
|
|
614
|
+
throw new PluginEnablementError('not-installed', `${state.packageName} ${state.installed ? `不是可加载的 Plugin 包${state.issues[0] ? `:${state.issues[0]}` : ''}` : '未安装'};先运行 zhin install ${state.packageName}`);
|
|
615
|
+
}
|
|
616
|
+
const configuration = this.#configuration();
|
|
617
|
+
if (!configuration)
|
|
618
|
+
throw new Error('Missing configVersion: 2 configuration; run zhin setup');
|
|
619
|
+
const current = await configuration.document.read();
|
|
620
|
+
if (current.revision !== state.revision)
|
|
621
|
+
throw new Error('配置在读取期间变化,请重试');
|
|
622
|
+
const declaration = readApplicationPluginDeclaration(current.document);
|
|
623
|
+
const wasDisabled = declaration.disabled.includes(state.instanceKey);
|
|
624
|
+
const changed = enabled === wasDisabled;
|
|
625
|
+
const disabled = enabled ? declaration.disabled.filter(item => item !== state.instanceKey)
|
|
626
|
+
: wasDisabled ? declaration.disabled : [...declaration.disabled, state.instanceKey];
|
|
627
|
+
if (enabled && changed) {
|
|
628
|
+
const candidate = { ...current.document, disable_plugins: disabled };
|
|
629
|
+
readApplicationPluginDeclaration(candidate);
|
|
630
|
+
try {
|
|
631
|
+
await this.#checkDependencies(undefined, candidate);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
throw new PluginEnablementError('invalid-config', `依赖检查未通过,未启用:${errorMessage(error)}`);
|
|
635
|
+
}
|
|
636
|
+
const validation = await configuration.store.validatePluginConfig(state.instanceKey, readPluginConfigurationMap(candidate)[state.instanceKey] ?? {}, candidate);
|
|
637
|
+
if (!validation.valid || validation.missingEnv.length) {
|
|
638
|
+
throw new PluginEnablementError('invalid-config', `配置未通过插件 Schema 校验,未启用:${[
|
|
639
|
+
...validation.errors.map(error => `${error.path} ${error.message}`),
|
|
640
|
+
...validation.missingEnv.map(name => `缺少环境变量 ${name}`)
|
|
641
|
+
].join(';')}`, validation.errors, validation.missingEnv);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return Object.freeze({ packageName: state.packageName, instanceKey: state.instanceKey, enabled, changed,
|
|
645
|
+
disabled: Object.freeze([...disabled]), projectRevision: state.projectRevision, configurationRevision: current.revision,
|
|
646
|
+
restartRequired: changed || state.restartRequired });
|
|
647
|
+
}
|
|
648
|
+
/** Writes `disable_plugins` in one atomic config write; already in the requested state → nothing written. */
|
|
649
|
+
async setEnabled(request, enabled, expectedRevision, expectedProjectRevision) {
|
|
650
|
+
return this.#mutate(async (completed) => {
|
|
651
|
+
await this.#assertRevision(expectedRevision, expectedProjectRevision);
|
|
652
|
+
const plan = await this.planSetEnabled(request, enabled);
|
|
653
|
+
if (plan.changed) {
|
|
654
|
+
const configuration = this.#configuration();
|
|
655
|
+
if (!configuration)
|
|
656
|
+
throw new Error('Missing configVersion: 2 configuration; run zhin setup');
|
|
657
|
+
const current = await configuration.document.read();
|
|
658
|
+
if (current.revision !== plan.configurationRevision)
|
|
659
|
+
throw new Error('配置在校验期间变化,请刷新后重试');
|
|
660
|
+
await (await configuration.document.prepare(current, [{ op: 'set', path: ['disable_plugins'], value: [...plan.disabled] }])).commit();
|
|
661
|
+
completed.push(enabled ? 'plugin-enable' : 'plugin-disable');
|
|
662
|
+
}
|
|
663
|
+
const state = await this.inspect(plan.instanceKey);
|
|
664
|
+
return Object.freeze({ plan, changed: plan.changed, restartRequired: plan.changed || state.restartRequired, state });
|
|
665
|
+
}, request);
|
|
666
|
+
}
|
|
564
667
|
async planUninstall(request) {
|
|
565
668
|
const state = await this.inspect(request);
|
|
566
669
|
const configuration = this.#configuration();
|
|
@@ -609,6 +712,9 @@ export class PluginManagementWorkflow {
|
|
|
609
712
|
return false;
|
|
610
713
|
if (pkg.packageJson.zhin.features.some(item => item.package === packageName))
|
|
611
714
|
return true;
|
|
715
|
+
// A convention directory implies its Feature package even when zhin.features omits it.
|
|
716
|
+
if (inferConventionFeatureReferences(pkg.root, pkg.packageJson.zhin).some(item => item.package === packageName))
|
|
717
|
+
return true;
|
|
612
718
|
if (!isRoot && [pkg.packageJson.dependencies, pkg.packageJson.optionalDependencies, pkg.packageJson.peerDependencies].some(deps => deps?.[packageName]))
|
|
613
719
|
return true;
|
|
614
720
|
const entries = isRoot ? await this.#declaredEntries() : [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/gateway",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Zhin.js management gateway: Console access control, terminal, project configuration and instance supervision (ADR 0054)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -72,13 +72,13 @@
|
|
|
72
72
|
"fs-extra": "^11.4.0",
|
|
73
73
|
"tsx": "^4.23.1",
|
|
74
74
|
"yaml": "^2.9.0",
|
|
75
|
-
"@zhin.js/console-protocol": "1.2.
|
|
76
|
-
"@zhin.js/database": "1.
|
|
77
|
-
"@zhin.js/host-http": "1.2.
|
|
75
|
+
"@zhin.js/console-protocol": "1.2.2",
|
|
76
|
+
"@zhin.js/database": "1.2.0",
|
|
77
|
+
"@zhin.js/host-http": "1.2.2",
|
|
78
78
|
"@zhin.js/logger": "1.1.3",
|
|
79
|
-
"@zhin.js/plugin-runtime": "1.2.
|
|
80
|
-
"@zhin.js/runtime": "1.2.
|
|
81
|
-
"@zhin.js/scaffold-wizard": "1.1.
|
|
79
|
+
"@zhin.js/plugin-runtime": "1.2.1",
|
|
80
|
+
"@zhin.js/runtime": "1.2.2",
|
|
81
|
+
"@zhin.js/scaffold-wizard": "1.1.21"
|
|
82
82
|
},
|
|
83
83
|
"optionalDependencies": {
|
|
84
84
|
"node-pty": "^1.0.0",
|