@zhin.js/gateway 0.0.0-stage → 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 +60 -2
  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 +116 -3
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The project `database` block as the gateway resolves it (ADR 0054 §7): the same file and
3
+ * driver settings as the instance's `resolveDatabaseConfig` (`@zhin.js/core`), ported here so the
4
+ * gateway never imports IM layers. Keep the two in step.
5
+ */
6
+ import { mkdir } from 'node:fs/promises';
7
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
8
+ import { expandEnvironmentValue } from '@zhin.js/plugin-runtime';
9
+ import { createConfigDocument } from '@zhin.js/runtime';
10
+ import { findConfigFile } from '../project/config-file.js';
11
+ import { loadRuntimeEnvironmentLayers } from '../project/environment.js';
12
+ /** Project-relative default when no `database` block is configured. */
13
+ export const DEFAULT_PROJECT_DATABASE_FILE = join('.zhin', 'data.sqlite');
14
+ const SUPPORTED_DIALECTS = new Set(['memory', 'mongodb', 'mysql', 'pg', 'redis', 'sqlite']);
15
+ function configurationError(path, expectation, envExample) {
16
+ const envHint = envExample
17
+ ? `若这里引用环境变量,请在项目根目录 .env 中设置 ${envExample}。`
18
+ : '若这里引用环境变量,请检查项目根目录 .env 中的对应变量。';
19
+ return new TypeError(`数据库配置 ${path} ${expectation}。${envHint}也可运行 \`zhin setup --database\` 重新配置。`);
20
+ }
21
+ function normalizePort(value, path, envExample) {
22
+ if (value === undefined)
23
+ return undefined;
24
+ const normalized = typeof value === 'number'
25
+ ? value
26
+ : typeof value === 'string' && /^\d+$/u.test(value.trim()) ? Number(value.trim()) : Number.NaN;
27
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 65535) {
28
+ throw configurationError(path, '必须是 1-65535 的整数,当前值为空或无效', envExample);
29
+ }
30
+ return normalized;
31
+ }
32
+ function normalizeOptionalText(config, key, path, envExample) {
33
+ if (!(key in config))
34
+ return;
35
+ const value = config[key];
36
+ if (typeof value !== 'string' || value.trim() === '')
37
+ throw configurationError(path, '不能为空', envExample);
38
+ config[key] = value.trim();
39
+ }
40
+ function normalizeRequiredText(config, key, path, envExample) {
41
+ if (!(key in config))
42
+ config[key] = undefined;
43
+ normalizeOptionalText(config, key, path, envExample);
44
+ }
45
+ function normalizeRedisDatabase(value) {
46
+ if (value === undefined)
47
+ return undefined;
48
+ const normalized = typeof value === 'number'
49
+ ? value
50
+ : typeof value === 'string' && /^\d+$/u.test(value.trim()) ? Number(value.trim()) : Number.NaN;
51
+ if (!Number.isInteger(normalized) || normalized < 0) {
52
+ throw configurationError('database.database', '必须是大于或等于 0 的整数', 'REDIS_DB=0');
53
+ }
54
+ return normalized;
55
+ }
56
+ /** Validates an expanded `database` block (port of `normalizeDatabaseHostConfig`). */
57
+ export function normalizeProjectDatabaseConfig(input) {
58
+ const dialect = input.dialect;
59
+ if (!SUPPORTED_DIALECTS.has(dialect)) {
60
+ throw new TypeError(`数据库配置 database.dialect 不支持 ${JSON.stringify(dialect)};可选值为 sqlite、mysql、pg、mongodb、redis、memory。`);
61
+ }
62
+ const config = { ...input, dialect };
63
+ switch (dialect) {
64
+ case 'mysql':
65
+ normalizeOptionalText(config, 'host', 'database.host', 'DB_HOST=127.0.0.1');
66
+ normalizeOptionalText(config, 'user', 'database.user', 'DB_USER=root');
67
+ normalizeOptionalText(config, 'database', 'database.database', 'DB_DATABASE=zhin_bot');
68
+ normalizeOptionalText(config, 'uri', 'database.uri');
69
+ config.port = normalizePort(config.port, 'database.port', 'DB_PORT=3306');
70
+ if (config.port === undefined)
71
+ delete config.port;
72
+ break;
73
+ case 'pg':
74
+ normalizeOptionalText(config, 'host', 'database.host', 'DB_HOST=127.0.0.1');
75
+ normalizeOptionalText(config, 'user', 'database.user', 'DB_USER=postgres');
76
+ normalizeOptionalText(config, 'database', 'database.database', 'DB_DATABASE=zhin_bot');
77
+ normalizeOptionalText(config, 'connectionString', 'database.connectionString');
78
+ config.port = normalizePort(config.port, 'database.port', 'DB_PORT=5432');
79
+ if (config.port === undefined)
80
+ delete config.port;
81
+ break;
82
+ case 'mongodb':
83
+ normalizeRequiredText(config, 'url', 'database.url', 'DB_URL=mongodb://127.0.0.1:27017');
84
+ normalizeRequiredText(config, 'dbName', 'database.dbName', 'DB_NAME=zhin_bot');
85
+ if (!/^mongodb(?:\+srv)?:\/\//u.test(String(config.url)))
86
+ throw configurationError('database.url', '必须使用 mongodb:// 或 mongodb+srv://');
87
+ break;
88
+ case 'redis': {
89
+ normalizeOptionalText(config, 'url', 'database.url', 'REDIS_URL=redis://127.0.0.1:6379');
90
+ if (config.socket !== undefined) {
91
+ if (!config.socket || typeof config.socket !== 'object' || Array.isArray(config.socket))
92
+ throw configurationError('database.socket', '必须是对象');
93
+ const socket = { ...config.socket };
94
+ normalizeOptionalText(socket, 'host', 'database.socket.host', 'REDIS_HOST=127.0.0.1');
95
+ socket.port = normalizePort(socket.port, 'database.socket.port', 'REDIS_PORT=6379');
96
+ if (socket.port === undefined)
97
+ delete socket.port;
98
+ config.socket = Object.freeze(socket);
99
+ }
100
+ config.database = normalizeRedisDatabase(config.database);
101
+ if (config.database === undefined)
102
+ delete config.database;
103
+ break;
104
+ }
105
+ case 'sqlite':
106
+ if (config.filename === undefined)
107
+ config.filename = './data/bot.db';
108
+ normalizeOptionalText(config, 'filename', 'database.filename');
109
+ break;
110
+ case 'memory':
111
+ break;
112
+ }
113
+ return Object.freeze(config);
114
+ }
115
+ /**
116
+ * Resolves the expanded root config document's `database` block. A relative SQLite file is
117
+ * anchored at the project root (the instance runs with the project root as cwd, so both
118
+ * processes open the same file).
119
+ */
120
+ export async function resolveProjectDatabaseConfig(projectRoot, document) {
121
+ const configured = document && typeof document === 'object' ? document.database : undefined;
122
+ if (configured && typeof configured === 'object' && !Array.isArray(configured)) {
123
+ const value = configured;
124
+ const normalized = normalizeProjectDatabaseConfig({ ...value, dialect: typeof value.dialect === 'string' ? value.dialect : 'sqlite' });
125
+ if (normalized.dialect === 'sqlite' && typeof normalized.filename === 'string' && !isSqliteMemory(normalized.filename) && !isAbsolute(normalized.filename)) {
126
+ return Object.freeze({ ...normalized, filename: resolve(projectRoot, normalized.filename) });
127
+ }
128
+ return normalized;
129
+ }
130
+ const filename = join(projectRoot, DEFAULT_PROJECT_DATABASE_FILE);
131
+ await mkdir(dirname(filename), { recursive: true });
132
+ return Object.freeze({ dialect: 'sqlite', filename });
133
+ }
134
+ /**
135
+ * The root config document with `${…}` expanded against process env and the project `.env`
136
+ * layers of `environment` (missing variables become empty, as in the instance). Only the gateway's
137
+ * own blocks are read from it (`database`, `console.terminal`): plugin declarations are not
138
+ * validated here, so a config the instance rejects still leaves the Console reachable.
139
+ */
140
+ export async function loadExpandedProjectConfig(projectRoot, environment = 'development') {
141
+ const file = findConfigFile(projectRoot);
142
+ if (!file)
143
+ return Object.freeze({});
144
+ const [document, layers] = await Promise.all([
145
+ createConfigDocument(join(projectRoot, file)).read().then((snapshot) => snapshot.document),
146
+ loadRuntimeEnvironmentLayers(projectRoot, environment),
147
+ ]);
148
+ if (!document || typeof document !== 'object' || Array.isArray(document))
149
+ return Object.freeze({});
150
+ const overlay = layers.environments?.[environment];
151
+ return expandEnvironmentValue(document, (key) => overlay?.[key] ?? layers.base?.[key] ?? process.env[key]);
152
+ }
153
+ /** {@link resolveProjectDatabaseConfig} over {@link loadExpandedProjectConfig}. */
154
+ export async function loadProjectDatabaseConfig(projectRoot, environment = 'development') {
155
+ return resolveProjectDatabaseConfig(projectRoot, await loadExpandedProjectConfig(projectRoot, environment));
156
+ }
157
+ function isSqliteMemory(filename) {
158
+ return filename === ':memory:' || filename.startsWith('file::memory:');
159
+ }
@@ -0,0 +1,2 @@
1
+ export * from './config.js';
2
+ export * from './project-database.js';
@@ -0,0 +1,2 @@
1
+ export * from './config.js';
2
+ export * from './project-database.js';
@@ -0,0 +1,31 @@
1
+ import type { DatabaseHostModel } from '@zhin.js/plugin-runtime';
2
+ import type { ProjectDatabaseConfig } from './config.js';
3
+ /**
4
+ * How long a gateway statement retries `SQLITE_BUSY` while the instance holds the write lock
5
+ * (`busyRetryMs` in `@zhin.js/database`). The retry backs off asynchronously, so the gateway's
6
+ * event loop keeps serving requests during the wait; a synchronous `busy_timeout` would stall
7
+ * every request for as long as an instance transaction runs. Past the budget the request
8
+ * answers `503 database_busy` ({@link isDatabaseBusyError}).
9
+ */
10
+ export declare const GATEWAY_SQLITE_BUSY_RETRY_MS = 5000;
11
+ /** Seconds a `503 database_busy` asks the client to wait. */
12
+ export declare const DATABASE_BUSY_RETRY_AFTER_SECONDS = 2;
13
+ /** The database stayed locked by another connection for the whole retry budget. */
14
+ export declare function isDatabaseBusyError(error: unknown): boolean;
15
+ /** `503 { code: 'database_busy' }` with `Retry-After`. */
16
+ export declare function databaseBusyResponse(extra?: Readonly<Record<string, unknown>>): Response;
17
+ /** One database connection; tables are defined before {@link start}. */
18
+ export declare class ProjectDatabase {
19
+ #private;
20
+ readonly config: ProjectDatabaseConfig;
21
+ constructor(config: ProjectDatabaseConfig);
22
+ get dialect(): string;
23
+ get started(): boolean;
24
+ define(name: string, definition: Record<string, unknown>): void;
25
+ tables(): readonly string[];
26
+ readonly models: Readonly<{
27
+ get: (name: string) => DatabaseHostModel | undefined;
28
+ }>;
29
+ start(): Promise<void>;
30
+ stop(): Promise<void>;
31
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The gateway's own connection to the project database (ADR 0054 §7). It defines and serves only
3
+ * the tables the gateway owns (`console_*`, ADR 0049); the instance opens the same database for
4
+ * everything else. SQLite runs in WAL mode (`@zhin.js/database`), so both processes can hold the
5
+ * file open.
6
+ */
7
+ import { isSqliteBusyError, Registry } from '@zhin.js/database';
8
+ /**
9
+ * How long a gateway statement retries `SQLITE_BUSY` while the instance holds the write lock
10
+ * (`busyRetryMs` in `@zhin.js/database`). The retry backs off asynchronously, so the gateway's
11
+ * event loop keeps serving requests during the wait; a synchronous `busy_timeout` would stall
12
+ * every request for as long as an instance transaction runs. Past the budget the request
13
+ * answers `503 database_busy` ({@link isDatabaseBusyError}).
14
+ */
15
+ export const GATEWAY_SQLITE_BUSY_RETRY_MS = 5_000;
16
+ /** Seconds a `503 database_busy` asks the client to wait. */
17
+ export const DATABASE_BUSY_RETRY_AFTER_SECONDS = 2;
18
+ /** The database stayed locked by another connection for the whole retry budget. */
19
+ export function isDatabaseBusyError(error) {
20
+ for (let current = error, depth = 0; current && depth < 4; depth += 1) {
21
+ if (isSqliteBusyError(current))
22
+ return true;
23
+ current = current.cause;
24
+ }
25
+ return false;
26
+ }
27
+ /** `503 { code: 'database_busy' }` with `Retry-After`. */
28
+ export function databaseBusyResponse(extra = {}) {
29
+ return new Response(JSON.stringify({
30
+ success: false,
31
+ code: 'database_busy',
32
+ error: '数据库正忙,请稍后重试',
33
+ ...extra,
34
+ }), {
35
+ status: 503,
36
+ headers: {
37
+ 'content-type': 'application/json; charset=utf-8',
38
+ 'cache-control': 'no-store',
39
+ 'retry-after': String(DATABASE_BUSY_RETRY_AFTER_SECONDS),
40
+ },
41
+ });
42
+ }
43
+ function wrapModel(model) {
44
+ return Object.freeze({
45
+ select: (...fields) => {
46
+ if (fields.includes('*'))
47
+ throw new TypeError('select() requires explicit column names');
48
+ const selection = model.select(...fields);
49
+ const chain = {
50
+ where: (query) => { selection.where(query); return chain; },
51
+ orderBy: (field, direction) => { selection.orderBy?.(field, direction); return chain; },
52
+ limit: (count) => { selection.limit?.(count); return chain; },
53
+ then: (onfulfilled, onrejected) => Promise.resolve(selection).then(onfulfilled, onrejected),
54
+ };
55
+ return chain;
56
+ },
57
+ insert: (row) => Promise.resolve(model.insert(row)),
58
+ delete: () => ({ where: (query) => Promise.resolve(model.delete(query)) }),
59
+ update: (patch) => ({ where: (query) => Promise.resolve(model.update(patch).where(query)) }),
60
+ });
61
+ }
62
+ /** One database connection; tables are defined before {@link start}. */
63
+ export class ProjectDatabase {
64
+ config;
65
+ #db;
66
+ #tables = new Set();
67
+ #wrapped = new Map();
68
+ #started = false;
69
+ constructor(config) {
70
+ this.config = config;
71
+ const { dialect, ...connection } = config;
72
+ // No synchronous busy_timeout: it would block the gateway's event loop (ADR 0054 §7).
73
+ const options = dialect === 'sqlite' && connection.busyRetryMs === undefined
74
+ ? { ...connection, busyRetryMs: GATEWAY_SQLITE_BUSY_RETRY_MS }
75
+ : connection;
76
+ this.#db = Registry.create(dialect, options);
77
+ }
78
+ get dialect() {
79
+ return this.config.dialect;
80
+ }
81
+ get started() {
82
+ return this.#started;
83
+ }
84
+ define(name, definition) {
85
+ if (this.#started)
86
+ throw new Error(`Project database already started; cannot define table ${name}`);
87
+ if (this.#tables.has(name))
88
+ return;
89
+ this.#tables.add(name);
90
+ this.#db.define(name, definition);
91
+ }
92
+ tables() {
93
+ return [...this.#tables];
94
+ }
95
+ models = Object.freeze({
96
+ get: (name) => {
97
+ if (!this.#started)
98
+ return undefined;
99
+ const cached = this.#wrapped.get(name);
100
+ if (cached)
101
+ return cached;
102
+ const raw = this.#db.models.get(name);
103
+ if (!raw)
104
+ return undefined;
105
+ const model = wrapModel(raw);
106
+ this.#wrapped.set(name, model);
107
+ return model;
108
+ },
109
+ });
110
+ async start() {
111
+ if (this.#started)
112
+ return;
113
+ try {
114
+ await this.#db.start();
115
+ }
116
+ catch (error) {
117
+ await this.#db.stop().catch(() => undefined);
118
+ throw error;
119
+ }
120
+ this.#started = true;
121
+ this.#wrapped.clear();
122
+ }
123
+ async stop() {
124
+ if (!this.#started)
125
+ return;
126
+ this.#started = false;
127
+ this.#wrapped.clear();
128
+ await this.#db.stop();
129
+ }
130
+ }
@@ -0,0 +1,2 @@
1
+ /** `@zhin.js/gateway/database` — the gateway's connection to the project database (ADR 0054 §7). */
2
+ export * from './database/index.js';
@@ -0,0 +1,2 @@
1
+ /** `@zhin.js/gateway/database` — the gateway's connection to the project database (ADR 0054 §7). */
2
+ export * from './database/index.js';
@@ -0,0 +1,19 @@
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
+ /** Further gateway-owned RPC types (`iam.*`, `onboarding.update`), see `GatewayHttpServer.useConsoleRpc`. */
16
+ export interface GatewayRpcHandler extends GatewayConsoleRpc {
17
+ owns(type: string): boolean;
18
+ }
19
+ 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;