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

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 (181) 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 +18 -0
  14. package/lib/config/configuration-redaction.js +75 -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 +223 -0
  19. package/lib/config/environment-files.d.ts +3 -0
  20. package/lib/config/environment-files.js +14 -0
  21. package/lib/config/literal-secrets.d.ts +34 -0
  22. package/lib/config/literal-secrets.js +136 -0
  23. package/lib/config/plugin-package-map.d.ts +2 -0
  24. package/lib/config/plugin-package-map.js +14 -0
  25. package/lib/config/plugin-schema-catalog.d.ts +22 -0
  26. package/lib/config/plugin-schema-catalog.js +262 -0
  27. package/lib/config.d.ts +13 -0
  28. package/lib/config.js +13 -0
  29. package/lib/database/config.d.ts +22 -0
  30. package/lib/database/config.js +159 -0
  31. package/lib/database/index.d.ts +2 -0
  32. package/lib/database/index.js +2 -0
  33. package/lib/database/project-database.d.ts +31 -0
  34. package/lib/database/project-database.js +130 -0
  35. package/lib/database.d.ts +2 -0
  36. package/lib/database.js +2 -0
  37. package/lib/http/console-rpc.d.ts +27 -0
  38. package/lib/http/console-rpc.js +51 -0
  39. package/lib/http/gateway-console.d.ts +63 -0
  40. package/lib/http/gateway-console.js +309 -0
  41. package/lib/http/gateway-events.d.ts +40 -0
  42. package/lib/http/gateway-events.js +185 -0
  43. package/lib/http/gateway-http-server.d.ts +95 -0
  44. package/lib/http/gateway-http-server.js +522 -0
  45. package/lib/http/local-principal.d.ts +2 -0
  46. package/lib/http/local-principal.js +16 -0
  47. package/lib/http/plugin-switch-rpc.d.ts +19 -0
  48. package/lib/http/plugin-switch-rpc.js +214 -0
  49. package/lib/http/run-gateway.d.ts +43 -0
  50. package/lib/http/run-gateway.js +268 -0
  51. package/lib/http/start-gateway.d.ts +114 -0
  52. package/lib/http/start-gateway.js +226 -0
  53. package/lib/http-response.d.ts +6 -0
  54. package/lib/http-response.js +25 -0
  55. package/lib/http.d.ts +6 -0
  56. package/lib/http.js +6 -0
  57. package/lib/iam/crypto.d.ts +12 -0
  58. package/lib/iam/crypto.js +58 -0
  59. package/lib/iam/index.d.ts +8 -0
  60. package/lib/iam/index.js +8 -0
  61. package/lib/iam/install.d.ts +36 -0
  62. package/lib/iam/install.js +74 -0
  63. package/lib/iam/local-admin.d.ts +27 -0
  64. package/lib/iam/local-admin.js +102 -0
  65. package/lib/iam/rate-limit.d.ts +15 -0
  66. package/lib/iam/rate-limit.js +52 -0
  67. package/lib/iam/routes.d.ts +23 -0
  68. package/lib/iam/routes.js +157 -0
  69. package/lib/iam/rpc.d.ts +14 -0
  70. package/lib/iam/rpc.js +100 -0
  71. package/lib/iam/service.d.ts +203 -0
  72. package/lib/iam/service.js +784 -0
  73. package/lib/iam/setup-service.d.ts +83 -0
  74. package/lib/iam/setup-service.js +206 -0
  75. package/lib/iam/tables.d.ts +20 -0
  76. package/lib/iam/tables.js +82 -0
  77. package/lib/iam.d.ts +2 -0
  78. package/lib/iam.js +2 -0
  79. package/lib/index.d.ts +22 -0
  80. package/lib/index.js +22 -0
  81. package/lib/local/client.d.ts +71 -0
  82. package/lib/local/client.js +296 -0
  83. package/lib/local/handler.d.ts +30 -0
  84. package/lib/local/handler.js +86 -0
  85. package/lib/local/local-socket.d.ts +55 -0
  86. package/lib/local/local-socket.js +232 -0
  87. package/lib/local/paths.d.ts +34 -0
  88. package/lib/local/paths.js +58 -0
  89. package/lib/local/protocol.d.ts +140 -0
  90. package/lib/local/protocol.js +90 -0
  91. package/lib/local/state-file.d.ts +8 -0
  92. package/lib/local/state-file.js +45 -0
  93. package/lib/local.d.ts +10 -0
  94. package/lib/local.js +10 -0
  95. package/lib/onboarding/allowlist.d.ts +54 -0
  96. package/lib/onboarding/allowlist.js +106 -0
  97. package/lib/onboarding/errors.d.ts +7 -0
  98. package/lib/onboarding/errors.js +17 -0
  99. package/lib/onboarding/index.d.ts +6 -0
  100. package/lib/onboarding/index.js +6 -0
  101. package/lib/onboarding/install.d.ts +99 -0
  102. package/lib/onboarding/install.js +132 -0
  103. package/lib/onboarding/package-manager.d.ts +47 -0
  104. package/lib/onboarding/package-manager.js +179 -0
  105. package/lib/onboarding/service.d.ts +53 -0
  106. package/lib/onboarding/service.js +144 -0
  107. package/lib/onboarding/state-store.d.ts +26 -0
  108. package/lib/onboarding/state-store.js +97 -0
  109. package/lib/onboarding.d.ts +2 -0
  110. package/lib/onboarding.js +2 -0
  111. package/lib/packages/authorization.d.ts +24 -0
  112. package/lib/packages/authorization.js +128 -0
  113. package/lib/packages/errors.d.ts +11 -0
  114. package/lib/packages/errors.js +26 -0
  115. package/lib/packages/index.d.ts +5 -0
  116. package/lib/packages/index.js +5 -0
  117. package/lib/packages/rpc.d.ts +30 -0
  118. package/lib/packages/rpc.js +160 -0
  119. package/lib/packages/service.d.ts +139 -0
  120. package/lib/packages/service.js +504 -0
  121. package/lib/packages/snapshot.d.ts +14 -0
  122. package/lib/packages/snapshot.js +73 -0
  123. package/lib/packages.d.ts +2 -0
  124. package/lib/packages.js +2 -0
  125. package/lib/plugin-management/activity.d.ts +6 -0
  126. package/lib/plugin-management/activity.js +14 -0
  127. package/lib/plugin-management/config-explanation.d.ts +6 -0
  128. package/lib/plugin-management/config-explanation.js +113 -0
  129. package/lib/plugin-management/use-cases.d.ts +15 -0
  130. package/lib/plugin-management/use-cases.js +120 -0
  131. package/lib/plugin-management/workflow.d.ts +136 -0
  132. package/lib/plugin-management/workflow.js +878 -0
  133. package/lib/plugin-management.d.ts +5 -0
  134. package/lib/plugin-management.js +5 -0
  135. package/lib/project/config-file.d.ts +6 -0
  136. package/lib/project/config-file.js +58 -0
  137. package/lib/project/environment.d.ts +13 -0
  138. package/lib/project/environment.js +52 -0
  139. package/lib/project/instance-declarations.d.ts +55 -0
  140. package/lib/project/instance-declarations.js +542 -0
  141. package/lib/project/legacy-config.d.ts +25 -0
  142. package/lib/project/legacy-config.js +70 -0
  143. package/lib/project/node-requirements.d.ts +7 -0
  144. package/lib/project/node-requirements.js +20 -0
  145. package/lib/project/tsx-module-loader.d.ts +7 -0
  146. package/lib/project/tsx-module-loader.js +29 -0
  147. package/lib/project.d.ts +7 -0
  148. package/lib/project.js +7 -0
  149. package/lib/server.d.ts +11 -0
  150. package/lib/server.js +11 -0
  151. package/lib/supervisor/instance-logs.d.ts +39 -0
  152. package/lib/supervisor/instance-logs.js +152 -0
  153. package/lib/supervisor/instance-supervisor.d.ts +140 -0
  154. package/lib/supervisor/instance-supervisor.js +408 -0
  155. package/lib/supervisor/run-instance.d.ts +35 -0
  156. package/lib/supervisor/run-instance.js +161 -0
  157. package/lib/supervisor/terminal-state.d.ts +30 -0
  158. package/lib/supervisor/terminal-state.js +57 -0
  159. package/lib/supervisor.d.ts +4 -0
  160. package/lib/supervisor.js +4 -0
  161. package/lib/terminal/backends.d.ts +38 -0
  162. package/lib/terminal/backends.js +193 -0
  163. package/lib/terminal/config.d.ts +63 -0
  164. package/lib/terminal/config.js +237 -0
  165. package/lib/terminal/host.d.ts +26 -0
  166. package/lib/terminal/host.js +76 -0
  167. package/lib/terminal/index.d.ts +60 -0
  168. package/lib/terminal/index.js +135 -0
  169. package/lib/terminal/modules.d.ts +87 -0
  170. package/lib/terminal/modules.js +44 -0
  171. package/lib/terminal/service.d.ts +101 -0
  172. package/lib/terminal/service.js +455 -0
  173. package/lib/terminal/tickets.d.ts +46 -0
  174. package/lib/terminal/tickets.js +76 -0
  175. package/lib/terminal.d.ts +3 -0
  176. package/lib/terminal.js +3 -0
  177. package/lib/usage/aggregate.d.ts +87 -0
  178. package/lib/usage/aggregate.js +212 -0
  179. package/lib/usage.d.ts +2 -0
  180. package/lib/usage.js +2 -0
  181. package/package.json +117 -4
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Gateway-owned Console RPC on `POST {base}/console/request` (ADR 0054 §4 / §5): the Console
3
+ * sends `{ type, requestId?, data? }`; `gateway.*` types are answered here with the same reply
4
+ * envelope the instance uses (`{ success, data, type, requestId }` / `{ success: false, code,
5
+ * error, requestId, errors?, missingEnv? }`, see `GatewayRpcHttpErrorReply`), every other
6
+ * type is forwarded to the instance by the HTTP server.
7
+ */
8
+ import { type RouteContext, type TokenRegistry } from '@zhin.js/host-http';
9
+ import type { GatewayRpcDispatcher } from '../packages/rpc.js';
10
+ /** RPC types with this prefix belong to the gateway (unknown ones are refused as unmapped). */
11
+ export declare const GATEWAY_RPC_PREFIX = "gateway.";
12
+ export interface GatewayConsoleRpc {
13
+ handle(message: Readonly<Record<string, unknown>>, context: RouteContext, tokenRegistry: TokenRegistry): Promise<Response>;
14
+ }
15
+ /**
16
+ * Further gateway-owned RPC types (`iam.*`, `onboarding.update`, the global plugin switch), see
17
+ * `GatewayHttpServer.useConsoleRpc`.
18
+ */
19
+ export interface GatewayRpcHandler extends GatewayConsoleRpc {
20
+ owns(type: string): boolean;
21
+ /**
22
+ * Narrows {@link owns} by payload: when present and false for the parsed message, the request
23
+ * is forwarded to the instance untouched (e.g. `feature.set-enabled` below the global level).
24
+ */
25
+ claims?(message: Readonly<Record<string, unknown>>): boolean;
26
+ }
27
+ export declare function createGatewayConsoleRpc(dispatch: GatewayRpcDispatcher): GatewayConsoleRpc;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Gateway-owned Console RPC on `POST {base}/console/request` (ADR 0054 §4 / §5): the Console
3
+ * sends `{ type, requestId?, data? }`; `gateway.*` types are answered here with the same reply
4
+ * envelope the instance uses (`{ success, data, type, requestId }` / `{ success: false, code,
5
+ * error, requestId, errors?, missingEnv? }`, see `GatewayRpcHttpErrorReply`), every other
6
+ * type is forwarded to the instance by the HTTP server.
7
+ */
8
+ import { auditActor, jsonResponse, } from '@zhin.js/host-http';
9
+ import { GATEWAY_RPC } from '@zhin.js/console-protocol';
10
+ /** RPC types with this prefix belong to the gateway (unknown ones are refused as unmapped). */
11
+ export const GATEWAY_RPC_PREFIX = 'gateway.';
12
+ const READ_ONLY_TYPES = new Set([
13
+ GATEWAY_RPC.LIST,
14
+ GATEWAY_RPC.PLAN,
15
+ GATEWAY_RPC.STATUS,
16
+ GATEWAY_RPC.LOGS,
17
+ ]);
18
+ export function createGatewayConsoleRpc(dispatch) {
19
+ return Object.freeze({
20
+ async handle(message, context, tokenRegistry) {
21
+ const type = typeof message.type === 'string' ? message.type : '';
22
+ const requestId = message.requestId;
23
+ const principal = context.principal;
24
+ const result = await dispatch({ type, data: message.data }, principal);
25
+ const write = !READ_ONLY_TYPES.has(type);
26
+ const outcome = result.ok ? 'ok' : result.error.code === 'forbidden' ? 'denied' : 'error';
27
+ if (write || outcome === 'denied') {
28
+ try {
29
+ tokenRegistry.accessProvider?.audit?.({
30
+ actor: auditActor(principal),
31
+ action: `rpc:${type}`.slice(0, 256),
32
+ outcome,
33
+ ...(context.remoteAddress ? { ip: context.remoteAddress } : {}),
34
+ });
35
+ }
36
+ catch { /* audit never breaks a request */ }
37
+ }
38
+ const noStore = { 'cache-control': 'no-store' };
39
+ if (result.ok)
40
+ return jsonResponse(200, { success: true, data: result.data, type, requestId }, noStore);
41
+ return jsonResponse(result.error.status, {
42
+ success: false,
43
+ code: result.error.code,
44
+ error: result.error.message,
45
+ requestId,
46
+ ...(result.error.errors ? { errors: result.error.errors } : {}),
47
+ ...(result.error.missingEnv ? { missingEnv: result.error.missingEnv } : {}),
48
+ }, noStore);
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,63 @@
1
+ import { type ConsoleAuditEntry } from '@zhin.js/host-http';
2
+ import { type ConsolePluginPermissionInfo } from '@zhin.js/console-protocol';
3
+ import { type ProjectDatabaseConfig } from '../database/config.js';
4
+ import { ProjectDatabase } from '../database/project-database.js';
5
+ import { type LocalConsoleAdmin } from '../iam/local-admin.js';
6
+ import { ConsoleIamService } from '../iam/service.js';
7
+ import { ConsoleSetupService } from '../iam/setup-service.js';
8
+ import { OnboardingService } from '../onboarding/service.js';
9
+ import type { ConsoleTerminalHandle } from '../terminal/index.js';
10
+ import type { GatewayRpcHandler } from './console-rpc.js';
11
+ import type { GatewayHttpServer } from './gateway-http-server.js';
12
+ /** What the Console module needs from the instance bridge (`IpcInstanceTransport` has all of it). */
13
+ export interface GatewayConsoleInstancePort {
14
+ /** Plugin permission catalog the instance last reported. */
15
+ pluginPermissions?(): readonly ConsolePluginPermissionInfo[];
16
+ /** Sends `principal-revoked` (the instance closes that user's live connections). */
17
+ revokePrincipal?(principalId: string, reason?: string): void;
18
+ /** Audit entries written by the instance. */
19
+ onAudit?(listener: (entry: ConsoleAuditEntry) => void): () => void;
20
+ }
21
+ export interface GatewayConsoleOptions {
22
+ readonly server: GatewayHttpServer;
23
+ readonly projectRoot: string;
24
+ /** Selects `.env.<environment>` for `${…}` in `database` / `console.terminal`. Default `development`. */
25
+ readonly environment?: string;
26
+ readonly instance?: GatewayConsoleInstancePort;
27
+ /** Restarts the instance after the setup wizard switched databases (ADR 0054 §7). */
28
+ readonly restartInstance?: () => Promise<void>;
29
+ /** Overrides the project `database` block (tests). */
30
+ readonly database?: ProjectDatabaseConfig;
31
+ /** Overrides the project `console.terminal` block. */
32
+ readonly terminal?: unknown;
33
+ readonly onboarding?: OnboardingService;
34
+ /** Retry interval while the database cannot be opened. Default 10 s. */
35
+ readonly retryMs?: number;
36
+ }
37
+ export interface GatewayConsole {
38
+ /** Settles once the first database open attempt finished (never rejects). */
39
+ readonly ready: Promise<void>;
40
+ /** The IAM service bound to the open database; undefined when the dialect cannot hold accounts. */
41
+ iam(): ConsoleIamService | undefined;
42
+ /** The gateway's database connection (undefined before the first open). */
43
+ database(): ProjectDatabase | undefined;
44
+ readonly setup: ConsoleSetupService;
45
+ readonly onboarding: OnboardingService;
46
+ readonly terminal: ConsoleTerminalHandle;
47
+ /** Closes the gateway's connection and opens the configured database again (setup wizard). */
48
+ reopenDatabase(): Promise<void>;
49
+ /** Socket-only `gateway.console.*` (`zhin console init` / `zhin console user` beside the gateway). */
50
+ readonly localAdmin: LocalConsoleAdmin;
51
+ close(): Promise<void>;
52
+ }
53
+ export declare function startGatewayConsole(options: GatewayConsoleOptions): Promise<GatewayConsole>;
54
+ export interface GatewayAccountRpcOptions {
55
+ readonly iam: () => ConsoleIamService | undefined;
56
+ readonly onboarding: OnboardingService;
57
+ }
58
+ /**
59
+ * `iam.*` (ADR 0049 §5) and `onboarding.update` (ADR 0053 §4) on the gateway's
60
+ * `POST {base}/console/request`. Authorization uses the shared permission map
61
+ * (`CONSOLE_RPC_PERMISSIONS`); denials and writes are audited like in the instance dispatcher.
62
+ */
63
+ export declare function createGatewayAccountRpc(options: GatewayAccountRpcOptions): GatewayRpcHandler;
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Console access control in the gateway (ADR 0054 §3 / §7, ADR 0049, ADR 0053 §4): the gateway
3
+ * opens the project database itself, owns the `console_*` tables, authenticates sessions / API
4
+ * tokens / the bootstrap token locally, and serves `/auth/*`, `iam.*`, `onboarding.update`, the
5
+ * setup wizard, the terminal and the audit log — all without an instance.
6
+ *
7
+ * The instance contributes what only it knows over the bridge: the plugin permission catalog
8
+ * (`catalog`) and its own audit entries (`audit`); the gateway tells it when a user's credentials
9
+ * were revoked (`principal-revoked`) so it closes that user's SSE / WebSocket connections.
10
+ */
11
+ import { join } from 'node:path';
12
+ import { auditActor, authorizeConsoleRpc, jsonResponse, rpcAuditDetail, rpcAuditTarget, } from '@zhin.js/host-http';
13
+ import { ONBOARDING_RPC } from '@zhin.js/console-protocol';
14
+ import { DEFAULT_ROOT_CONFIG_FILE_NAME } from '@zhin.js/plugin-runtime';
15
+ import { createConfigDocument } from '@zhin.js/runtime';
16
+ import { formatCompact, getLogger } from '@zhin.js/logger';
17
+ import { ConsoleConfigurationStore } from '../config/configuration.js';
18
+ import { readEnvFile, writeEnvFile } from '../config/environment-files.js';
19
+ import { loadExpandedProjectConfig, resolveProjectDatabaseConfig } from '../database/config.js';
20
+ import { ProjectDatabase } from '../database/project-database.js';
21
+ import { projectEnvironment } from '../iam/install.js';
22
+ import { createLocalConsoleAdmin } from '../iam/local-admin.js';
23
+ import { registerConsoleAuthRoutes } from '../iam/routes.js';
24
+ import { dispatchIamRpc, isIamRpc } from '../iam/rpc.js';
25
+ import { ConsoleIamService } from '../iam/service.js';
26
+ import { ConsoleSetupService } from '../iam/setup-service.js';
27
+ import { defineConsoleIamTables } from '../iam/tables.js';
28
+ import { OnboardingError } from '../onboarding/errors.js';
29
+ import { OnboardingService } from '../onboarding/service.js';
30
+ import { findConfigFile } from '../project/config-file.js';
31
+ import { installHostConsoleTerminal } from '../terminal/host.js';
32
+ const logger = getLogger('gateway');
33
+ const DEFAULT_RETRY_MS = 10_000;
34
+ /** A database that never starts: the IAM service then fails closed on its setup marker. */
35
+ const UNAVAILABLE_DATABASE = Object.freeze({ started: false, models: Object.freeze({ get: () => undefined }) });
36
+ export async function startGatewayConsole(options) {
37
+ const { server, projectRoot } = options;
38
+ const failClosed = () => ({
39
+ service: new ConsoleIamService({ database: UNAVAILABLE_DATABASE, projectRoot }),
40
+ supported: true,
41
+ dispose: [],
42
+ });
43
+ const environment = options.environment ?? 'development';
44
+ const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
45
+ const registry = server.tokenRegistry;
46
+ const settings = await loadExpandedProjectConfig(projectRoot, environment).catch((error) => {
47
+ logger.warn(formatCompact({ op: 'gateway_config_read_failed', error: errorText(error) }));
48
+ return Object.freeze({});
49
+ });
50
+ // Until (and while re-) connecting, fail closed: the owner state comes from the setup marker.
51
+ let connection = failClosed();
52
+ let closed = false;
53
+ let retryTimer;
54
+ let opening;
55
+ const iam = () => (connection.supported ? connection.service : undefined);
56
+ const disposers = [];
57
+ // Level grants need the catalog of the instance's loaded plugins (kept across restarts).
58
+ disposers.push(registry.usePluginPermissions(() => options.instance?.pluginPermissions?.() ?? []));
59
+ const access = Object.freeze({
60
+ initialized: () => connection.service?.initialized() ?? false,
61
+ authenticate: async (token, meta) => (await iam()?.authenticate(token, meta)) ?? null,
62
+ audit: (entry) => { iam()?.audit(entry); },
63
+ });
64
+ disposers.push(registry.useAccessProvider(access));
65
+ const stopInstanceAudit = options.instance?.onAudit?.((entry) => { iam()?.audit(entry); });
66
+ if (stopInstanceAudit)
67
+ disposers.push(stopInstanceAudit);
68
+ const terminal = installHostConsoleTerminal(server, {
69
+ config: options.terminal !== undefined ? options.terminal : readTerminalBlock(settings),
70
+ projectRoot,
71
+ base: server.apiBase,
72
+ iam,
73
+ logger: { warn: (message) => logger.warn(message), info: (message) => logger.info(message) },
74
+ });
75
+ const revoke = (userId, reason) => {
76
+ terminal.terminatePrincipal(`user:${userId}`, 'revoked');
77
+ try {
78
+ options.instance?.revokePrincipal?.(userId, reason);
79
+ }
80
+ catch { /* bridge gone */ }
81
+ };
82
+ async function connect() {
83
+ let config;
84
+ try {
85
+ config = options.database ?? await resolveProjectDatabaseConfig(projectRoot, await loadExpandedProjectConfig(projectRoot, environment));
86
+ }
87
+ catch (error) {
88
+ logger.warn(formatCompact({ op: 'gateway_database_config_invalid', error: errorText(error) }));
89
+ connection = failClosed();
90
+ return;
91
+ }
92
+ let database;
93
+ try {
94
+ database = new ProjectDatabase(config);
95
+ }
96
+ catch (error) {
97
+ logger.warn(formatCompact({ op: 'gateway_database_driver_failed', dialect: config.dialect, error: errorText(error) }));
98
+ connection = failClosed();
99
+ return;
100
+ }
101
+ const supported = defineConsoleIamTables(database);
102
+ if (!supported) {
103
+ logger.warn('console iam: the configured database cannot store Console users; run the setup wizard to choose sqlite/mysql/pg/mongodb');
104
+ connection = { database, supported: false, dispose: [] };
105
+ return;
106
+ }
107
+ const service = new ConsoleIamService({
108
+ database,
109
+ projectRoot,
110
+ onError: (error) => logger.warn(`console iam: ${errorText(error)}`),
111
+ pluginPermissions: () => registry.pluginPermissions(),
112
+ });
113
+ const dispose = [service.onCredentialsRevoked(({ userId, reason }) => revoke(userId, reason))];
114
+ connection = { database, service, supported: true, dispose };
115
+ try {
116
+ await database.start();
117
+ await service.start();
118
+ logger.info(formatCompact({ op: 'gateway_database_open', dialect: config.dialect }));
119
+ }
120
+ catch (error) {
121
+ // Not ready: `/auth/*` answers 503 and the owner state comes from the setup marker.
122
+ logger.warn(formatCompact({ op: 'gateway_database_open_failed', dialect: config.dialect, error: errorText(error) }));
123
+ scheduleRetry();
124
+ }
125
+ }
126
+ function scheduleRetry() {
127
+ if (closed || retryTimer)
128
+ return;
129
+ retryTimer = setTimeout(() => {
130
+ retryTimer = undefined;
131
+ if (closed)
132
+ return;
133
+ void reopen();
134
+ }, retryMs);
135
+ retryTimer.unref?.();
136
+ }
137
+ async function disconnect() {
138
+ const current = connection;
139
+ connection = failClosed();
140
+ for (const dispose of current.dispose.splice(0))
141
+ dispose();
142
+ current.service?.stop();
143
+ await current.database?.stop().catch((error) => {
144
+ logger.warn(formatCompact({ op: 'gateway_database_close_failed', error: errorText(error) }));
145
+ });
146
+ }
147
+ function reopen() {
148
+ opening = (opening ?? Promise.resolve()).then(async () => {
149
+ if (closed)
150
+ return;
151
+ clearTimeout(retryTimer);
152
+ retryTimer = undefined;
153
+ await disconnect();
154
+ if (!closed)
155
+ await connect();
156
+ });
157
+ return opening;
158
+ }
159
+ const configFile = join(projectRoot, findConfigFile(projectRoot) ?? DEFAULT_ROOT_CONFIG_FILE_NAME);
160
+ const setup = new ConsoleSetupService({
161
+ projectRoot,
162
+ config: new ConsoleConfigurationStore({ projectRoot, document: createConfigDocument(configFile) }),
163
+ envFile: {
164
+ read: () => readEnvFile(projectRoot, '.env'),
165
+ write: (content) => writeEnvFile(projectRoot, '.env', content),
166
+ },
167
+ iam,
168
+ environment: () => projectEnvironment(projectRoot),
169
+ });
170
+ const onboarding = options.onboarding ?? new OnboardingService({
171
+ projectRoot,
172
+ hasOwner: () => connection.service?.initialized() ?? false,
173
+ });
174
+ const restartInstance = () => {
175
+ void options.restartInstance?.().catch((error) => {
176
+ logger.warn(formatCompact({ op: 'gateway_database_switch_failed', error: errorText(error) }));
177
+ });
178
+ };
179
+ const localAdmin = createLocalConsoleAdmin({
180
+ setup,
181
+ iam,
182
+ // ADR 0054 §7, as for the web wizard: the gateway uses the new database before it answers.
183
+ switchDatabase: async () => {
184
+ await reopen();
185
+ restartInstance();
186
+ return { restarting: options.restartInstance !== undefined };
187
+ },
188
+ });
189
+ disposers.push(...registerConsoleAuthRoutes({
190
+ http: server,
191
+ base: server.apiBase,
192
+ iam,
193
+ setup,
194
+ onboarding: () => onboarding.status(),
195
+ // ADR 0054 §7: close the gateway's connection, open the new database, restart the instance.
196
+ requestRestart: () => {
197
+ void reopen()
198
+ .then(() => options.restartInstance?.())
199
+ .catch((error) => logger.warn(formatCompact({ op: 'gateway_database_switch_failed', error: errorText(error) })));
200
+ },
201
+ }));
202
+ // `/auth/*` belongs to the gateway alone: nothing under it is forwarded to the instance.
203
+ disposers.push(server.route('ALL', `${server.apiBase}/auth/*`, () => jsonResponse(404, { success: false, code: 'not_found', error: 'Not found' }), { summary: 'Unknown /auth route', tags: ['auth'] }));
204
+ disposers.push(server.useConsoleRpc(createGatewayAccountRpc({ iam, onboarding })));
205
+ const ready = reopen().catch((error) => {
206
+ logger.warn(formatCompact({ op: 'gateway_database_open_failed', error: errorText(error) }));
207
+ });
208
+ let closeResult;
209
+ return Object.freeze({
210
+ ready,
211
+ iam,
212
+ database: () => connection.database,
213
+ setup,
214
+ onboarding,
215
+ terminal,
216
+ reopenDatabase: reopen,
217
+ localAdmin,
218
+ close() {
219
+ closeResult ??= (async () => {
220
+ closed = true;
221
+ clearTimeout(retryTimer);
222
+ terminal.close();
223
+ for (const dispose of disposers.splice(0).reverse()) {
224
+ try {
225
+ dispose();
226
+ }
227
+ catch { /* keep closing */ }
228
+ }
229
+ await opening?.catch(() => undefined);
230
+ await disconnect();
231
+ })();
232
+ return closeResult;
233
+ },
234
+ });
235
+ }
236
+ /** `console.terminal` of the expanded root config. */
237
+ function readTerminalBlock(document) {
238
+ const block = document.console;
239
+ return block && typeof block === 'object' && !Array.isArray(block) ? block.terminal : undefined;
240
+ }
241
+ const ONBOARDING_STEPS = new Set(['ai', 'adapters', 'finish']);
242
+ /**
243
+ * `iam.*` (ADR 0049 §5) and `onboarding.update` (ADR 0053 §4) on the gateway's
244
+ * `POST {base}/console/request`. Authorization uses the shared permission map
245
+ * (`CONSOLE_RPC_PERMISSIONS`); denials and writes are audited like in the instance dispatcher.
246
+ */
247
+ export function createGatewayAccountRpc(options) {
248
+ return Object.freeze({
249
+ owns: (type) => isIamRpc(type) || type === ONBOARDING_RPC.UPDATE,
250
+ async handle(message, context, registry) {
251
+ const noStore = { 'cache-control': 'no-store' };
252
+ const requestId = message.requestId;
253
+ const principal = context.principal;
254
+ if (!principal) {
255
+ return jsonResponse(401, { success: false, code: 'unauthorized', error: 'Invalid or missing token', requestId }, noStore);
256
+ }
257
+ const decision = authorizeConsoleRpc(principal, message);
258
+ const audit = (outcome) => {
259
+ try {
260
+ registry.accessProvider?.audit?.({
261
+ actor: auditActor(principal),
262
+ action: `rpc:${String(message.type ?? '')}`.slice(0, 256),
263
+ ...(decision.allowed && rpcAuditTarget(decision.rule) ? { target: rpcAuditTarget(decision.rule) } : {}),
264
+ outcome,
265
+ detail: rpcAuditDetail(message),
266
+ ...(context.remoteAddress ? { ip: context.remoteAddress } : {}),
267
+ });
268
+ }
269
+ catch { /* audit never breaks a request */ }
270
+ };
271
+ if (!decision.allowed) {
272
+ audit('denied');
273
+ return jsonResponse(403, { success: false, code: decision.code, error: decision.error, requestId }, noStore);
274
+ }
275
+ const result = isIamRpc(message.type)
276
+ ? await dispatchIamRpc(message, options.iam(), principal)
277
+ : await updateOnboarding(options.onboarding, message, principal);
278
+ if (decision.write)
279
+ audit(result.status < 400 && result.body.success !== false ? 'ok' : 'error');
280
+ else if (result.status === 403)
281
+ audit('denied');
282
+ return jsonResponse(result.status, result.body, { ...noStore, ...result.headers });
283
+ },
284
+ });
285
+ }
286
+ async function updateOnboarding(onboarding, message, principal) {
287
+ const type = String(message.type ?? '');
288
+ const requestId = message.requestId;
289
+ // The Console sends `{ type, step, status }`; `data` is accepted as well.
290
+ const payload = (message.data && typeof message.data === 'object' && !Array.isArray(message.data) ? message.data : message);
291
+ try {
292
+ const step = payload.step;
293
+ if (typeof step !== 'string' || !ONBOARDING_STEPS.has(step))
294
+ throw new OnboardingError('invalid_input', 'step 只能是 ai / adapters / finish');
295
+ const status = step === 'finish'
296
+ ? await onboarding.complete({ principal })
297
+ : await onboarding.markStep(step, payload.status, { principal });
298
+ return { status: 200, body: { success: true, data: status, type, requestId } };
299
+ }
300
+ catch (error) {
301
+ if (error instanceof OnboardingError) {
302
+ return { status: error.status, body: { success: false, code: error.code, error: error.message, requestId } };
303
+ }
304
+ return { status: 500, body: { success: false, code: 'failed', error: '引导状态更新失败', requestId } };
305
+ }
306
+ }
307
+ function errorText(error) {
308
+ return error instanceof Error ? error.message : String(error);
309
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Gateway events on the Console event stream (ADR 0054 §4): package transaction progress
3
+ * (`gateway.packages.progress`) must reach the Console while the instance is stopped for the
4
+ * transaction, so the gateway owns `GET {base}/events`:
5
+ *
6
+ * - instance ready: the instance's SSE stream is forwarded and gateway frames are spliced in
7
+ * between its frames (gateway frames carry no `id:`, so the resumable cursor stays the
8
+ * instance's); the stream ends with the instance's;
9
+ * - instance offline: a gateway-only stream, closed as soon as the instance is ready again so the
10
+ * Console reconnects to the merged stream. `GET {base}/events/history` then answers an empty
11
+ * page that keeps the caller's cursor.
12
+ *
13
+ * A succeeded plugin enable / disable that changed `disable_plugins` also sends
14
+ * `plugin:lifecycle-updated` on `{base}/events` (the Console reloads its entries on it).
15
+ *
16
+ * `GET {base}/gateway/packages/progress` serves the progress frames alone.
17
+ */
18
+ import type { GatewayPackageEvent } from '@zhin.js/console-protocol';
19
+ import type { GatewayHttpServer } from './gateway-http-server.js';
20
+ /** Console event the client reloads its entries on (`CONSOLE_ENTRY_LIFECYCLE_EVENTS`). */
21
+ export declare const PLUGIN_LIFECYCLE_UPDATED = "plugin:lifecycle-updated";
22
+ export interface GatewayEventSource {
23
+ /** Package transaction events (`GatewayPackageService.subscribe`). */
24
+ subscribe(listener: (event: GatewayPackageEvent) => void): () => void;
25
+ }
26
+ export interface GatewayInstanceReadiness {
27
+ /** Called whenever the instance's route table changes (ready, reload, gone). */
28
+ onChange(listener: () => void): () => void;
29
+ }
30
+ export interface RegisterGatewayEventRoutesOptions {
31
+ readonly server: GatewayHttpServer;
32
+ readonly packages?: GatewayEventSource;
33
+ readonly instance?: GatewayInstanceReadiness;
34
+ readonly keepAliveMs?: number;
35
+ }
36
+ export declare function gatewayEventFrame(type: string, data: unknown): string;
37
+ /** Registers the gateway's event routes; returns the unregister function. */
38
+ export declare function registerGatewayEventRoutes(options: RegisterGatewayEventRoutesOptions): () => void;
39
+ /** The text after the last complete SSE frame (empty when the buffer ends on a frame boundary). */
40
+ export declare function trailingPartialFrame(buffer: string): string;