@zhin.js/gateway 0.0.1-bootstrap.0 → 1.1.1

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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/lib/config/ai-config-loader.d.ts +22 -0
  4. package/lib/config/ai-config-loader.js +96 -0
  5. package/lib/config/configuration-chat-policy.d.ts +57 -0
  6. package/lib/config/configuration-chat-policy.js +423 -0
  7. package/lib/config/configuration-patch.d.ts +16 -0
  8. package/lib/config/configuration-patch.js +105 -0
  9. package/lib/config/configuration-path.d.ts +6 -0
  10. package/lib/config/configuration-path.js +40 -0
  11. package/lib/config/configuration-projection.d.ts +6 -0
  12. package/lib/config/configuration-projection.js +28 -0
  13. package/lib/config/configuration-redaction.d.ts +24 -0
  14. package/lib/config/configuration-redaction.js +128 -0
  15. package/lib/config/configuration.d.ts +81 -0
  16. package/lib/config/configuration.js +686 -0
  17. package/lib/config/console-schema.d.ts +11 -0
  18. package/lib/config/console-schema.js +222 -0
  19. package/lib/config/environment-files.d.ts +2 -0
  20. package/lib/config/environment-files.js +21 -0
  21. package/lib/config/plugin-package-map.d.ts +2 -0
  22. package/lib/config/plugin-package-map.js +14 -0
  23. package/lib/config/plugin-schema-catalog.d.ts +22 -0
  24. package/lib/config/plugin-schema-catalog.js +262 -0
  25. package/lib/config.d.ts +12 -0
  26. package/lib/config.js +12 -0
  27. package/lib/database/config.d.ts +22 -0
  28. package/lib/database/config.js +159 -0
  29. package/lib/database/index.d.ts +2 -0
  30. package/lib/database/index.js +2 -0
  31. package/lib/database/project-database.d.ts +31 -0
  32. package/lib/database/project-database.js +130 -0
  33. package/lib/database.d.ts +2 -0
  34. package/lib/database.js +2 -0
  35. package/lib/http/console-rpc.d.ts +19 -0
  36. package/lib/http/console-rpc.js +51 -0
  37. package/lib/http/gateway-console.d.ts +63 -0
  38. package/lib/http/gateway-console.js +309 -0
  39. package/lib/http/gateway-events.d.ts +35 -0
  40. package/lib/http/gateway-events.js +172 -0
  41. package/lib/http/gateway-http-server.d.ts +95 -0
  42. package/lib/http/gateway-http-server.js +518 -0
  43. package/lib/http/local-principal.d.ts +2 -0
  44. package/lib/http/local-principal.js +16 -0
  45. package/lib/http/run-gateway.d.ts +43 -0
  46. package/lib/http/run-gateway.js +268 -0
  47. package/lib/http/start-gateway.d.ts +114 -0
  48. package/lib/http/start-gateway.js +220 -0
  49. package/lib/http-response.d.ts +6 -0
  50. package/lib/http-response.js +25 -0
  51. package/lib/http.d.ts +6 -0
  52. package/lib/http.js +6 -0
  53. package/lib/iam/crypto.d.ts +12 -0
  54. package/lib/iam/crypto.js +58 -0
  55. package/lib/iam/index.d.ts +8 -0
  56. package/lib/iam/index.js +8 -0
  57. package/lib/iam/install.d.ts +36 -0
  58. package/lib/iam/install.js +74 -0
  59. package/lib/iam/local-admin.d.ts +27 -0
  60. package/lib/iam/local-admin.js +102 -0
  61. package/lib/iam/rate-limit.d.ts +15 -0
  62. package/lib/iam/rate-limit.js +52 -0
  63. package/lib/iam/routes.d.ts +23 -0
  64. package/lib/iam/routes.js +157 -0
  65. package/lib/iam/rpc.d.ts +14 -0
  66. package/lib/iam/rpc.js +100 -0
  67. package/lib/iam/service.d.ts +203 -0
  68. package/lib/iam/service.js +784 -0
  69. package/lib/iam/setup-service.d.ts +83 -0
  70. package/lib/iam/setup-service.js +206 -0
  71. package/lib/iam/tables.d.ts +20 -0
  72. package/lib/iam/tables.js +82 -0
  73. package/lib/iam.d.ts +2 -0
  74. package/lib/iam.js +2 -0
  75. package/lib/index.d.ts +22 -0
  76. package/lib/index.js +22 -0
  77. package/lib/local/client.d.ts +71 -0
  78. package/lib/local/client.js +296 -0
  79. package/lib/local/handler.d.ts +30 -0
  80. package/lib/local/handler.js +86 -0
  81. package/lib/local/local-socket.d.ts +55 -0
  82. package/lib/local/local-socket.js +232 -0
  83. package/lib/local/paths.d.ts +34 -0
  84. package/lib/local/paths.js +58 -0
  85. package/lib/local/protocol.d.ts +140 -0
  86. package/lib/local/protocol.js +90 -0
  87. package/lib/local/state-file.d.ts +8 -0
  88. package/lib/local/state-file.js +45 -0
  89. package/lib/local.d.ts +10 -0
  90. package/lib/local.js +10 -0
  91. package/lib/onboarding/allowlist.d.ts +54 -0
  92. package/lib/onboarding/allowlist.js +106 -0
  93. package/lib/onboarding/errors.d.ts +7 -0
  94. package/lib/onboarding/errors.js +17 -0
  95. package/lib/onboarding/index.d.ts +6 -0
  96. package/lib/onboarding/index.js +6 -0
  97. package/lib/onboarding/install.d.ts +99 -0
  98. package/lib/onboarding/install.js +132 -0
  99. package/lib/onboarding/package-manager.d.ts +47 -0
  100. package/lib/onboarding/package-manager.js +179 -0
  101. package/lib/onboarding/service.d.ts +53 -0
  102. package/lib/onboarding/service.js +144 -0
  103. package/lib/onboarding/state-store.d.ts +26 -0
  104. package/lib/onboarding/state-store.js +97 -0
  105. package/lib/onboarding.d.ts +2 -0
  106. package/lib/onboarding.js +2 -0
  107. package/lib/packages/authorization.d.ts +23 -0
  108. package/lib/packages/authorization.js +109 -0
  109. package/lib/packages/errors.d.ts +11 -0
  110. package/lib/packages/errors.js +26 -0
  111. package/lib/packages/index.d.ts +5 -0
  112. package/lib/packages/index.js +5 -0
  113. package/lib/packages/rpc.d.ts +30 -0
  114. package/lib/packages/rpc.js +155 -0
  115. package/lib/packages/service.d.ts +121 -0
  116. package/lib/packages/service.js +441 -0
  117. package/lib/packages/snapshot.d.ts +14 -0
  118. package/lib/packages/snapshot.js +73 -0
  119. package/lib/packages.d.ts +2 -0
  120. package/lib/packages.js +2 -0
  121. package/lib/plugin-management/activity.d.ts +6 -0
  122. package/lib/plugin-management/activity.js +14 -0
  123. package/lib/plugin-management/config-explanation.d.ts +6 -0
  124. package/lib/plugin-management/config-explanation.js +113 -0
  125. package/lib/plugin-management/use-cases.d.ts +15 -0
  126. package/lib/plugin-management/use-cases.js +120 -0
  127. package/lib/plugin-management/workflow.d.ts +92 -0
  128. package/lib/plugin-management/workflow.js +772 -0
  129. package/lib/plugin-management.d.ts +5 -0
  130. package/lib/plugin-management.js +5 -0
  131. package/lib/project/config-file.d.ts +6 -0
  132. package/lib/project/config-file.js +58 -0
  133. package/lib/project/environment.d.ts +13 -0
  134. package/lib/project/environment.js +52 -0
  135. package/lib/project/instance-declarations.d.ts +55 -0
  136. package/lib/project/instance-declarations.js +542 -0
  137. package/lib/project/legacy-config.d.ts +25 -0
  138. package/lib/project/legacy-config.js +70 -0
  139. package/lib/project/node-requirements.d.ts +7 -0
  140. package/lib/project/node-requirements.js +20 -0
  141. package/lib/project/tsx-module-loader.d.ts +7 -0
  142. package/lib/project/tsx-module-loader.js +29 -0
  143. package/lib/project.d.ts +7 -0
  144. package/lib/project.js +7 -0
  145. package/lib/server.d.ts +11 -0
  146. package/lib/server.js +11 -0
  147. package/lib/supervisor/instance-logs.d.ts +39 -0
  148. package/lib/supervisor/instance-logs.js +152 -0
  149. package/lib/supervisor/instance-supervisor.d.ts +140 -0
  150. package/lib/supervisor/instance-supervisor.js +408 -0
  151. package/lib/supervisor/run-instance.d.ts +35 -0
  152. package/lib/supervisor/run-instance.js +161 -0
  153. package/lib/supervisor/terminal-state.d.ts +30 -0
  154. package/lib/supervisor/terminal-state.js +57 -0
  155. package/lib/supervisor.d.ts +4 -0
  156. package/lib/supervisor.js +4 -0
  157. package/lib/terminal/backends.d.ts +38 -0
  158. package/lib/terminal/backends.js +193 -0
  159. package/lib/terminal/config.d.ts +63 -0
  160. package/lib/terminal/config.js +237 -0
  161. package/lib/terminal/host.d.ts +26 -0
  162. package/lib/terminal/host.js +76 -0
  163. package/lib/terminal/index.d.ts +60 -0
  164. package/lib/terminal/index.js +135 -0
  165. package/lib/terminal/modules.d.ts +87 -0
  166. package/lib/terminal/modules.js +44 -0
  167. package/lib/terminal/service.d.ts +101 -0
  168. package/lib/terminal/service.js +455 -0
  169. package/lib/terminal/tickets.d.ts +46 -0
  170. package/lib/terminal/tickets.js +76 -0
  171. package/lib/terminal.d.ts +3 -0
  172. package/lib/terminal.js +3 -0
  173. package/lib/usage/aggregate.d.ts +87 -0
  174. package/lib/usage/aggregate.js +212 -0
  175. package/lib/usage.d.ts +2 -0
  176. package/lib/usage.js +2 -0
  177. package/package.json +117 -4
