@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.
Files changed (57) hide show
  1. package/README.md +4 -4
  2. package/lib/config/configuration-chat-policy.js +2 -2
  3. package/lib/config/configuration-redaction.d.ts +2 -8
  4. package/lib/config/configuration-redaction.js +15 -68
  5. package/lib/config/console-schema.js +4 -3
  6. package/lib/config/environment-files.d.ts +1 -0
  7. package/lib/config/environment-files.js +4 -11
  8. package/lib/config/literal-secrets.d.ts +34 -0
  9. package/lib/config/literal-secrets.js +136 -0
  10. package/lib/config.d.ts +1 -0
  11. package/lib/config.js +1 -0
  12. package/lib/database/config.js +31 -0
  13. package/lib/database/console-role.d.ts +35 -0
  14. package/lib/database/console-role.js +108 -0
  15. package/lib/database/gateway-store.d.ts +43 -0
  16. package/lib/database/gateway-store.js +90 -0
  17. package/lib/database/index.d.ts +2 -0
  18. package/lib/database/index.js +2 -0
  19. package/lib/database/project-database.d.ts +45 -0
  20. package/lib/database/project-database.js +212 -6
  21. package/lib/http/console-rpc.d.ts +9 -1
  22. package/lib/http/gateway-console.d.ts +15 -8
  23. package/lib/http/gateway-console.js +128 -45
  24. package/lib/http/gateway-events.d.ts +5 -0
  25. package/lib/http/gateway-events.js +16 -3
  26. package/lib/http/gateway-http-server.js +11 -7
  27. package/lib/http/plugin-switch-rpc.d.ts +19 -0
  28. package/lib/http/plugin-switch-rpc.js +218 -0
  29. package/lib/http/start-gateway.js +15 -0
  30. package/lib/iam/index.d.ts +4 -3
  31. package/lib/iam/index.js +4 -3
  32. package/lib/iam/install.d.ts +17 -11
  33. package/lib/iam/install.js +39 -18
  34. package/lib/iam/legacy-import.d.ts +151 -0
  35. package/lib/iam/legacy-import.js +600 -0
  36. package/lib/iam/local-admin.d.ts +2 -0
  37. package/lib/iam/local-admin.js +6 -1
  38. package/lib/iam/routes.js +10 -2
  39. package/lib/iam/rpc.js +1 -1
  40. package/lib/iam/service.d.ts +31 -0
  41. package/lib/iam/service.js +49 -4
  42. package/lib/iam/setup-service.d.ts +2 -0
  43. package/lib/iam/setup-service.js +11 -4
  44. package/lib/iam/tables.d.ts +6 -6
  45. package/lib/iam/tables.js +6 -8
  46. package/lib/local/handler.js +1 -1
  47. package/lib/local/protocol.d.ts +6 -0
  48. package/lib/local/protocol.js +6 -0
  49. package/lib/packages/authorization.d.ts +2 -1
  50. package/lib/packages/authorization.js +20 -1
  51. package/lib/packages/rpc.js +5 -0
  52. package/lib/packages/service.d.ts +18 -0
  53. package/lib/packages/service.js +66 -3
  54. package/lib/plugin-management/config-explanation.js +3 -3
  55. package/lib/plugin-management/workflow.d.ts +44 -0
  56. package/lib/plugin-management/workflow.js +109 -3
  57. 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
- await access(join(pkg.root, pkg.packageJson.zhin.entry));
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
- await access(join(installedPackage.root, installedPackage.packageJson.zhin.entry));
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.1.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.0",
76
- "@zhin.js/database": "1.1.8",
77
- "@zhin.js/host-http": "1.2.0",
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.0",
80
- "@zhin.js/runtime": "1.2.0",
81
- "@zhin.js/scaffold-wizard": "1.1.19"
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",