@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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/lib/config/ai-config-loader.d.ts +22 -0
- package/lib/config/ai-config-loader.js +96 -0
- package/lib/config/configuration-chat-policy.d.ts +57 -0
- package/lib/config/configuration-chat-policy.js +423 -0
- package/lib/config/configuration-patch.d.ts +16 -0
- package/lib/config/configuration-patch.js +105 -0
- package/lib/config/configuration-path.d.ts +6 -0
- package/lib/config/configuration-path.js +40 -0
- package/lib/config/configuration-projection.d.ts +6 -0
- package/lib/config/configuration-projection.js +28 -0
- package/lib/config/configuration-redaction.d.ts +24 -0
- package/lib/config/configuration-redaction.js +128 -0
- package/lib/config/configuration.d.ts +81 -0
- package/lib/config/configuration.js +686 -0
- package/lib/config/console-schema.d.ts +11 -0
- package/lib/config/console-schema.js +222 -0
- package/lib/config/environment-files.d.ts +2 -0
- package/lib/config/environment-files.js +21 -0
- package/lib/config/plugin-package-map.d.ts +2 -0
- package/lib/config/plugin-package-map.js +14 -0
- package/lib/config/plugin-schema-catalog.d.ts +22 -0
- package/lib/config/plugin-schema-catalog.js +262 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.js +12 -0
- package/lib/database/config.d.ts +22 -0
- package/lib/database/config.js +159 -0
- package/lib/database/index.d.ts +2 -0
- package/lib/database/index.js +2 -0
- package/lib/database/project-database.d.ts +31 -0
- package/lib/database/project-database.js +130 -0
- package/lib/database.d.ts +2 -0
- package/lib/database.js +2 -0
- package/lib/http/console-rpc.d.ts +19 -0
- package/lib/http/console-rpc.js +51 -0
- package/lib/http/gateway-console.d.ts +63 -0
- package/lib/http/gateway-console.js +309 -0
- package/lib/http/gateway-events.d.ts +35 -0
- package/lib/http/gateway-events.js +172 -0
- package/lib/http/gateway-http-server.d.ts +95 -0
- package/lib/http/gateway-http-server.js +518 -0
- package/lib/http/local-principal.d.ts +2 -0
- package/lib/http/local-principal.js +16 -0
- package/lib/http/run-gateway.d.ts +43 -0
- package/lib/http/run-gateway.js +268 -0
- package/lib/http/start-gateway.d.ts +114 -0
- package/lib/http/start-gateway.js +220 -0
- package/lib/http-response.d.ts +6 -0
- package/lib/http-response.js +25 -0
- package/lib/http.d.ts +6 -0
- package/lib/http.js +6 -0
- package/lib/iam/crypto.d.ts +12 -0
- package/lib/iam/crypto.js +58 -0
- package/lib/iam/index.d.ts +8 -0
- package/lib/iam/index.js +8 -0
- package/lib/iam/install.d.ts +36 -0
- package/lib/iam/install.js +74 -0
- package/lib/iam/local-admin.d.ts +27 -0
- package/lib/iam/local-admin.js +102 -0
- package/lib/iam/rate-limit.d.ts +15 -0
- package/lib/iam/rate-limit.js +52 -0
- package/lib/iam/routes.d.ts +23 -0
- package/lib/iam/routes.js +157 -0
- package/lib/iam/rpc.d.ts +14 -0
- package/lib/iam/rpc.js +100 -0
- package/lib/iam/service.d.ts +203 -0
- package/lib/iam/service.js +784 -0
- package/lib/iam/setup-service.d.ts +83 -0
- package/lib/iam/setup-service.js +206 -0
- package/lib/iam/tables.d.ts +20 -0
- package/lib/iam/tables.js +82 -0
- package/lib/iam.d.ts +2 -0
- package/lib/iam.js +2 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +22 -0
- package/lib/local/client.d.ts +71 -0
- package/lib/local/client.js +296 -0
- package/lib/local/handler.d.ts +30 -0
- package/lib/local/handler.js +86 -0
- package/lib/local/local-socket.d.ts +55 -0
- package/lib/local/local-socket.js +232 -0
- package/lib/local/paths.d.ts +34 -0
- package/lib/local/paths.js +58 -0
- package/lib/local/protocol.d.ts +140 -0
- package/lib/local/protocol.js +90 -0
- package/lib/local/state-file.d.ts +8 -0
- package/lib/local/state-file.js +45 -0
- package/lib/local.d.ts +10 -0
- package/lib/local.js +10 -0
- package/lib/onboarding/allowlist.d.ts +54 -0
- package/lib/onboarding/allowlist.js +106 -0
- package/lib/onboarding/errors.d.ts +7 -0
- package/lib/onboarding/errors.js +17 -0
- package/lib/onboarding/index.d.ts +6 -0
- package/lib/onboarding/index.js +6 -0
- package/lib/onboarding/install.d.ts +99 -0
- package/lib/onboarding/install.js +132 -0
- package/lib/onboarding/package-manager.d.ts +47 -0
- package/lib/onboarding/package-manager.js +179 -0
- package/lib/onboarding/service.d.ts +53 -0
- package/lib/onboarding/service.js +144 -0
- package/lib/onboarding/state-store.d.ts +26 -0
- package/lib/onboarding/state-store.js +97 -0
- package/lib/onboarding.d.ts +2 -0
- package/lib/onboarding.js +2 -0
- package/lib/packages/authorization.d.ts +23 -0
- package/lib/packages/authorization.js +109 -0
- package/lib/packages/errors.d.ts +11 -0
- package/lib/packages/errors.js +26 -0
- package/lib/packages/index.d.ts +5 -0
- package/lib/packages/index.js +5 -0
- package/lib/packages/rpc.d.ts +30 -0
- package/lib/packages/rpc.js +155 -0
- package/lib/packages/service.d.ts +121 -0
- package/lib/packages/service.js +441 -0
- package/lib/packages/snapshot.d.ts +14 -0
- package/lib/packages/snapshot.js +73 -0
- package/lib/packages.d.ts +2 -0
- package/lib/packages.js +2 -0
- package/lib/plugin-management/activity.d.ts +6 -0
- package/lib/plugin-management/activity.js +14 -0
- package/lib/plugin-management/config-explanation.d.ts +6 -0
- package/lib/plugin-management/config-explanation.js +113 -0
- package/lib/plugin-management/use-cases.d.ts +15 -0
- package/lib/plugin-management/use-cases.js +120 -0
- package/lib/plugin-management/workflow.d.ts +92 -0
- package/lib/plugin-management/workflow.js +772 -0
- package/lib/plugin-management.d.ts +5 -0
- package/lib/plugin-management.js +5 -0
- package/lib/project/config-file.d.ts +6 -0
- package/lib/project/config-file.js +58 -0
- package/lib/project/environment.d.ts +13 -0
- package/lib/project/environment.js +52 -0
- package/lib/project/instance-declarations.d.ts +55 -0
- package/lib/project/instance-declarations.js +542 -0
- package/lib/project/legacy-config.d.ts +25 -0
- package/lib/project/legacy-config.js +70 -0
- package/lib/project/node-requirements.d.ts +7 -0
- package/lib/project/node-requirements.js +20 -0
- package/lib/project/tsx-module-loader.d.ts +7 -0
- package/lib/project/tsx-module-loader.js +29 -0
- package/lib/project.d.ts +7 -0
- package/lib/project.js +7 -0
- package/lib/server.d.ts +11 -0
- package/lib/server.js +11 -0
- package/lib/supervisor/instance-logs.d.ts +39 -0
- package/lib/supervisor/instance-logs.js +152 -0
- package/lib/supervisor/instance-supervisor.d.ts +140 -0
- package/lib/supervisor/instance-supervisor.js +408 -0
- package/lib/supervisor/run-instance.d.ts +35 -0
- package/lib/supervisor/run-instance.js +161 -0
- package/lib/supervisor/terminal-state.d.ts +30 -0
- package/lib/supervisor/terminal-state.js +57 -0
- package/lib/supervisor.d.ts +4 -0
- package/lib/supervisor.js +4 -0
- package/lib/terminal/backends.d.ts +38 -0
- package/lib/terminal/backends.js +193 -0
- package/lib/terminal/config.d.ts +63 -0
- package/lib/terminal/config.js +237 -0
- package/lib/terminal/host.d.ts +26 -0
- package/lib/terminal/host.js +76 -0
- package/lib/terminal/index.d.ts +60 -0
- package/lib/terminal/index.js +135 -0
- package/lib/terminal/modules.d.ts +87 -0
- package/lib/terminal/modules.js +44 -0
- package/lib/terminal/service.d.ts +101 -0
- package/lib/terminal/service.js +455 -0
- package/lib/terminal/tickets.d.ts +46 -0
- package/lib/terminal/tickets.js +76 -0
- package/lib/terminal.d.ts +3 -0
- package/lib/terminal.js +3 -0
- package/lib/usage/aggregate.d.ts +87 -0
- package/lib/usage/aggregate.js +212 -0
- package/lib/usage.d.ts +2 -0
- package/lib/usage.js +2 -0
- package/package.json +117 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type DatabaseProbeResult, type ProbeDialect } from '@zhin.js/database';
|
|
2
|
+
import { type ConsoleIamService, type ConsoleUserView } from './service.js';
|
|
3
|
+
export type SetupDialect = ProbeDialect;
|
|
4
|
+
export interface SetupDatabaseField {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly type: 'string' | 'integer' | 'enum';
|
|
7
|
+
readonly required: boolean;
|
|
8
|
+
readonly secret: boolean;
|
|
9
|
+
readonly default?: string | number;
|
|
10
|
+
readonly options?: readonly string[];
|
|
11
|
+
/** Env variable a secret is written to. */
|
|
12
|
+
readonly env?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SetupDriver {
|
|
15
|
+
readonly dialect: SetupDialect;
|
|
16
|
+
readonly label: string;
|
|
17
|
+
readonly description: string;
|
|
18
|
+
readonly available: boolean;
|
|
19
|
+
readonly missingPackage?: string;
|
|
20
|
+
readonly fields: readonly SetupDatabaseField[];
|
|
21
|
+
}
|
|
22
|
+
export interface SetupDatabaseSummary {
|
|
23
|
+
readonly dialect: string;
|
|
24
|
+
readonly source: 'default' | 'config';
|
|
25
|
+
/** Connection facts without secrets. */
|
|
26
|
+
readonly summary: Readonly<Record<string, string | number>>;
|
|
27
|
+
}
|
|
28
|
+
export interface SetupStatus {
|
|
29
|
+
readonly setupRequired: boolean;
|
|
30
|
+
readonly database: SetupDatabaseSummary;
|
|
31
|
+
}
|
|
32
|
+
export interface SetupDatabaseInput {
|
|
33
|
+
readonly dialect: unknown;
|
|
34
|
+
/** Field values; a secret may be a `${VAR}` reference to an existing variable instead. */
|
|
35
|
+
readonly connection?: unknown;
|
|
36
|
+
}
|
|
37
|
+
/** Config-file and dotenv access the setup flow needs (ConsoleConfigurationStore satisfies `config`). */
|
|
38
|
+
export interface SetupConfigPort {
|
|
39
|
+
readDocument(): Promise<Record<string, unknown>>;
|
|
40
|
+
setKey(key: string, value: unknown): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface SetupEnvFilePort {
|
|
43
|
+
read(): Promise<string>;
|
|
44
|
+
write(content: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export interface ConsoleSetupServiceOptions {
|
|
47
|
+
readonly projectRoot: string;
|
|
48
|
+
readonly config: SetupConfigPort;
|
|
49
|
+
readonly envFile: SetupEnvFilePort;
|
|
50
|
+
/** The access-control service bound to the database currently in use. */
|
|
51
|
+
readonly iam: () => ConsoleIamService | undefined;
|
|
52
|
+
/** Variables for `${…}` expansion (process env + project dotenv layers). */
|
|
53
|
+
readonly environment?: () => Promise<Readonly<Record<string, string | undefined>>>;
|
|
54
|
+
readonly probe?: (dialect: SetupDialect, config: Record<string, unknown>) => Promise<DatabaseProbeResult>;
|
|
55
|
+
readonly driverAvailable?: (dialect: SetupDialect) => Promise<boolean>;
|
|
56
|
+
}
|
|
57
|
+
export declare const SETUP_NO_MIGRATION_NOTICE = "\u5207\u6362\u6570\u636E\u5E93\u4E0D\u4F1A\u8FC1\u79FB\u65E7\u5E93\u6570\u636E\uFF1A\u63D2\u4EF6\u6570\u636E\u4ECD\u7559\u5728\u539F\u6570\u636E\u5E93\u3002";
|
|
58
|
+
/**
|
|
59
|
+
* The one setup flow (ADR 0049 §7): `/auth/setup*` and `zhin console init` are thin wrappers.
|
|
60
|
+
* Database changes are written to the config file / `.env`; restarting is the caller's job.
|
|
61
|
+
*/
|
|
62
|
+
export declare class ConsoleSetupService {
|
|
63
|
+
#private;
|
|
64
|
+
constructor(options: ConsoleSetupServiceOptions);
|
|
65
|
+
setupRequired(): boolean;
|
|
66
|
+
status(): Promise<SetupStatus>;
|
|
67
|
+
drivers(): Promise<readonly SetupDriver[]>;
|
|
68
|
+
testDatabase(input: SetupDatabaseInput): Promise<DatabaseProbeResult>;
|
|
69
|
+
/** Tests, then writes `database` (secrets as `${…}` into `.env`). */
|
|
70
|
+
applyDatabase(input: SetupDatabaseInput): Promise<{
|
|
71
|
+
restartRequired: true;
|
|
72
|
+
notice: string;
|
|
73
|
+
}>;
|
|
74
|
+
createOwner(input: Readonly<{
|
|
75
|
+
username: unknown;
|
|
76
|
+
password: unknown;
|
|
77
|
+
displayName?: unknown;
|
|
78
|
+
}>): Promise<{
|
|
79
|
+
user: ConsoleUserView;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
/** Sets `KEY=value` lines in dotenv text, keeping every other line. */
|
|
83
|
+
export declare function upsertDotenv(source: string, updates: Readonly<Record<string, string>>): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { isDatabaseDriverAvailable, probeDatabaseConnection, } from '@zhin.js/database';
|
|
3
|
+
import { expandEnvironmentValue } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { DATABASE_DIALECT_DEFINITIONS, validateDatabaseConfig, } from '@zhin.js/scaffold-wizard';
|
|
5
|
+
import { IamError } from './service.js';
|
|
6
|
+
import { IAM_DIALECTS } from './tables.js';
|
|
7
|
+
const DEFAULT_SQLITE_FILE = '.zhin/data.sqlite';
|
|
8
|
+
const SECRET_FIELDS = new Set(['password']);
|
|
9
|
+
const ENV_REFERENCE = /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/u;
|
|
10
|
+
export const SETUP_NO_MIGRATION_NOTICE = '切换数据库不会迁移旧库数据:插件数据仍留在原数据库。';
|
|
11
|
+
function envNameFor(field) {
|
|
12
|
+
return `ZHIN_DATABASE_${field.replace(/([a-z0-9])([A-Z])/gu, '$1_$2').toUpperCase()}`;
|
|
13
|
+
}
|
|
14
|
+
function fieldsOf(definition) {
|
|
15
|
+
const schema = definition.schema;
|
|
16
|
+
const required = new Set(schema.required ?? []);
|
|
17
|
+
return Object.freeze(Object.entries(schema.properties ?? {}).filter(([name]) => name !== 'dialect').flatMap(([name, property]) => {
|
|
18
|
+
if (property.type === 'object')
|
|
19
|
+
return [];
|
|
20
|
+
const options = Array.isArray(property.enum) ? property.enum : undefined;
|
|
21
|
+
// A MongoDB URL may embed credentials, so it is treated as a secret.
|
|
22
|
+
const secret = SECRET_FIELDS.has(name) || (definition.dialect === 'mongodb' && name === 'url');
|
|
23
|
+
let defaultValue = property.default;
|
|
24
|
+
if (definition.dialect === 'sqlite' && name === 'filename')
|
|
25
|
+
defaultValue = DEFAULT_SQLITE_FILE;
|
|
26
|
+
return [Object.freeze({
|
|
27
|
+
name,
|
|
28
|
+
type: options ? 'enum' : property.type === 'integer' ? 'integer' : 'string',
|
|
29
|
+
required: required.has(name) || property.minLength !== undefined,
|
|
30
|
+
secret,
|
|
31
|
+
...(defaultValue === undefined ? {} : { default: defaultValue }),
|
|
32
|
+
...(options ? { options: Object.freeze([...options]) } : {}),
|
|
33
|
+
...(secret ? { env: envNameFor(name) } : {}),
|
|
34
|
+
})];
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
const SETUP_DEFINITIONS = DATABASE_DIALECT_DEFINITIONS.filter(definition => IAM_DIALECTS.has(definition.dialect));
|
|
38
|
+
/**
|
|
39
|
+
* The one setup flow (ADR 0049 §7): `/auth/setup*` and `zhin console init` are thin wrappers.
|
|
40
|
+
* Database changes are written to the config file / `.env`; restarting is the caller's job.
|
|
41
|
+
*/
|
|
42
|
+
export class ConsoleSetupService {
|
|
43
|
+
#options;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.#options = options;
|
|
46
|
+
}
|
|
47
|
+
setupRequired() {
|
|
48
|
+
const iam = this.#options.iam();
|
|
49
|
+
return !(iam?.initialized() ?? false);
|
|
50
|
+
}
|
|
51
|
+
async status() {
|
|
52
|
+
return Object.freeze({ setupRequired: this.setupRequired(), database: await this.#currentDatabase() });
|
|
53
|
+
}
|
|
54
|
+
async drivers() {
|
|
55
|
+
const available = this.#options.driverAvailable ?? ((dialect) => isDatabaseDriverAvailable(dialect));
|
|
56
|
+
return Promise.all(SETUP_DEFINITIONS.map(async (definition) => {
|
|
57
|
+
const dialect = definition.dialect;
|
|
58
|
+
const ok = await available(dialect);
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
dialect,
|
|
61
|
+
label: definition.name,
|
|
62
|
+
description: definition.description,
|
|
63
|
+
available: ok,
|
|
64
|
+
...(ok || !definition.driver ? {} : { missingPackage: definition.driver.package }),
|
|
65
|
+
fields: fieldsOf(definition),
|
|
66
|
+
});
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
async testDatabase(input) {
|
|
70
|
+
this.#assertSetupOpen();
|
|
71
|
+
const prepared = await this.#prepare(input);
|
|
72
|
+
return this.#probe(prepared);
|
|
73
|
+
}
|
|
74
|
+
/** Tests, then writes `database` (secrets as `${…}` into `.env`). */
|
|
75
|
+
async applyDatabase(input) {
|
|
76
|
+
this.#assertSetupOpen();
|
|
77
|
+
const prepared = await this.#prepare(input);
|
|
78
|
+
const probe = await this.#probe(prepared);
|
|
79
|
+
if (!probe.ok)
|
|
80
|
+
throw new IamError('invalid_input', `数据库连接失败:${probe.error ?? 'unknown error'}`);
|
|
81
|
+
const envUpdates = {};
|
|
82
|
+
const block = { dialect: prepared.dialect };
|
|
83
|
+
for (const field of prepared.fields) {
|
|
84
|
+
const value = prepared.raw[field.name];
|
|
85
|
+
if (value === undefined || value === '')
|
|
86
|
+
continue;
|
|
87
|
+
if (field.secret && typeof value === 'string' && !ENV_REFERENCE.test(value)) {
|
|
88
|
+
envUpdates[field.env] = value;
|
|
89
|
+
block[field.name] = `\${${field.env}}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
block[field.name] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(envUpdates).length) {
|
|
96
|
+
await this.#options.envFile.write(upsertDotenv(await this.#options.envFile.read(), envUpdates));
|
|
97
|
+
}
|
|
98
|
+
await this.#options.config.setKey('database', block);
|
|
99
|
+
return Object.freeze({ restartRequired: true, notice: SETUP_NO_MIGRATION_NOTICE });
|
|
100
|
+
}
|
|
101
|
+
async createOwner(input) {
|
|
102
|
+
const iam = this.#options.iam();
|
|
103
|
+
if (!iam?.ready)
|
|
104
|
+
throw new IamError('unavailable', '当前数据库不能存储账号,或尚未就绪');
|
|
105
|
+
if (iam.initialized())
|
|
106
|
+
throw new IamError('conflict', '已初始化:请改用 zhin console user 管理用户');
|
|
107
|
+
return { user: await iam.createOwner(input) };
|
|
108
|
+
}
|
|
109
|
+
#assertSetupOpen() {
|
|
110
|
+
if (!this.setupRequired())
|
|
111
|
+
throw new IamError('conflict', '已初始化:数据库已在使用中');
|
|
112
|
+
}
|
|
113
|
+
async #environment() {
|
|
114
|
+
return this.#options.environment ? this.#options.environment() : process.env;
|
|
115
|
+
}
|
|
116
|
+
async #prepare(input) {
|
|
117
|
+
const definition = SETUP_DEFINITIONS.find(candidate => candidate.dialect === input.dialect);
|
|
118
|
+
if (!definition)
|
|
119
|
+
throw new IamError('invalid_input', '不支持的数据库:可选 sqlite / mysql / pg / mongodb');
|
|
120
|
+
const connection = input.connection === undefined ? {} : input.connection;
|
|
121
|
+
if (!connection || typeof connection !== 'object' || Array.isArray(connection))
|
|
122
|
+
throw new IamError('invalid_input', 'connection 必须是对象');
|
|
123
|
+
const fields = fieldsOf(definition);
|
|
124
|
+
const raw = {};
|
|
125
|
+
for (const field of fields) {
|
|
126
|
+
let value = connection[field.name];
|
|
127
|
+
if (value === undefined || value === null || value === '')
|
|
128
|
+
value = field.default;
|
|
129
|
+
if (value === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
if (field.type === 'integer') {
|
|
132
|
+
const number = typeof value === 'number' ? value : Number(String(value).trim());
|
|
133
|
+
if (!Number.isSafeInteger(number))
|
|
134
|
+
throw new IamError('invalid_input', `${field.name} 必须是整数`);
|
|
135
|
+
value = number;
|
|
136
|
+
}
|
|
137
|
+
else if (typeof value !== 'string') {
|
|
138
|
+
throw new IamError('invalid_input', `${field.name} 必须是字符串`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
value = field.secret ? value : value.trim();
|
|
142
|
+
if (typeof value === 'string' && /[\r\n]/u.test(value))
|
|
143
|
+
throw new IamError('invalid_input', `${field.name} 不能包含换行`);
|
|
144
|
+
}
|
|
145
|
+
raw[field.name] = value;
|
|
146
|
+
}
|
|
147
|
+
const env = await this.#environment();
|
|
148
|
+
const expanded = expandEnvironmentValue({ dialect: definition.dialect, ...raw }, key => env[key]);
|
|
149
|
+
const issues = validateDatabaseConfig(expanded, env);
|
|
150
|
+
if (issues.length)
|
|
151
|
+
throw new IamError('invalid_input', issues.map(issue => issue.message).join('; ').slice(0, 500));
|
|
152
|
+
return { dialect: definition.dialect, fields, raw, expanded };
|
|
153
|
+
}
|
|
154
|
+
#probe(prepared) {
|
|
155
|
+
const { dialect: _dialect, ...config } = prepared.expanded;
|
|
156
|
+
if (prepared.dialect === 'sqlite' && typeof config.filename === 'string' && !isAbsolute(config.filename)) {
|
|
157
|
+
config.filename = resolve(this.#options.projectRoot, config.filename);
|
|
158
|
+
}
|
|
159
|
+
return (this.#options.probe ?? probeDatabaseConnection)(prepared.dialect, config);
|
|
160
|
+
}
|
|
161
|
+
async #currentDatabase() {
|
|
162
|
+
const document = await this.#options.config.readDocument();
|
|
163
|
+
const configured = document.database;
|
|
164
|
+
if (!configured || typeof configured !== 'object' || Array.isArray(configured)) {
|
|
165
|
+
return Object.freeze({ dialect: 'sqlite', source: 'default', summary: Object.freeze({ filename: DEFAULT_SQLITE_FILE }) });
|
|
166
|
+
}
|
|
167
|
+
const value = configured;
|
|
168
|
+
const dialect = typeof value.dialect === 'string' ? value.dialect : 'sqlite';
|
|
169
|
+
const summary = {};
|
|
170
|
+
for (const key of ['filename', 'host', 'port', 'user', 'database', 'dbName']) {
|
|
171
|
+
const item = value[key];
|
|
172
|
+
if (typeof item === 'string' || typeof item === 'number')
|
|
173
|
+
summary[key] = item;
|
|
174
|
+
}
|
|
175
|
+
if (typeof value.url === 'string')
|
|
176
|
+
summary.url = value.url.replace(/\/\/[^@/]*@/u, '//***@');
|
|
177
|
+
return Object.freeze({ dialect, source: 'config', summary: Object.freeze(summary) });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Sets `KEY=value` lines in dotenv text, keeping every other line. */
|
|
181
|
+
export function upsertDotenv(source, updates) {
|
|
182
|
+
const lines = source.length ? source.split(/\r?\n/u) : [];
|
|
183
|
+
if (lines.length && lines[lines.length - 1] === '')
|
|
184
|
+
lines.pop();
|
|
185
|
+
const pending = new Map(Object.entries(updates));
|
|
186
|
+
const next = lines.map(line => {
|
|
187
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/u.exec(line);
|
|
188
|
+
if (!match || !pending.has(match[1]))
|
|
189
|
+
return line;
|
|
190
|
+
const value = pending.get(match[1]);
|
|
191
|
+
pending.delete(match[1]);
|
|
192
|
+
return `${match[1]}=${quoteDotenv(value)}`;
|
|
193
|
+
});
|
|
194
|
+
for (const [key, value] of pending)
|
|
195
|
+
next.push(`${key}=${quoteDotenv(value)}`);
|
|
196
|
+
return `${next.join('\n')}\n`;
|
|
197
|
+
}
|
|
198
|
+
function quoteDotenv(value) {
|
|
199
|
+
if (/[\r\n]/u.test(value))
|
|
200
|
+
throw new IamError('invalid_input', '机密值不能包含换行');
|
|
201
|
+
if (!value.includes("'"))
|
|
202
|
+
return `'${value}'`;
|
|
203
|
+
if (!value.includes('"') && !value.includes('\\'))
|
|
204
|
+
return `"${value}"`;
|
|
205
|
+
throw new IamError('invalid_input', '机密值不能同时包含单引号与双引号;请改用 --database-password-env 引用已有变量');
|
|
206
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Console access-control tables (ADR 0049 §2). Defined by the composition root before DB start. */
|
|
2
|
+
export declare const IAM_TABLES: Readonly<{
|
|
3
|
+
readonly users: "console_users";
|
|
4
|
+
readonly roles: "console_roles";
|
|
5
|
+
readonly bindings: "console_role_bindings";
|
|
6
|
+
readonly sessions: "console_sessions";
|
|
7
|
+
readonly tokens: "console_api_tokens";
|
|
8
|
+
readonly audit: "console_audit_log";
|
|
9
|
+
}>;
|
|
10
|
+
export declare const IAM_TABLE_DEFINITIONS: Readonly<Record<string, Record<string, unknown>>>;
|
|
11
|
+
/** Dialects that can hold accounts (redis / memory cannot, ADR 0049 §8). */
|
|
12
|
+
export declare const IAM_DIALECTS: ReadonlySet<string>;
|
|
13
|
+
export interface IamTableHost {
|
|
14
|
+
readonly dialect: string;
|
|
15
|
+
readonly started: boolean;
|
|
16
|
+
define(name: string, definition: Record<string, unknown>): void;
|
|
17
|
+
tables(): readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/** Idempotent; must run before the database starts. Returns false for unsupported dialects. */
|
|
20
|
+
export declare function defineConsoleIamTables(host: IamTableHost): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Console access-control tables (ADR 0049 §2). Defined by the composition root before DB start. */
|
|
2
|
+
export const IAM_TABLES = Object.freeze({
|
|
3
|
+
users: 'console_users',
|
|
4
|
+
roles: 'console_roles',
|
|
5
|
+
bindings: 'console_role_bindings',
|
|
6
|
+
sessions: 'console_sessions',
|
|
7
|
+
tokens: 'console_api_tokens',
|
|
8
|
+
audit: 'console_audit_log',
|
|
9
|
+
});
|
|
10
|
+
const id = { type: 'text', length: 64, primary: true, nullable: false };
|
|
11
|
+
const text = (nullable = false) => ({ type: 'text', nullable });
|
|
12
|
+
const time = (nullable = false) => ({ type: 'bigint', nullable });
|
|
13
|
+
export const IAM_TABLE_DEFINITIONS = Object.freeze({
|
|
14
|
+
[IAM_TABLES.users]: {
|
|
15
|
+
id,
|
|
16
|
+
username: { type: 'text', length: 64, unique: true, nullable: false },
|
|
17
|
+
display_name: text(),
|
|
18
|
+
password_hash: text(),
|
|
19
|
+
disabled: { type: 'integer', nullable: false, default: 0 },
|
|
20
|
+
password_changed_at: time(),
|
|
21
|
+
created_at: time(),
|
|
22
|
+
updated_at: time(),
|
|
23
|
+
},
|
|
24
|
+
[IAM_TABLES.roles]: {
|
|
25
|
+
id,
|
|
26
|
+
name: { type: 'text', length: 64, unique: true, nullable: false },
|
|
27
|
+
description: text(),
|
|
28
|
+
builtin: { type: 'integer', nullable: false, default: 0 },
|
|
29
|
+
permissions: text(),
|
|
30
|
+
},
|
|
31
|
+
[IAM_TABLES.bindings]: {
|
|
32
|
+
id,
|
|
33
|
+
user_id: { type: 'text', length: 64, nullable: false },
|
|
34
|
+
role_id: { type: 'text', length: 64, nullable: false },
|
|
35
|
+
scope_kind: { type: 'text', length: 16, nullable: false },
|
|
36
|
+
scope_key: text(true),
|
|
37
|
+
},
|
|
38
|
+
[IAM_TABLES.sessions]: {
|
|
39
|
+
id,
|
|
40
|
+
user_id: { type: 'text', length: 64, nullable: false },
|
|
41
|
+
created_at: time(),
|
|
42
|
+
expires_at: time(),
|
|
43
|
+
last_seen_at: time(),
|
|
44
|
+
ip: text(true),
|
|
45
|
+
user_agent: text(true),
|
|
46
|
+
},
|
|
47
|
+
[IAM_TABLES.tokens]: {
|
|
48
|
+
id,
|
|
49
|
+
user_id: { type: 'text', length: 64, nullable: false },
|
|
50
|
+
name: text(),
|
|
51
|
+
token_hash: { type: 'text', length: 64, unique: true, nullable: false },
|
|
52
|
+
permissions: text(true),
|
|
53
|
+
expires_at: time(true),
|
|
54
|
+
last_used_at: time(true),
|
|
55
|
+
created_at: time(),
|
|
56
|
+
},
|
|
57
|
+
[IAM_TABLES.audit]: {
|
|
58
|
+
id,
|
|
59
|
+
at: time(),
|
|
60
|
+
actor: text(),
|
|
61
|
+
action: text(),
|
|
62
|
+
target: text(true),
|
|
63
|
+
outcome: { type: 'text', length: 16, nullable: false },
|
|
64
|
+
detail: text(true),
|
|
65
|
+
ip: text(true),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
/** Dialects that can hold accounts (redis / memory cannot, ADR 0049 §8). */
|
|
69
|
+
export const IAM_DIALECTS = new Set(['sqlite', 'mysql', 'pg', 'mongodb']);
|
|
70
|
+
/** Idempotent; must run before the database starts. Returns false for unsupported dialects. */
|
|
71
|
+
export function defineConsoleIamTables(host) {
|
|
72
|
+
if (!IAM_DIALECTS.has(host.dialect))
|
|
73
|
+
return false;
|
|
74
|
+
if (host.started)
|
|
75
|
+
return true;
|
|
76
|
+
const existing = new Set(host.tables());
|
|
77
|
+
for (const [name, definition] of Object.entries(IAM_TABLE_DEFINITIONS)) {
|
|
78
|
+
if (!existing.has(name))
|
|
79
|
+
host.define(name, definition);
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
package/lib/iam.d.ts
ADDED
package/lib/iam.js
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zhin.js management gateway (ADR 0054). It must not import `zhin.js` or any IM layer.
|
|
3
|
+
*
|
|
4
|
+
* Until the gateway process exists (B1.1–B1.4) this package is a library: the Zhin instance
|
|
5
|
+
* (`zhin.js/instance`) still installs Console access control and the terminal into its in-process
|
|
6
|
+
* HTTP host, and both the instance and `@zhin.js/cli` share the project-file services below.
|
|
7
|
+
* Import the narrow subpaths (`@zhin.js/gateway/<area>`) to avoid loading unrelated areas.
|
|
8
|
+
* @module @zhin.js/gateway
|
|
9
|
+
*/
|
|
10
|
+
export * from './iam.js';
|
|
11
|
+
export * from './terminal.js';
|
|
12
|
+
export * from './config.js';
|
|
13
|
+
export * from './plugin-management.js';
|
|
14
|
+
export * from './project.js';
|
|
15
|
+
export * from './supervisor.js';
|
|
16
|
+
export * from './usage.js';
|
|
17
|
+
export * from './onboarding.js';
|
|
18
|
+
export * from './http.js';
|
|
19
|
+
export * from './packages.js';
|
|
20
|
+
export * from './database.js';
|
|
21
|
+
export * from './local.js';
|
|
22
|
+
export * from './server.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zhin.js management gateway (ADR 0054). It must not import `zhin.js` or any IM layer.
|
|
3
|
+
*
|
|
4
|
+
* Until the gateway process exists (B1.1–B1.4) this package is a library: the Zhin instance
|
|
5
|
+
* (`zhin.js/instance`) still installs Console access control and the terminal into its in-process
|
|
6
|
+
* HTTP host, and both the instance and `@zhin.js/cli` share the project-file services below.
|
|
7
|
+
* Import the narrow subpaths (`@zhin.js/gateway/<area>`) to avoid loading unrelated areas.
|
|
8
|
+
* @module @zhin.js/gateway
|
|
9
|
+
*/
|
|
10
|
+
export * from './iam.js';
|
|
11
|
+
export * from './terminal.js';
|
|
12
|
+
export * from './config.js';
|
|
13
|
+
export * from './plugin-management.js';
|
|
14
|
+
export * from './project.js';
|
|
15
|
+
export * from './supervisor.js';
|
|
16
|
+
export * from './usage.js';
|
|
17
|
+
export * from './onboarding.js';
|
|
18
|
+
export * from './http.js';
|
|
19
|
+
export * from './packages.js';
|
|
20
|
+
export * from './database.js';
|
|
21
|
+
export * from './local.js';
|
|
22
|
+
export * from './server.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { GatewayPackageEvent, GatewayRpcErrorDetails } from '@zhin.js/console-protocol';
|
|
2
|
+
import { type LocalGatewayErrorCode, type LocalGatewayInfoResult } from './protocol.js';
|
|
3
|
+
/**
|
|
4
|
+
* - `not_running`: no socket, or a stale one nobody listens on;
|
|
5
|
+
* - `not_owner`: the socket belongs to (or is only accessible by) another user;
|
|
6
|
+
* - `unreachable`: a socket exists but the gateway does not answer;
|
|
7
|
+
* - `protocol`: the peer sent something that is not the gateway protocol.
|
|
8
|
+
*/
|
|
9
|
+
export type LocalGatewayClientErrorCode = 'not_running' | 'not_owner' | 'unreachable' | 'protocol';
|
|
10
|
+
export declare class LocalGatewayClientError extends Error {
|
|
11
|
+
readonly code: LocalGatewayClientErrorCode;
|
|
12
|
+
constructor(code: LocalGatewayClientErrorCode, message: string, options?: ErrorOptions);
|
|
13
|
+
}
|
|
14
|
+
/** The gateway answered `{ ok: false }`. */
|
|
15
|
+
export declare class LocalGatewayRpcError extends Error {
|
|
16
|
+
readonly code: LocalGatewayErrorCode;
|
|
17
|
+
readonly status: number;
|
|
18
|
+
readonly details: GatewayRpcErrorDetails;
|
|
19
|
+
constructor(code: LocalGatewayErrorCode, message: string, status: number, details?: GatewayRpcErrorDetails);
|
|
20
|
+
}
|
|
21
|
+
export interface LocalGatewayClientOptions {
|
|
22
|
+
readonly root: string;
|
|
23
|
+
/** Overrides the endpoint (default: `gateway.json#socket`, else {@link gatewaySocketPath}). */
|
|
24
|
+
readonly path?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Handshake key the endpoint must prove (default: `gateway.json#handshakeKey` when the endpoint
|
|
27
|
+
* is the one recorded there). An endpoint that cannot prove it is refused (`not_owner`).
|
|
28
|
+
*/
|
|
29
|
+
readonly handshakeKey?: string;
|
|
30
|
+
/** Current user id; `undefined` skips the owner check. Default `process.getuid()`. */
|
|
31
|
+
readonly uid?: () => number | undefined;
|
|
32
|
+
readonly connectTimeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface LocalRequestOptions {
|
|
35
|
+
readonly onEvent?: (event: GatewayPackageEvent) => void;
|
|
36
|
+
/** Default: none (package transactions can take minutes). */
|
|
37
|
+
readonly timeoutMs?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class LocalGatewayClient {
|
|
40
|
+
#private;
|
|
41
|
+
readonly path: string;
|
|
42
|
+
private constructor();
|
|
43
|
+
/** Connects to the project's gateway; see {@link LocalGatewayClientErrorCode} for failures. */
|
|
44
|
+
static connect(options: LocalGatewayClientOptions): Promise<LocalGatewayClient>;
|
|
45
|
+
request<T = unknown>(type: string, data?: unknown, options?: LocalRequestOptions): Promise<T>;
|
|
46
|
+
info(timeoutMs?: number): Promise<LocalGatewayInfoResult>;
|
|
47
|
+
close(): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The running gateway's info, or `undefined` when no gateway runs for `root`. Ownership and
|
|
51
|
+
* reachability problems are thrown (a caller must not fall back to working on the files then).
|
|
52
|
+
*/
|
|
53
|
+
export declare function probeLocalGateway(options: LocalGatewayClientOptions): Promise<LocalGatewayInfoResult | undefined>;
|
|
54
|
+
/** The subset of `fetch` the CLI's Host clients use. */
|
|
55
|
+
export type LocalGatewayFetch = (input: string | URL, init?: Readonly<{
|
|
56
|
+
method?: string;
|
|
57
|
+
body?: string | null;
|
|
58
|
+
signal?: AbortSignal | null;
|
|
59
|
+
headers?: unknown;
|
|
60
|
+
}>) => Promise<Response>;
|
|
61
|
+
/** The Bearer credential of `Authorization` in `headers` (a `Headers`, record or entry list). */
|
|
62
|
+
export declare function bearerFromHeaders(headers: unknown): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* A `fetch` that sends `{base}/…` requests over the local control socket
|
|
65
|
+
* (`gateway.http.request`) instead of HTTP (ADR 0054 §6). Only the path and query of the URL are
|
|
66
|
+
* used. Without an `Authorization: Bearer` header the gateway runs the request as the local
|
|
67
|
+
* owner, so no Console token is needed; with one (an explicit `ZHIN_CONSOLE_TOKEN`), the gateway
|
|
68
|
+
* authenticates that credential and runs — and audits — the request as its principal. A gateway
|
|
69
|
+
* that is not running rejects like a refused connection (`cause.code === 'ECONNREFUSED'`).
|
|
70
|
+
*/
|
|
71
|
+
export declare function createLocalGatewayFetch(options: LocalGatewayClientOptions): LocalGatewayFetch;
|