@@ -0,0 +1,53 @@
1
+ import type { ConsolePrincipal } from '@zhin.js/host-http';
2
+ import { type PluginManagementOptions } from '../plugin-management/workflow.js';
3
+ import { type InstallAllowlist, type InstallPrincipal, type MarketCatalogEntry } from './allowlist.js';
4
+ import { InstallOutputRelay, type InstallEvent, type InstallListener, type InstallReport, type PackageInstallPort } from './install.js';
5
+ import { OnboardingStateStore, type OnboardingState } from './state-store.js';
6
+ export interface OnboardingStatus extends OnboardingState {
7
+ /** True until `complete()` ran. */
8
+ readonly required: boolean;
9
+ }
10
+ export interface OnboardingServiceOptions {
11
+ readonly projectRoot: string;
12
+ /** True once an enabled global owner exists (IAM). */
13
+ readonly hasOwner: () => boolean | Promise<boolean>;
14
+ /** Root config document used for auto-detection; defaults to the project's YAML/JSON config. */
15
+ readonly readConfig?: () => Readonly<Record<string, unknown>> | Promise<Readonly<Record<string, unknown>>>;
16
+ readonly store?: OnboardingStateStore;
17
+ /** Plugin-market entries joining the install allowlist (re-read on every install). */
18
+ readonly marketCatalog?: () => readonly MarketCatalogEntry[] | Promise<readonly MarketCatalogEntry[]>;
19
+ /**
20
+ * Installation port; defaults to a `PluginManagementWorkflow` on the project root whose
21
+ * package manager streams through `relay`.
22
+ */
23
+ readonly installer?: PackageInstallPort;
24
+ /** Options for the default workflow (activity guard, snapshots, restart request). */
25
+ readonly workflow?: Omit<PluginManagementOptions, 'installDependencies'>;
26
+ readonly now?: () => Date;
27
+ }
28
+ export interface MarkStepOptions {
29
+ /** When given (Web / RPC), the step's credential rule is enforced; the CLI is trusted locally. */
30
+ readonly principal?: ConsolePrincipal;
31
+ }
32
+ /**
33
+ * The one onboarding service (ADR 0053 §2/§5): Console and `zhin setup` / `zhin console init`
34
+ * call the same methods. Transport-independent; routes / RPC wrap it.
35
+ */
36
+ export declare class OnboardingService {
37
+ #private;
38
+ readonly relay: InstallOutputRelay;
39
+ constructor(options: OnboardingServiceOptions);
40
+ status(): Promise<OnboardingStatus>;
41
+ /** Marks database / ai / adapters as done or skipped. The owner step is derived from IAM. */
42
+ markStep(step: unknown, value: unknown, options?: MarkStepOptions): Promise<OnboardingStatus>;
43
+ /** Step 5: requires an owner; remaining pending optional steps stay pending (can be resumed later). */
44
+ complete(options?: MarkStepOptions): Promise<OnboardingStatus>;
45
+ /** The current install allowlist (official catalogs + market entries). */
46
+ allowlist(): Promise<InstallAllowlist>;
47
+ /** Authorizes then installs; progress goes to `onEvent`. Authorization errors throw before any work. */
48
+ installPackages(packages: readonly unknown[], principal: InstallPrincipal | undefined, onEvent?: InstallListener): Promise<InstallReport>;
49
+ /** Same as `installPackages`, as an async iterator of progress events (ends with `done`). */
50
+ streamInstall(packages: readonly unknown[], principal: InstallPrincipal | undefined): AsyncIterableIterator<InstallEvent> & {
51
+ readonly result: Promise<InstallReport>;
52
+ };
53
+ }
@@ -0,0 +1,144 @@
1
+ import { resolve } from 'node:path';
2
+ import { CONSOLE_PERMISSIONS, grantsAllow } from '@zhin.js/console-protocol';
3
+ import { loadProjectConfig } from '@zhin.js/scaffold-wizard';
4
+ import { PluginManagementWorkflow } from '../plugin-management/workflow.js';
5
+ import { authorizeInstall, createInstallAllowlist, } from './allowlist.js';
6
+ import { OnboardingError } from './errors.js';
7
+ import { InstallOutputRelay, installPackages, streamEvents, } from './install.js';
8
+ import { createStreamingPackageManager } from './package-manager.js';
9
+ import { applyDetection, detectConfiguredSteps, emptyOnboardingState, OnboardingStateStore, ONBOARDING_STEPS, } from './state-store.js';
10
+ const OPTIONAL_STEPS = new Set(['database', 'ai', 'adapters']);
11
+ /** Permissions a step needs besides `plugin.manage` (ADR 0053 §2). */
12
+ const STEP_PERMISSIONS = Object.freeze({
13
+ ai: CONSOLE_PERMISSIONS.AGENT_CONFIGURE,
14
+ adapters: CONSOLE_PERMISSIONS.ENDPOINT_MANAGE,
15
+ });
16
+ /**
17
+ * The one onboarding service (ADR 0053 §2/§5): Console and `zhin setup` / `zhin console init`
18
+ * call the same methods. Transport-independent; routes / RPC wrap it.
19
+ */
20
+ export class OnboardingService {
21
+ #options;
22
+ #store;
23
+ relay = new InstallOutputRelay();
24
+ #installer;
25
+ #queue = Promise.resolve();
26
+ // Installs queue separately so status reads are not blocked by a long install.
27
+ #installQueue = Promise.resolve();
28
+ constructor(options) {
29
+ this.#options = options;
30
+ this.#store = options.store ?? new OnboardingStateStore(options.projectRoot);
31
+ this.#installer = options.installer;
32
+ }
33
+ /** Serializes state read-modify-write cycles within this process. */
34
+ #exclusive(operation) {
35
+ const next = this.#queue.then(operation, operation);
36
+ this.#queue = next.catch(() => undefined);
37
+ return next;
38
+ }
39
+ #exclusiveInstall(operation) {
40
+ const next = this.#installQueue.then(operation, operation);
41
+ this.#installQueue = next.catch(() => undefined);
42
+ return next;
43
+ }
44
+ async #readConfig() {
45
+ if (this.#options.readConfig)
46
+ return this.#options.readConfig();
47
+ const loaded = loadProjectConfig(resolve(this.#options.projectRoot));
48
+ return loaded.status === 'loaded' ? loaded.config : {};
49
+ }
50
+ /** Stored state with auto-detection applied; persisted only when detection changed it. */
51
+ async #current() {
52
+ const stored = await this.#store.read();
53
+ const detected = new Set(detectConfiguredSteps(await this.#readConfig()));
54
+ if (await this.#options.hasOwner())
55
+ detected.add('owner');
56
+ const next = applyDetection(stored ?? emptyOnboardingState(), detected);
57
+ if (!stored || next !== stored)
58
+ await this.#store.write(next);
59
+ return next;
60
+ }
61
+ #view(state) {
62
+ return Object.freeze({ ...state, required: !state.completedAt });
63
+ }
64
+ status() {
65
+ return this.#exclusive(async () => this.#view(await this.#current()));
66
+ }
67
+ /** Marks database / ai / adapters as done or skipped. The owner step is derived from IAM. */
68
+ async markStep(step, value, options = {}) {
69
+ if (typeof step !== 'string' || !ONBOARDING_STEPS.includes(step)) {
70
+ throw new OnboardingError('invalid_input', '未知的引导步骤');
71
+ }
72
+ if (value !== 'done' && value !== 'skipped')
73
+ throw new OnboardingError('invalid_input', '步骤状态只能是 done 或 skipped');
74
+ if (!OPTIONAL_STEPS.has(step)) {
75
+ throw new OnboardingError('invalid_input', '所有者步骤在创建第一个所有者后自动完成,不能手动标记');
76
+ }
77
+ return this.#exclusive(async () => {
78
+ if (options.principal)
79
+ await this.#authorizeStep(step, options.principal);
80
+ const state = await this.#current();
81
+ const steps = { ...state.steps, [step]: value };
82
+ const next = Object.freeze({ ...state, steps: Object.freeze(steps) });
83
+ await this.#store.write(next);
84
+ return this.#view(next);
85
+ });
86
+ }
87
+ /** Step 5: requires an owner; remaining pending optional steps stay pending (can be resumed later). */
88
+ complete(options = {}) {
89
+ return this.#exclusive(async () => {
90
+ if (options.principal)
91
+ this.#requireManager(options.principal);
92
+ const state = await this.#current();
93
+ if (state.steps.owner !== 'done')
94
+ throw new OnboardingError('conflict', '请先创建所有者');
95
+ if (state.completedAt)
96
+ return this.#view(state);
97
+ const next = Object.freeze({ ...state, completedAt: (this.#options.now?.() ?? new Date()).toISOString() });
98
+ await this.#store.write(next);
99
+ return this.#view(next);
100
+ });
101
+ }
102
+ async #authorizeStep(step, principal) {
103
+ // Database (step 1) is a bootstrap-token step until an owner exists; later steps need an owner session.
104
+ if (step === 'database' && principal.kind === 'bootstrap') {
105
+ if (await this.#options.hasOwner())
106
+ throw new OnboardingError('forbidden', '已创建所有者:初始化令牌已失效');
107
+ return;
108
+ }
109
+ this.#requireManager(principal);
110
+ const extra = STEP_PERMISSIONS[step];
111
+ if (extra && (principal.kind === 'bootstrap' || !grantsAllow(principal.grants, extra))) {
112
+ throw new OnboardingError('forbidden', `需要 ${extra} 权限`);
113
+ }
114
+ }
115
+ #requireManager(principal) {
116
+ if (principal.kind === 'bootstrap' || !grantsAllow(principal.grants, CONSOLE_PERMISSIONS.PLUGIN_MANAGE)) {
117
+ throw new OnboardingError('forbidden', `需要 ${CONSOLE_PERMISSIONS.PLUGIN_MANAGE} 权限`);
118
+ }
119
+ }
120
+ /** The current install allowlist (official catalogs + market entries). */
121
+ async allowlist() {
122
+ return createInstallAllowlist({ market: await this.#options.marketCatalog?.() ?? [] });
123
+ }
124
+ #port() {
125
+ this.#installer ??= new PluginManagementWorkflow(this.#options.projectRoot, {
126
+ ...this.#options.workflow,
127
+ installDependencies: createStreamingPackageManager(this.relay.output),
128
+ });
129
+ return this.#installer;
130
+ }
131
+ /** Authorizes then installs; progress goes to `onEvent`. Authorization errors throw before any work. */
132
+ installPackages(packages, principal, onEvent) {
133
+ return this.#exclusiveInstall(async () => {
134
+ const authorized = authorizeInstall(packages, principal, await this.allowlist(), {
135
+ ownerExists: await this.#options.hasOwner(),
136
+ });
137
+ return installPackages(authorized, { port: this.#port(), relay: this.relay, ...(onEvent ? { onEvent } : {}) });
138
+ });
139
+ }
140
+ /** Same as `installPackages`, as an async iterator of progress events (ends with `done`). */
141
+ streamInstall(packages, principal) {
142
+ return streamEvents(emit => this.installPackages(packages, principal, emit));
143
+ }
144
+ }
@@ -0,0 +1,26 @@
1
+ export declare const ONBOARDING_STEPS: readonly ["database", "owner", "ai", "adapters"];
2
+ export type OnboardingStep = typeof ONBOARDING_STEPS[number];
3
+ export type OnboardingStepStatus = 'pending' | 'done' | 'skipped';
4
+ /** `.zhin/onboarding.json` (ADR 0053 §4). */
5
+ export interface OnboardingState {
6
+ readonly version: 1;
7
+ readonly steps: Readonly<Record<OnboardingStep, OnboardingStepStatus>>;
8
+ /** ISO timestamp set by `complete()`. */
9
+ readonly completedAt?: string;
10
+ }
11
+ export declare const ONBOARDING_FILE: string;
12
+ export declare function emptyOnboardingState(): OnboardingState;
13
+ /** Strict parse; anything malformed returns undefined (treated as a fresh state). */
14
+ export declare function parseOnboardingState(value: unknown): OnboardingState | undefined;
15
+ /** Reads / atomically writes `.zhin/onboarding.json` (temp file + rename, mode 0600). */
16
+ export declare class OnboardingStateStore {
17
+ readonly file: string;
18
+ constructor(projectRoot: string, file?: string);
19
+ /** The stored state, or undefined when the file is absent or invalid. */
20
+ read(): Promise<OnboardingState | undefined>;
21
+ write(state: OnboardingState): Promise<void>;
22
+ }
23
+ /** Steps an existing project has already configured (ADR 0053 §4 upgrade rule). */
24
+ export declare function detectConfiguredSteps(config: Readonly<Record<string, unknown>>): ReadonlySet<OnboardingStep>;
25
+ /** Pending steps become `done` when detected; `done` / `skipped` are never overridden. */
26
+ export declare function applyDetection(state: OnboardingState, detected: ReadonlySet<OnboardingStep>): OnboardingState;
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { ADAPTERS, isAiEnabledInConfig } from '@zhin.js/scaffold-wizard';
5
+ export const ONBOARDING_STEPS = Object.freeze(['database', 'owner', 'ai', 'adapters']);
6
+ export const ONBOARDING_FILE = join('.zhin', 'onboarding.json');
7
+ export function emptyOnboardingState() {
8
+ return Object.freeze({
9
+ version: 1,
10
+ steps: Object.freeze({ database: 'pending', owner: 'pending', ai: 'pending', adapters: 'pending' }),
11
+ });
12
+ }
13
+ const STATUSES = new Set(['pending', 'done', 'skipped']);
14
+ /** Strict parse; anything malformed returns undefined (treated as a fresh state). */
15
+ export function parseOnboardingState(value) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17
+ return undefined;
18
+ const raw = value;
19
+ if (raw.version !== 1 || !raw.steps || typeof raw.steps !== 'object')
20
+ return undefined;
21
+ const steps = {};
22
+ for (const step of ONBOARDING_STEPS) {
23
+ const status = raw.steps[step];
24
+ steps[step] = typeof status === 'string' && STATUSES.has(status) ? status : 'pending';
25
+ }
26
+ const completedAt = typeof raw.completedAt === 'string' && !Number.isNaN(Date.parse(raw.completedAt)) ? raw.completedAt : undefined;
27
+ return Object.freeze({ version: 1, steps: Object.freeze(steps), ...(completedAt ? { completedAt } : {}) });
28
+ }
29
+ /** Reads / atomically writes `.zhin/onboarding.json` (temp file + rename, mode 0600). */
30
+ export class OnboardingStateStore {
31
+ file;
32
+ constructor(projectRoot, file = ONBOARDING_FILE) {
33
+ this.file = resolve(projectRoot, file);
34
+ }
35
+ /** The stored state, or undefined when the file is absent or invalid. */
36
+ async read() {
37
+ let source;
38
+ try {
39
+ source = await readFile(this.file, 'utf8');
40
+ }
41
+ catch (error) {
42
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
43
+ return undefined;
44
+ throw error;
45
+ }
46
+ try {
47
+ return parseOnboardingState(JSON.parse(source));
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ async write(state) {
54
+ const parsed = parseOnboardingState(state);
55
+ if (!parsed)
56
+ throw new Error('Invalid onboarding state');
57
+ await mkdir(dirname(this.file), { recursive: true });
58
+ const temporary = `${this.file}.${randomUUID()}.tmp`;
59
+ try {
60
+ await writeFile(temporary, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
61
+ await chmod(temporary, 0o600);
62
+ await rename(temporary, this.file);
63
+ }
64
+ finally {
65
+ await rm(temporary, { force: true });
66
+ }
67
+ }
68
+ }
69
+ const ADAPTER_PACKAGES = new Set(ADAPTERS.map(adapter => adapter.package));
70
+ /** Steps an existing project has already configured (ADR 0053 §4 upgrade rule). */
71
+ export function detectConfiguredSteps(config) {
72
+ const detected = new Set();
73
+ const database = config.database;
74
+ if (database && typeof database === 'object' && !Array.isArray(database)
75
+ && typeof database.dialect === 'string')
76
+ detected.add('database');
77
+ const plugins = config.plugins;
78
+ const packages = plugins && typeof plugins === 'object' && !Array.isArray(plugins)
79
+ ? Object.values(plugins) : [];
80
+ if (packages.some(value => typeof value === 'string' && ADAPTER_PACKAGES.has(value)))
81
+ detected.add('adapters');
82
+ if (isAiEnabledInConfig(config))
83
+ detected.add('ai');
84
+ return detected;
85
+ }
86
+ /** Pending steps become `done` when detected; `done` / `skipped` are never overridden. */
87
+ export function applyDetection(state, detected) {
88
+ let changed = false;
89
+ const steps = { ...state.steps };
90
+ for (const step of detected) {
91
+ if (steps[step] === 'pending') {
92
+ steps[step] = 'done';
93
+ changed = true;
94
+ }
95
+ }
96
+ return changed ? Object.freeze({ ...state, steps: Object.freeze(steps) }) : state;
97
+ }
@@ -0,0 +1,2 @@
1
+ /** `@zhin.js/gateway/onboarding` — Console onboarding state, install allowlist and installs (ADR 0053). */
2
+ export * from './onboarding/index.js';
@@ -0,0 +1,2 @@
1
+ /** `@zhin.js/gateway/onboarding` — Console onboarding state, install allowlist and installs (ADR 0053). */
2
+ export * from './onboarding/index.js';
@@ -0,0 +1,23 @@
1
+ import type { ConsolePrincipal } from '@zhin.js/host-http';
2
+ import { type GatewayAuthorizedPackage, type GatewayTransactionOperation } from '@zhin.js/console-protocol';
3
+ import { type InstallAllowlist } from '../onboarding/allowlist.js';
4
+ /** A Console principal, or `'local'` for the CLI on the local control socket (owner, ADR 0054 §6). */
5
+ export type GatewayPrincipal = ConsolePrincipal | 'local';
6
+ export type GatewayPackageAction = 'list' | 'plan' | GatewayTransactionOperation;
7
+ export interface AuthorizePackagesOptions {
8
+ readonly allowlist: InstallAllowlist;
9
+ /** True once an owner exists: the bootstrap token is refused afterwards. */
10
+ readonly ownerExists: boolean;
11
+ }
12
+ /** A global `owner` binding, or the local control socket. */
13
+ export declare function isGatewayOwner(principal: GatewayPrincipal | undefined): boolean;
14
+ /** Whether the principal holds a global Console permission (bootstrap: only an open, local Host). */
15
+ export declare function gatewayPrincipalAllows(principal: GatewayPrincipal | undefined, permission: string): boolean;
16
+ /**
17
+ * Authorizes one package transaction (deny by default):
18
+ * - bootstrap token: `install` of allowlisted database drivers only, before the first owner (ADR 0053 §3);
19
+ * - `plugin.manage` without a global owner binding: allowlisted packages only (ADR 0053 §3);
20
+ * - owners and the local socket: any npm package by name, with the same strict spec rules.
21
+ * `'all'` (update every dependency) is owner-only. `declare` takes bare names, like `uninstall`.
22
+ */
23
+ export declare function authorizePackages(principal: GatewayPrincipal | undefined, operation: GatewayTransactionOperation, requests: readonly unknown[] | 'all', options: AuthorizePackagesOptions): readonly GatewayAuthorizedPackage[];
@@ -0,0 +1,109 @@
1
+ import { CONSOLE_PERMISSIONS, grantsAllow, } from '@zhin.js/console-protocol';
2
+ import { authorizeInstall, parseInstallRequest } from '../onboarding/allowlist.js';
3
+ import { OnboardingError } from '../onboarding/errors.js';
4
+ import { GatewayError } from './errors.js';
5
+ const MAX_PACKAGES = 32;
6
+ const EXACT_VERSION = /^\d{1,9}\.\d{1,9}\.\d{1,9}(?:-[0-9A-Za-z][0-9A-Za-z.-]{0,63})?$/u;
7
+ /** A global `owner` binding, or the local control socket. */
8
+ export function isGatewayOwner(principal) {
9
+ if (principal === 'local')
10
+ return true;
11
+ if (!principal || principal.kind === 'bootstrap')
12
+ return false;
13
+ return principal.grants.some(grant => grant.role === 'owner' && grant.scope.kind === 'global');
14
+ }
15
+ /** Whether the principal holds a global Console permission (bootstrap: only an open, local Host). */
16
+ export function gatewayPrincipalAllows(principal, permission) {
17
+ if (principal === 'local')
18
+ return true;
19
+ if (!principal)
20
+ return false;
21
+ if (principal.kind === 'bootstrap')
22
+ return principal.open;
23
+ return grantsAllow(principal.grants, permission);
24
+ }
25
+ function rethrow(error) {
26
+ if (error instanceof OnboardingError)
27
+ throw new GatewayError(error.code, error.message, { cause: error });
28
+ throw error;
29
+ }
30
+ /**
31
+ * Authorizes one package transaction (deny by default):
32
+ * - bootstrap token: `install` of allowlisted database drivers only, before the first owner (ADR 0053 §3);
33
+ * - `plugin.manage` without a global owner binding: allowlisted packages only (ADR 0053 §3);
34
+ * - owners and the local socket: any npm package by name, with the same strict spec rules.
35
+ * `'all'` (update every dependency) is owner-only. `declare` takes bare names, like `uninstall`.
36
+ */
37
+ export function authorizePackages(principal, operation, requests, options) {
38
+ if (!principal)
39
+ throw new GatewayError('forbidden', '未登录');
40
+ if (principal !== 'local' && principal.kind === 'bootstrap') {
41
+ if (operation !== 'install' || requests === 'all')
42
+ throw new GatewayError('forbidden', '初始化令牌只能安装数据库驱动');
43
+ try {
44
+ return Object.freeze(authorizeInstall(requests, principal, options.allowlist, { ownerExists: options.ownerExists })
45
+ .map(item => Object.freeze({ name: item.name, ...(item.version === undefined ? {} : { version: item.version }), spec: item.spec, source: item.kind })));
46
+ }
47
+ catch (error) {
48
+ rethrow(error);
49
+ }
50
+ }
51
+ if (!gatewayPrincipalAllows(principal, CONSOLE_PERMISSIONS.PLUGIN_MANAGE)) {
52
+ throw new GatewayError('forbidden', `需要 ${CONSOLE_PERMISSIONS.PLUGIN_MANAGE} 权限`);
53
+ }
54
+ const owner = isGatewayOwner(principal);
55
+ if (requests === 'all') {
56
+ if (operation !== 'update')
57
+ throw new GatewayError('invalid_input', '只有 update 可以作用于全部依赖');
58
+ if (!owner)
59
+ throw new GatewayError('forbidden', '只有所有者可以更新全部依赖');
60
+ return Object.freeze([]);
61
+ }
62
+ if (!Array.isArray(requests) || requests.length === 0 || requests.length > MAX_PACKAGES) {
63
+ throw new GatewayError('invalid_input', `请提供 1–${MAX_PACKAGES} 个包`);
64
+ }
65
+ if (operation === 'install' && !owner) {
66
+ try {
67
+ return Object.freeze(authorizeInstall(requests, principal, options.allowlist)
68
+ .map(item => Object.freeze({ name: item.name, ...(item.version === undefined ? {} : { version: item.version }), spec: item.spec, source: item.kind })));
69
+ }
70
+ catch (error) {
71
+ rethrow(error);
72
+ }
73
+ }
74
+ const seen = new Set();
75
+ const authorized = [];
76
+ for (const request of requests) {
77
+ let parsed;
78
+ try {
79
+ parsed = parseInstallRequest(request);
80
+ }
81
+ catch (error) {
82
+ rethrow(error);
83
+ }
84
+ const { name } = parsed;
85
+ if (seen.has(name))
86
+ throw new GatewayError('invalid_input', `重复的包:${name}`);
87
+ seen.add(name);
88
+ const entry = options.allowlist.get(name);
89
+ if (!owner && !entry)
90
+ throw new GatewayError('forbidden', `${name} 不在可管理目录中;只有所有者可以管理任意 npm 包`);
91
+ let version = parsed.version;
92
+ if (operation === 'uninstall' && version !== undefined)
93
+ throw new GatewayError('invalid_input', `卸载 ${name} 时不能指定版本`);
94
+ if (operation === 'declare' && version !== undefined)
95
+ throw new GatewayError('invalid_input', `声明 ${name} 时不能指定版本`);
96
+ if (operation === 'update' && version !== undefined && !EXACT_VERSION.test(version)) {
97
+ throw new GatewayError('invalid_input', `${name} 的更新版本必须是精确 semver 版本`);
98
+ }
99
+ if (operation === 'install')
100
+ version ??= entry?.defaultVersion;
101
+ authorized.push(Object.freeze({
102
+ name,
103
+ ...(version === undefined ? {} : { version }),
104
+ spec: version === undefined ? name : `${name}@${version}`,
105
+ source: entry?.kind ?? 'npm',
106
+ }));
107
+ }
108
+ return Object.freeze(authorized);
109
+ }
@@ -0,0 +1,11 @@
1
+ import type { GatewayRpcErrorCode, GatewayRpcErrorDetails } from '@zhin.js/console-protocol';
2
+ /** Transport-independent gateway error raised before a transaction does any work. */
3
+ export declare class GatewayError extends Error {
4
+ readonly code: GatewayRpcErrorCode;
5
+ readonly status: number;
6
+ /** Structured details sent with the reply (only non-empty fields). */
7
+ readonly details?: GatewayRpcErrorDetails;
8
+ constructor(code: GatewayRpcErrorCode, message: string, options?: ErrorOptions & {
9
+ readonly details?: GatewayRpcErrorDetails;
10
+ });
11
+ }
@@ -0,0 +1,26 @@
1
+ const STATUS = {
2
+ invalid_input: 400,
3
+ forbidden: 403,
4
+ unmapped: 403,
5
+ conflict: 409,
6
+ unavailable: 503,
7
+ internal: 500,
8
+ };
9
+ /** Transport-independent gateway error raised before a transaction does any work. */
10
+ export class GatewayError extends Error {
11
+ code;
12
+ status;
13
+ /** Structured details sent with the reply (only non-empty fields). */
14
+ details;
15
+ constructor(code, message, options) {
16
+ super(message, options);
17
+ this.code = code;
18
+ this.name = 'GatewayError';
19
+ this.status = STATUS[code];
20
+ const details = options?.details;
21
+ const errors = details?.errors?.length ? Object.freeze(details.errors.map(item => Object.freeze({ path: item.path, message: item.message }))) : undefined;
22
+ const missingEnv = details?.missingEnv?.length ? Object.freeze([...details.missingEnv]) : undefined;
23
+ if (errors || missingEnv)
24
+ this.details = Object.freeze({ ...(errors ? { errors } : {}), ...(missingEnv ? { missingEnv } : {}) });
25
+ }
26
+ }
@@ -0,0 +1,5 @@
1
+ export * from './errors.js';
2
+ export * from './authorization.js';
3
+ export * from './snapshot.js';
4
+ export * from './service.js';
5
+ export * from './rpc.js';
@@ -0,0 +1,5 @@
1
+ export * from './errors.js';
2
+ export * from './authorization.js';
3
+ export * from './snapshot.js';
4
+ export * from './service.js';
5
+ export * from './rpc.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Transport-independent dispatcher for the gateway's own RPC (`gateway.packages.*`,
3
+ * `gateway.instance.*`, ADR 0054 §4 / §5). The HTTP server maps envelopes onto it; the permission
4
+ * map in `@zhin.js/console-protocol` is checked here, then each service applies its own rules.
5
+ */
6
+ import { type GatewayInstanceStatus, type GatewayRpcRequest, type GatewayRpcResponse, type GatewayRpcType } from '@zhin.js/console-protocol';
7
+ import type { InstanceStatus, InstanceStopOptions } from '../supervisor/instance-supervisor.js';
8
+ import type { InstanceLogLine } from '../supervisor/instance-logs.js';
9
+ import { type GatewayPrincipal } from './authorization.js';
10
+ import type { GatewayPackageService } from './service.js';
11
+ /** The part of `InstanceSupervisor` the lifecycle RPC uses. */
12
+ export interface GatewayRpcInstance {
13
+ start(): Promise<void>;
14
+ stop(options?: InstanceStopOptions): Promise<void>;
15
+ restart(options?: Omit<InstanceStopOptions, 'force'>): Promise<void>;
16
+ status(): InstanceStatus;
17
+ readonly logs: {
18
+ tail(limit?: number): readonly InstanceLogLine[];
19
+ };
20
+ }
21
+ export interface GatewayRpcDependencies {
22
+ readonly packages?: GatewayPackageService;
23
+ readonly instance?: GatewayRpcInstance;
24
+ }
25
+ export type GatewayRpcDispatcher = (request: GatewayRpcRequest, principal: GatewayPrincipal | undefined) => Promise<GatewayRpcResponse>;
26
+ export declare function isGatewayRpcType(type: unknown): type is GatewayRpcType;
27
+ export declare function createGatewayRpcDispatcher(dependencies: GatewayRpcDependencies): GatewayRpcDispatcher;
28
+ /** Dispatches one gateway RPC; never throws (errors become `{ ok: false }`). */
29
+ export declare function dispatchGatewayRpc(request: GatewayRpcRequest, principal: GatewayPrincipal | undefined, dependencies: GatewayRpcDependencies): Promise<GatewayRpcResponse>;
30
+ export declare function instanceStatus(status: InstanceStatus): GatewayInstanceStatus;