@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,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway process (ADR 0054 §1 / §5 / §6): `zhin start` / `zhin runtime start` / `pnpm dev`.
|
|
3
|
+
* Long-lived: the instance crashing, entering `crashed` after a restart storm, or being stopped
|
|
4
|
+
* from the Console / CLI does not end it. It ends on SIGINT / SIGTERM (both processes stop) or
|
|
5
|
+
* `gateway.shutdown` on the local control socket. Only `--once` (CI) still ends with the instance.
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { REMOTE_CONSOLE_ORIGIN } from '@zhin.js/host-http';
|
|
12
|
+
import { GATEWAY_SOCKET_ENV } from '@zhin.js/host-http/bridge';
|
|
13
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
14
|
+
import { instanceStatus } from '../packages/rpc.js';
|
|
15
|
+
import { createLocalRpcHandler } from '../local/handler.js';
|
|
16
|
+
import { LocalControlServer, LocalSocketError, probeLocalSocket } from '../local/local-socket.js';
|
|
17
|
+
import { gatewayInfoFile, gatewaySocketPath, isNamedPipe } from '../local/paths.js';
|
|
18
|
+
import { readGatewayInfo, removeGatewayInfo, writeGatewayInfo } from '../local/state-file.js';
|
|
19
|
+
import { isNodeVersionSupported, NODE_ENGINES_HINT } from '../project/node-requirements.js';
|
|
20
|
+
import { InstanceSupervisor } from '../supervisor/instance-supervisor.js';
|
|
21
|
+
import { defaultInstanceLogFile, supervisedNodeArguments } from '../supervisor/run-instance.js';
|
|
22
|
+
import { captureTerminalState, isForegroundProcessGroup } from '../supervisor/terminal-state.js';
|
|
23
|
+
import { startGateway } from './start-gateway.js';
|
|
24
|
+
/** Set on the instance when its output is echoed to an interactive terminal (TTY-style logging). */
|
|
25
|
+
export const INSTANCE_INTERACTIVE_ENV = 'ZHIN_INSTANCE_INTERACTIVE';
|
|
26
|
+
let cachedVersion;
|
|
27
|
+
/** `@zhin.js/gateway` version. */
|
|
28
|
+
export function gatewayVersion() {
|
|
29
|
+
if (cachedVersion)
|
|
30
|
+
return cachedVersion;
|
|
31
|
+
try {
|
|
32
|
+
const manifest = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
33
|
+
cachedVersion = typeof manifest.version === 'string' ? manifest.version : '0.0.0';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
cachedVersion = '0.0.0';
|
|
37
|
+
}
|
|
38
|
+
return cachedVersion;
|
|
39
|
+
}
|
|
40
|
+
/** Console link in the ADR 0053 format: `?apiBaseUrl=`, the token only after `#`. */
|
|
41
|
+
export function consoleLink(origin, token) {
|
|
42
|
+
const url = `${REMOTE_CONSOLE_ORIGIN}/?apiBaseUrl=${encodeURIComponent(origin)}`;
|
|
43
|
+
return token ? `${url}#token=${encodeURIComponent(token)}` : url;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether the instance gets the gateway's stdin: a foreground gateway whose stdin is a TTY and
|
|
47
|
+
* that is the terminal's foreground job (a background job's child reading the TTY gets SIGTTIN).
|
|
48
|
+
*/
|
|
49
|
+
export function foregroundStdin(daemon, stdin = process.stdin, foregroundGroup = isForegroundProcessGroup) {
|
|
50
|
+
return !daemon && stdin.isTTY === true && foregroundGroup();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The local control endpoint of this run. Windows named pipes get a fresh random name component
|
|
54
|
+
* (Node cannot set a DACL on them; see `windowsPipeName`), so a second gateway on the project is
|
|
55
|
+
* detected through the pipe the state file records instead of a name clash.
|
|
56
|
+
*/
|
|
57
|
+
async function localEndpoint(root, platform = process.platform) {
|
|
58
|
+
if (platform !== 'win32')
|
|
59
|
+
return gatewaySocketPath(root, { platform });
|
|
60
|
+
const recorded = await readGatewayInfo(root);
|
|
61
|
+
if (recorded && isNamedPipe(recorded.socket) && await probeLocalSocket(recorded.socket).catch(() => 'live') === 'live') {
|
|
62
|
+
throw new LocalSocketError('in_use', `另一个网关正在使用 ${recorded.socket}`);
|
|
63
|
+
}
|
|
64
|
+
return gatewaySocketPath(root, { platform, nonce: randomBytes(8).toString('hex') });
|
|
65
|
+
}
|
|
66
|
+
/** Strips the `zhin runtime start` / `zhin start` prefix of the instance arguments. */
|
|
67
|
+
function startArguments(args) {
|
|
68
|
+
if (args[0] === 'runtime' && args[1] === 'start')
|
|
69
|
+
return args.slice(2);
|
|
70
|
+
if (args[0] === 'start')
|
|
71
|
+
return args.slice(1);
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
/** Runs the gateway until it is shut down; resolves with the exit code for this process. */
|
|
75
|
+
export async function runGateway(input) {
|
|
76
|
+
if (!isNodeVersionSupported()) {
|
|
77
|
+
throw new Error(`zhin start requires Node ${NODE_ENGINES_HINT}; found ${process.versions.node}`);
|
|
78
|
+
}
|
|
79
|
+
const entry = input.entry ?? process.argv[1];
|
|
80
|
+
if (!entry)
|
|
81
|
+
throw new Error('Cannot determine the zhin executable path');
|
|
82
|
+
const { root, options } = input;
|
|
83
|
+
const logger = getLogger('gateway');
|
|
84
|
+
const args = [...(input.args ?? process.argv.slice(2))];
|
|
85
|
+
const daemon = options.daemon;
|
|
86
|
+
const echo = input.echo ?? !daemon;
|
|
87
|
+
const interactive = echo && process.stdout.isTTY === true;
|
|
88
|
+
const logFile = options.logFile ?? defaultInstanceLogFile(root);
|
|
89
|
+
await mkdir(dirname(logFile), { recursive: true });
|
|
90
|
+
const socketPath = input.localSocket === false ? undefined : input.localSocket?.path ?? await localEndpoint(root);
|
|
91
|
+
// Keyed proof the CLI checks before it talks to the endpoint (only in the 0600 state file).
|
|
92
|
+
const handshakeKey = randomBytes(32).toString('hex');
|
|
93
|
+
const shareTerminal = foregroundStdin(daemon);
|
|
94
|
+
// The instance may leave the shared terminal raw / without echo when it exits or crashes.
|
|
95
|
+
const terminal = shareTerminal ? captureTerminalState() : undefined;
|
|
96
|
+
// gateway.readyTimeout: startGateway re-reads it before every start, restart and transaction.
|
|
97
|
+
const supervisor = new InstanceSupervisor({
|
|
98
|
+
spawn: {
|
|
99
|
+
command: process.execPath,
|
|
100
|
+
args: [...supervisedNodeArguments(), entry, ...args],
|
|
101
|
+
cwd: root,
|
|
102
|
+
env: {
|
|
103
|
+
...process.env,
|
|
104
|
+
// The instance keeps the Agent's file / shell tools away from the owner endpoint.
|
|
105
|
+
...(socketPath ? { [GATEWAY_SOCKET_ENV]: socketPath } : {}),
|
|
106
|
+
...(interactive ? { [INSTANCE_INTERACTIVE_ENV]: '1', FORCE_COLOR: process.env.FORCE_COLOR ?? '1' } : {}),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
output: 'pipe',
|
|
110
|
+
// Foreground on a TTY: the instance reads the terminal (terminal adapters); never in the background.
|
|
111
|
+
input: shareTerminal ? 'inherit' : 'ignore',
|
|
112
|
+
logs: { ...input.logs, file: logFile },
|
|
113
|
+
// The gateway outlives the instance (ADR 0054 §5); `--once` is a single check run.
|
|
114
|
+
restart: { onCrash: !options.once, onRequest: !options.once, ...input.restart },
|
|
115
|
+
});
|
|
116
|
+
if (echo) {
|
|
117
|
+
supervisor.on('log', (line) => {
|
|
118
|
+
(line.stream === 'stderr' ? process.stderr : process.stdout).write(`${line.line}\n`);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
supervisor.on('state', (status) => {
|
|
122
|
+
if (terminal && (status.state === 'stopped' || status.state === 'crashed' || status.state === 'backoff'))
|
|
123
|
+
terminal.restore();
|
|
124
|
+
logger.info(formatCompact({ op: 'instance_state', state: status.state, ...(status.pid ? { pid: status.pid } : {}) }));
|
|
125
|
+
});
|
|
126
|
+
supervisor.on('failure', (failure) => {
|
|
127
|
+
logger.warn(formatCompact({ op: 'instance_failure', reason: failure.reason, message: failure.message }));
|
|
128
|
+
});
|
|
129
|
+
supervisor.on('restart-request', (request) => {
|
|
130
|
+
logger.info(formatCompact({ op: 'instance_restart_request', reason: request.reason }));
|
|
131
|
+
});
|
|
132
|
+
const control = socketPath
|
|
133
|
+
? await LocalControlServer.open({
|
|
134
|
+
path: socketPath,
|
|
135
|
+
handshakeKey,
|
|
136
|
+
// No other gateway serves the project: a state file left by a dead one is stale.
|
|
137
|
+
beforeListen: () => rm(gatewayInfoFile(root), { force: true }),
|
|
138
|
+
...(input.localSocket && input.localSocket.uid ? { uid: input.localSocket.uid } : {}),
|
|
139
|
+
}).catch((error) => { supervisor.dispose(); throw error; })
|
|
140
|
+
: undefined;
|
|
141
|
+
let gateway;
|
|
142
|
+
try {
|
|
143
|
+
gateway = await startGateway({
|
|
144
|
+
projectRoot: root,
|
|
145
|
+
environment: options.environment,
|
|
146
|
+
...(input.http ? { http: input.http } : {}),
|
|
147
|
+
supervisor,
|
|
148
|
+
transport: () => supervisor.transport,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
await control?.close();
|
|
153
|
+
supervisor.dispose();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
let resolveEnd;
|
|
157
|
+
const ended = new Promise((resolve) => { resolveEnd = resolve; });
|
|
158
|
+
let ending = false;
|
|
159
|
+
const shutdown = (exitCode = 0) => {
|
|
160
|
+
if (ending)
|
|
161
|
+
return;
|
|
162
|
+
ending = true;
|
|
163
|
+
logger.info(formatCompact({ op: 'gateway_stop' }));
|
|
164
|
+
resolveEnd(exitCode);
|
|
165
|
+
};
|
|
166
|
+
const addresses = gateway.http.addresses;
|
|
167
|
+
const info = Object.freeze({
|
|
168
|
+
pid: process.pid,
|
|
169
|
+
ports: Object.freeze(addresses.map((address) => address.port)),
|
|
170
|
+
origins: Object.freeze(addresses.map((address) => address.origin)),
|
|
171
|
+
socket: socketPath ?? '',
|
|
172
|
+
startedAt: Date.now(),
|
|
173
|
+
version: gatewayVersion(),
|
|
174
|
+
mode: daemon ? 'daemon' : 'foreground',
|
|
175
|
+
args: Object.freeze([...startArguments(args)]),
|
|
176
|
+
logFile,
|
|
177
|
+
});
|
|
178
|
+
const infoResult = () => Object.freeze({
|
|
179
|
+
gateway: info,
|
|
180
|
+
instance: instanceStatus(supervisor.status()),
|
|
181
|
+
consoleInitialized: gateway.http.tokenRegistry.initialized(),
|
|
182
|
+
});
|
|
183
|
+
control?.setHandler(createLocalRpcHandler({
|
|
184
|
+
dispatch: gateway.rpc,
|
|
185
|
+
...(gateway.packages ? { packages: gateway.packages } : {}),
|
|
186
|
+
...(gateway.console ? { console: gateway.console.localAdmin } : {}),
|
|
187
|
+
info: infoResult,
|
|
188
|
+
shutdown: () => shutdown(0),
|
|
189
|
+
http: (request) => gateway.http.localRequest(request),
|
|
190
|
+
}));
|
|
191
|
+
const pidFile = join(root, '.zhin.pid');
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(pidFile, String(process.pid));
|
|
194
|
+
}
|
|
195
|
+
catch { /* informational */ }
|
|
196
|
+
if (socketPath) {
|
|
197
|
+
await writeGatewayInfo(root, { ...info, handshakeKey }).catch((error) => {
|
|
198
|
+
logger.warn(formatCompact({ op: 'gateway_info_write_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const origin = info.origins[0];
|
|
202
|
+
logger.info(formatCompact({
|
|
203
|
+
op: 'gateway_listening', pid: process.pid, ...(origin ? { http: origin } : {}),
|
|
204
|
+
...(socketPath ? { socket: socketPath } : {}), log: logFile,
|
|
205
|
+
}));
|
|
206
|
+
if (origin && !options.once)
|
|
207
|
+
logger.info(`Console: ${consoleLink(origin)}`);
|
|
208
|
+
let interrupted = 0;
|
|
209
|
+
const onSignal = (signal) => {
|
|
210
|
+
interrupted += 1;
|
|
211
|
+
// A second signal kills the instance without waiting for its graceful stop.
|
|
212
|
+
if (interrupted > 1)
|
|
213
|
+
void supervisor.stop({ reason: 'signal', force: true });
|
|
214
|
+
shutdown(signal === 'SIGINT' ? 130 : 0);
|
|
215
|
+
};
|
|
216
|
+
const onSighup = () => { if (!daemon)
|
|
217
|
+
onSignal('SIGHUP'); };
|
|
218
|
+
const onExit = () => {
|
|
219
|
+
supervisor.dispose();
|
|
220
|
+
terminal?.restore();
|
|
221
|
+
removePidFile();
|
|
222
|
+
};
|
|
223
|
+
const removePidFile = () => {
|
|
224
|
+
try {
|
|
225
|
+
if (readFileSync(pidFile, 'utf8').trim() === String(process.pid))
|
|
226
|
+
rmSync(pidFile, { force: true });
|
|
227
|
+
}
|
|
228
|
+
catch { /* already gone */ }
|
|
229
|
+
};
|
|
230
|
+
const signals = input.signals !== false;
|
|
231
|
+
if (signals) {
|
|
232
|
+
process.on('SIGINT', onSignal);
|
|
233
|
+
process.on('SIGTERM', onSignal);
|
|
234
|
+
process.on('SIGHUP', onSighup);
|
|
235
|
+
}
|
|
236
|
+
process.on('exit', onExit);
|
|
237
|
+
// `--once`: the run ends with the instance's single generation (CI check).
|
|
238
|
+
if (options.once) {
|
|
239
|
+
const check = (status = supervisor.status()) => {
|
|
240
|
+
if ((status.state === 'stopped' || status.state === 'crashed') && !gateway.ownsInstanceLifecycle()) {
|
|
241
|
+
shutdown(status.state === 'crashed' ? status.lastExitCode || 1 : status.lastExitCode ?? 0);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
void gateway.instanceStarted.then(() => {
|
|
245
|
+
supervisor.on('state', check);
|
|
246
|
+
check();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
input.onReady?.({ gateway, info, shutdown });
|
|
250
|
+
try {
|
|
251
|
+
return await ended;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await gateway.close().catch((error) => {
|
|
255
|
+
logger.warn(formatCompact({ op: 'gateway_close_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
256
|
+
});
|
|
257
|
+
await control?.close().catch(() => undefined);
|
|
258
|
+
if (socketPath)
|
|
259
|
+
await removeGatewayInfo(root).catch(() => undefined);
|
|
260
|
+
if (signals) {
|
|
261
|
+
process.off('SIGINT', onSignal);
|
|
262
|
+
process.off('SIGTERM', onSignal);
|
|
263
|
+
process.off('SIGHUP', onSighup);
|
|
264
|
+
}
|
|
265
|
+
process.off('exit', onExit);
|
|
266
|
+
removePidFile();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway composition (ADR 0054 §1): the HTTP server and the instance supervisor, joined by a
|
|
3
|
+
* pluggable instance transport (the bridge supplies the IPC one). CLI wiring lives elsewhere.
|
|
4
|
+
*/
|
|
5
|
+
import { type ResolvedHttpConfig, type TokenRegistry } from '@zhin.js/host-http';
|
|
6
|
+
import type { InstanceTransport } from '@zhin.js/host-http/bridge';
|
|
7
|
+
import { InstanceSupervisor, type InstanceSupervisorOptions } from '../supervisor/instance-supervisor.js';
|
|
8
|
+
import { GatewayPackageService } from '../packages/service.js';
|
|
9
|
+
import { type GatewayRpcDispatcher } from '../packages/rpc.js';
|
|
10
|
+
import { type GatewayConsole, type GatewayConsoleInstancePort, type GatewayConsoleOptions } from './gateway-console.js';
|
|
11
|
+
import { GatewayHttpServer } from './gateway-http-server.js';
|
|
12
|
+
/**
|
|
13
|
+
* A transport the gateway owns; `dispose` runs on gateway close. The Console module also uses the
|
|
14
|
+
* optional {@link GatewayConsoleInstancePort} methods (plugin catalog, instance audit,
|
|
15
|
+
* `principal-revoked`) when the transport has them (`IpcInstanceTransport` does).
|
|
16
|
+
*/
|
|
17
|
+
export interface GatewayInstanceTransport extends InstanceTransport, GatewayConsoleInstancePort {
|
|
18
|
+
dispose?(): void | Promise<void>;
|
|
19
|
+
/** Told the bound listener addresses after listen (the instance's `HttpHost.address`). */
|
|
20
|
+
setAddresses?(addresses: readonly import('@zhin.js/host-http').HttpHostAddress[]): void;
|
|
21
|
+
}
|
|
22
|
+
export interface InstanceTransportFactoryContext {
|
|
23
|
+
readonly supervisor: InstanceSupervisor;
|
|
24
|
+
readonly projectRoot: string;
|
|
25
|
+
}
|
|
26
|
+
export type InstanceTransportFactory = (context: InstanceTransportFactoryContext) => GatewayInstanceTransport;
|
|
27
|
+
export interface StartGatewayOptions {
|
|
28
|
+
readonly projectRoot: string;
|
|
29
|
+
/** Selects `.env.<environment>` for `http.*` placeholders. Default `development`. */
|
|
30
|
+
readonly environment?: string;
|
|
31
|
+
/** Overrides the `http` block of the project config. */
|
|
32
|
+
readonly http?: ResolvedHttpConfig;
|
|
33
|
+
/** Instance supervision; pass a supervisor to own its construction. */
|
|
34
|
+
readonly supervisor: InstanceSupervisorOptions | InstanceSupervisor;
|
|
35
|
+
readonly transport: InstanceTransportFactory;
|
|
36
|
+
/** Console authentication registry (the Console module installs its access provider on it). */
|
|
37
|
+
readonly tokenRegistry?: TokenRegistry;
|
|
38
|
+
/**
|
|
39
|
+
* Console access control in the gateway (project database, `/auth/*`, `iam.*`, setup,
|
|
40
|
+
* onboarding, terminal, audit). Default on; `false` leaves only the bootstrap token (tests).
|
|
41
|
+
*/
|
|
42
|
+
readonly console?: false | Omit<GatewayConsoleOptions, 'server' | 'projectRoot' | 'environment' | 'instance' | 'restartInstance'>;
|
|
43
|
+
/** Longest wait for the project database before listening anyway (default 10 s). */
|
|
44
|
+
readonly databaseWaitMs?: number;
|
|
45
|
+
readonly retryAfterSeconds?: number;
|
|
46
|
+
readonly maxRequestBodyBytes?: number;
|
|
47
|
+
/** Start the instance after the server listens. Default true; a failed start keeps the gateway up. */
|
|
48
|
+
readonly startInstance?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Dependency transactions (`gateway.packages.*`). Default: a {@link GatewayPackageService} on
|
|
51
|
+
* the project root over the supervisor; `false` disables the package RPC.
|
|
52
|
+
*/
|
|
53
|
+
readonly packages?: GatewayPackageService | false;
|
|
54
|
+
/**
|
|
55
|
+
* `gateway.readyTimeout` for dependency transactions when the project sets none (see
|
|
56
|
+
* {@link loadGatewaySettings}). Default 60 s.
|
|
57
|
+
*/
|
|
58
|
+
readonly readyTimeoutMs?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Reads the project's `gateway` block. Called before every instance start / restart (Console,
|
|
61
|
+
* CLI, automatic restart after a crash) and every dependency transaction, so a changed
|
|
62
|
+
* `gateway.readyTimeout` applies without restarting the gateway. Default
|
|
63
|
+
* {@link loadGatewaySettings} on the project root; `false` reads nothing.
|
|
64
|
+
*/
|
|
65
|
+
readonly settings?: false | (() => Promise<GatewaySettings>);
|
|
66
|
+
}
|
|
67
|
+
export interface RunningGateway {
|
|
68
|
+
readonly http: GatewayHttpServer;
|
|
69
|
+
readonly supervisor: InstanceSupervisor;
|
|
70
|
+
readonly transport: GatewayInstanceTransport;
|
|
71
|
+
readonly packages: GatewayPackageService | undefined;
|
|
72
|
+
/** Console access control; undefined with `console: false`. */
|
|
73
|
+
readonly console: GatewayConsole | undefined;
|
|
74
|
+
/** The gateway RPC (`gateway.packages.*`, `gateway.instance.*`), shared with the local socket. */
|
|
75
|
+
readonly rpc: GatewayRpcDispatcher;
|
|
76
|
+
/**
|
|
77
|
+
* True while the gateway owns the instance's lifecycle: a dependency transaction runs, or the
|
|
78
|
+
* Console stopped the instance (a run must not end because of it).
|
|
79
|
+
*/
|
|
80
|
+
ownsInstanceLifecycle(): boolean;
|
|
81
|
+
/** Resolves when the first instance start attempt settled (never rejects). */
|
|
82
|
+
readonly instanceStarted: Promise<void>;
|
|
83
|
+
/** Stops the instance, then closes the listeners. */
|
|
84
|
+
close(): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reads and resolves the project's `http` config the same way the instance used to. Only the
|
|
88
|
+
* `http` block is read: plugin declarations are not validated here, so a config the instance
|
|
89
|
+
* rejects still leaves the Console reachable.
|
|
90
|
+
*/
|
|
91
|
+
export declare function loadGatewayHttpConfig(projectRoot: string, environment?: string): Promise<ResolvedHttpConfig>;
|
|
92
|
+
/** The project's `gateway` block (ADR 0054 §4 / §5). */
|
|
93
|
+
export interface GatewaySettings {
|
|
94
|
+
/** `gateway.readyTimeout` (seconds) in ms; undefined: the built-in 60 s. */
|
|
95
|
+
readonly readyTimeoutMs?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reads the `gateway` block. An invalid value is logged and ignored (the instance rejects the
|
|
99
|
+
* config on its own); like {@link loadGatewayHttpConfig}, it never keeps the gateway down.
|
|
100
|
+
*/
|
|
101
|
+
export declare function loadGatewaySettings(projectRoot: string): Promise<GatewaySettings>;
|
|
102
|
+
export declare function startGateway(options: StartGatewayOptions): Promise<RunningGateway>;
|
|
103
|
+
/** Thrown when the gateway would serve an open Host on a network address. */
|
|
104
|
+
export declare class OpenHostOnNetworkError extends Error {
|
|
105
|
+
readonly host: string;
|
|
106
|
+
readonly code = "ZHIN_OPEN_HOST_ON_NETWORK";
|
|
107
|
+
constructor(host: string);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* An open Host (no bootstrap token, no owner yet) holds every permission and is only meant for
|
|
111
|
+
* this machine. The per-request loopback check cannot see through a same-host reverse proxy that
|
|
112
|
+
* adds no forwarding header, so a network-facing `http.host` must not start open (ADR 0049 §1).
|
|
113
|
+
*/
|
|
114
|
+
export declare function assertNotOpenOnNetwork(http: ResolvedHttpConfig, registry: TokenRegistry): void;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway composition (ADR 0054 §1): the HTTP server and the instance supervisor, joined by a
|
|
3
|
+
* pluggable instance transport (the bridge supplies the IPC one). CLI wiring lives elsewhere.
|
|
4
|
+
*/
|
|
5
|
+
import { isLoopbackHost, resolveHttpConfig } from '@zhin.js/host-http';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
|
+
import { createConfigDocument } from '@zhin.js/runtime';
|
|
9
|
+
import { findConfigFile } from '../project/config-file.js';
|
|
10
|
+
import { loadRuntimeEnvironmentLayers } from '../project/environment.js';
|
|
11
|
+
import { InstanceSupervisor } from '../supervisor/instance-supervisor.js';
|
|
12
|
+
import { GatewayPackageService } from '../packages/service.js';
|
|
13
|
+
import { createGatewayRpcDispatcher } from '../packages/rpc.js';
|
|
14
|
+
import { createGatewayConsoleRpc } from './console-rpc.js';
|
|
15
|
+
import { startGatewayConsole } from './gateway-console.js';
|
|
16
|
+
import { registerGatewayEventRoutes } from './gateway-events.js';
|
|
17
|
+
import { GatewayHttpServer } from './gateway-http-server.js';
|
|
18
|
+
const logger = getLogger('gateway');
|
|
19
|
+
/**
|
|
20
|
+
* Reads and resolves the project's `http` config the same way the instance used to. Only the
|
|
21
|
+
* `http` block is read: plugin declarations are not validated here, so a config the instance
|
|
22
|
+
* rejects still leaves the Console reachable.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadGatewayHttpConfig(projectRoot, environment = 'development') {
|
|
25
|
+
const file = findConfigFile(projectRoot);
|
|
26
|
+
if (!file)
|
|
27
|
+
return {};
|
|
28
|
+
const [document, layers] = await Promise.all([
|
|
29
|
+
createConfigDocument(join(projectRoot, file)).read().then((snapshot) => snapshot.document),
|
|
30
|
+
loadRuntimeEnvironmentLayers(projectRoot, environment),
|
|
31
|
+
]);
|
|
32
|
+
return resolveHttpConfig(document, layers.environments?.[environment], projectRoot);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reads the `gateway` block. An invalid value is logged and ignored (the instance rejects the
|
|
36
|
+
* config on its own); like {@link loadGatewayHttpConfig}, it never keeps the gateway down.
|
|
37
|
+
*/
|
|
38
|
+
export async function loadGatewaySettings(projectRoot) {
|
|
39
|
+
const file = findConfigFile(projectRoot);
|
|
40
|
+
if (!file)
|
|
41
|
+
return {};
|
|
42
|
+
let block;
|
|
43
|
+
try {
|
|
44
|
+
block = (await createConfigDocument(join(projectRoot, file)).read()).document.gateway;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
if (block === undefined || block === null)
|
|
50
|
+
return {};
|
|
51
|
+
const readyTimeout = typeof block === 'object' && !Array.isArray(block) ? block.readyTimeout : undefined;
|
|
52
|
+
if (readyTimeout === undefined)
|
|
53
|
+
return {};
|
|
54
|
+
if (typeof readyTimeout === 'number' && Number.isInteger(readyTimeout) && readyTimeout >= 1 && readyTimeout <= 3600) {
|
|
55
|
+
return Object.freeze({ readyTimeoutMs: readyTimeout * 1000 });
|
|
56
|
+
}
|
|
57
|
+
logger.warn(formatCompact({ op: 'gateway_config_invalid', key: 'gateway.readyTimeout', hint: 'integer seconds 1-3600; using 60' }));
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
export async function startGateway(options) {
|
|
61
|
+
const http = options.http ?? await loadGatewayHttpConfig(options.projectRoot, options.environment);
|
|
62
|
+
const supervisor = options.supervisor instanceof InstanceSupervisor
|
|
63
|
+
? options.supervisor
|
|
64
|
+
: new InstanceSupervisor(options.supervisor);
|
|
65
|
+
const transport = options.transport({ supervisor, projectRoot: options.projectRoot });
|
|
66
|
+
// `tokenRegistry` is read lazily: the server creates the default one.
|
|
67
|
+
let registry = options.tokenRegistry;
|
|
68
|
+
// gateway.readyTimeout is re-read before each start and transaction (ADR 0054 §4).
|
|
69
|
+
const readSettings = options.settings === false ? undefined : options.settings ?? (() => loadGatewaySettings(options.projectRoot));
|
|
70
|
+
let configuredReadyTimeoutMs;
|
|
71
|
+
const refreshSettings = async () => {
|
|
72
|
+
if (!readSettings)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
configuredReadyTimeoutMs = (await readSettings()).readyTimeoutMs;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return; // keep the previous value
|
|
79
|
+
}
|
|
80
|
+
supervisor.setReadyTimeout(configuredReadyTimeoutMs);
|
|
81
|
+
};
|
|
82
|
+
// Automatic restarts wait in `backoff` (crash) or for the old process to stop (`restart-request`,
|
|
83
|
+
// e.g. after a config change): refresh meanwhile.
|
|
84
|
+
supervisor.on('state', (status) => { if (status.state === 'backoff')
|
|
85
|
+
void refreshSettings(); });
|
|
86
|
+
supervisor.on('restart-request', () => { void refreshSettings(); });
|
|
87
|
+
const packages = options.packages === false
|
|
88
|
+
? undefined
|
|
89
|
+
: options.packages ?? new GatewayPackageService({
|
|
90
|
+
projectRoot: options.projectRoot,
|
|
91
|
+
instance: supervisor,
|
|
92
|
+
readyTimeoutMs: () => configuredReadyTimeoutMs ?? options.readyTimeoutMs,
|
|
93
|
+
beforeTransaction: refreshSettings,
|
|
94
|
+
hasOwner: () => registry?.initialized() ?? true,
|
|
95
|
+
});
|
|
96
|
+
let consoleStopped = false;
|
|
97
|
+
const lifecycle = {
|
|
98
|
+
start: async () => { await refreshSettings(); await supervisor.start(); consoleStopped = false; },
|
|
99
|
+
stop: async (stopOptions) => { consoleStopped = true; await supervisor.stop(stopOptions); },
|
|
100
|
+
restart: async (restartOptions) => { await refreshSettings(); await supervisor.restart(restartOptions); consoleStopped = false; },
|
|
101
|
+
status: () => supervisor.status(),
|
|
102
|
+
logs: supervisor.logs,
|
|
103
|
+
};
|
|
104
|
+
const rpc = createGatewayRpcDispatcher({
|
|
105
|
+
...(packages ? { packages } : {}),
|
|
106
|
+
instance: lifecycle,
|
|
107
|
+
});
|
|
108
|
+
const server = new GatewayHttpServer({
|
|
109
|
+
http,
|
|
110
|
+
instance: { transport: () => transport, state: () => supervisor.state },
|
|
111
|
+
...(options.tokenRegistry ? { tokenRegistry: options.tokenRegistry } : {}),
|
|
112
|
+
...(options.retryAfterSeconds !== undefined ? { retryAfterSeconds: options.retryAfterSeconds } : {}),
|
|
113
|
+
...(options.maxRequestBodyBytes !== undefined ? { maxRequestBodyBytes: options.maxRequestBodyBytes } : {}),
|
|
114
|
+
consoleRpc: createGatewayConsoleRpc(rpc),
|
|
115
|
+
});
|
|
116
|
+
registry = server.tokenRegistry;
|
|
117
|
+
const disposeEvents = registerGatewayEventRoutes({
|
|
118
|
+
server,
|
|
119
|
+
...(packages ? { packages } : {}),
|
|
120
|
+
instance: { onChange: (listener) => transport.onRoutes(() => listener()) },
|
|
121
|
+
});
|
|
122
|
+
let gatewayConsole;
|
|
123
|
+
try {
|
|
124
|
+
// ADR 0054 §7: Console accounts live in the gateway's own connection to the project database.
|
|
125
|
+
if (options.console !== false) {
|
|
126
|
+
gatewayConsole = await startGatewayConsole({
|
|
127
|
+
...(options.console ?? {}),
|
|
128
|
+
server,
|
|
129
|
+
projectRoot: options.projectRoot,
|
|
130
|
+
...(options.environment ? { environment: options.environment } : {}),
|
|
131
|
+
instance: transport,
|
|
132
|
+
restartInstance: async () => {
|
|
133
|
+
if (supervisor.state === 'stopped')
|
|
134
|
+
return;
|
|
135
|
+
await refreshSettings();
|
|
136
|
+
await supervisor.restart({ reason: 'database' });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
await settleWithin(gatewayConsole.ready, options.databaseWaitMs ?? 10_000);
|
|
140
|
+
}
|
|
141
|
+
assertNotOpenOnNetwork(http, server.tokenRegistry);
|
|
142
|
+
// Settings for the first instance start, read before listening so it starts right away.
|
|
143
|
+
if (options.startInstance !== false)
|
|
144
|
+
await refreshSettings();
|
|
145
|
+
await server.listen();
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
disposeEvents();
|
|
149
|
+
await gatewayConsole?.close().catch(() => undefined);
|
|
150
|
+
await transport.dispose?.();
|
|
151
|
+
supervisor.dispose();
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
transport.setAddresses?.(server.addresses);
|
|
155
|
+
const instanceStarted = options.startInstance === false
|
|
156
|
+
? Promise.resolve()
|
|
157
|
+
: supervisor.start().catch((error) => {
|
|
158
|
+
// The Console stays reachable to show the failure and the logs.
|
|
159
|
+
logger.warn(formatCompact({
|
|
160
|
+
op: 'gateway_instance_start_failed',
|
|
161
|
+
error: error instanceof Error ? error.message : String(error),
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
let closeResult;
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
http: server,
|
|
167
|
+
supervisor,
|
|
168
|
+
transport,
|
|
169
|
+
packages,
|
|
170
|
+
console: gatewayConsole,
|
|
171
|
+
rpc,
|
|
172
|
+
ownsInstanceLifecycle: () => consoleStopped || packages?.activeTransaction !== undefined,
|
|
173
|
+
instanceStarted,
|
|
174
|
+
close() {
|
|
175
|
+
closeResult ??= (async () => {
|
|
176
|
+
try {
|
|
177
|
+
await supervisor.stop({ reason: 'gateway-stop' }).catch(() => undefined);
|
|
178
|
+
await transport.dispose?.();
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
disposeEvents();
|
|
182
|
+
await gatewayConsole?.close().catch(() => undefined);
|
|
183
|
+
supervisor.dispose();
|
|
184
|
+
await server.close();
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
return closeResult;
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** Thrown when the gateway would serve an open Host on a network address. */
|
|
192
|
+
export class OpenHostOnNetworkError extends Error {
|
|
193
|
+
host;
|
|
194
|
+
code = 'ZHIN_OPEN_HOST_ON_NETWORK';
|
|
195
|
+
constructor(host) {
|
|
196
|
+
super(`http.host 为 ${host}(可从网络访问),但既没有初始化令牌(http.token),Console 也还没有所有者:`
|
|
197
|
+
+ '开放模式拥有全部权限,只允许监听本机地址。请运行 zhin doctor --fix 在 .env 生成 HTTP_TOKEN 并在配置里用 '
|
|
198
|
+
+ 'http.token: ${HTTP_TOKEN} 引用,或先运行 zhin console init 创建所有者;也可以把 http.host 改为 127.0.0.1。');
|
|
199
|
+
this.host = host;
|
|
200
|
+
this.name = 'OpenHostOnNetworkError';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* An open Host (no bootstrap token, no owner yet) holds every permission and is only meant for
|
|
205
|
+
* this machine. The per-request loopback check cannot see through a same-host reverse proxy that
|
|
206
|
+
* adds no forwarding header, so a network-facing `http.host` must not start open (ADR 0049 §1).
|
|
207
|
+
*/
|
|
208
|
+
export function assertNotOpenOnNetwork(http, registry) {
|
|
209
|
+
if (registry.hasAnyToken() || isLoopbackHost(http.host))
|
|
210
|
+
return;
|
|
211
|
+
throw new OpenHostOnNetworkError(http.host ?? '');
|
|
212
|
+
}
|
|
213
|
+
/** Waits for `promise` at most `ms` (the gateway listens anyway; the work continues). */
|
|
214
|
+
function settleWithin(promise, ms) {
|
|
215
|
+
let timer;
|
|
216
|
+
return Promise.race([
|
|
217
|
+
promise.then(() => undefined, () => undefined),
|
|
218
|
+
new Promise((resolve) => { timer = setTimeout(resolve, ms); timer.unref?.(); }),
|
|
219
|
+
]).finally(() => clearTimeout(timer));
|
|
220
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function parseBoundedInteger(value: string | null, fallback: number, minimum: number, maximum: number): number;
|
|
2
|
+
/** A complete HTML page response. */
|
|
3
|
+
export declare function htmlResponse(html: string, status?: number): Response;
|
|
4
|
+
/** One Server-Sent Events frame. */
|
|
5
|
+
export declare function sseFrame(type: string, data: unknown, id?: string): string;
|
|
6
|
+
export declare function normalizeBase(value: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function parseBoundedInteger(value, fallback, minimum, maximum) {
|
|
2
|
+
if (value == null || value.trim() === '')
|
|
3
|
+
return fallback;
|
|
4
|
+
const parsed = Number(value);
|
|
5
|
+
if (!Number.isInteger(parsed))
|
|
6
|
+
return fallback;
|
|
7
|
+
return Math.min(maximum, Math.max(minimum, parsed));
|
|
8
|
+
}
|
|
9
|
+
/** A complete HTML page response. */
|
|
10
|
+
export function htmlResponse(html, status = 200) {
|
|
11
|
+
return new Response(html, { status, headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
12
|
+
}
|
|
13
|
+
/** One Server-Sent Events frame. */
|
|
14
|
+
export function sseFrame(type, data, id) {
|
|
15
|
+
return `${id ? `id: ${id}\n` : ''}event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
16
|
+
}
|
|
17
|
+
export function normalizeBase(value) {
|
|
18
|
+
if (!value.startsWith('/'))
|
|
19
|
+
return `/${value}`;
|
|
20
|
+
// Linear trim instead of /\/+$/ (polynomial backtracking on long runs of '/').
|
|
21
|
+
let end = value.length;
|
|
22
|
+
while (end > 0 && value.charCodeAt(end - 1) === 0x2f)
|
|
23
|
+
end -= 1;
|
|
24
|
+
return value.slice(0, end) || '/api';
|
|
25
|
+
}
|
package/lib/http.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@zhin.js/gateway/http` — HTML / SSE response helpers shared by gateway and instance routes
|
|
3
|
+
* (JSON: `jsonResponse` from `@zhin.js/host-http`). The instance imports this subpath, so it must
|
|
4
|
+
* not load the Node HTTP stack; the gateway HTTP server is `@zhin.js/gateway/server`.
|
|
5
|
+
*/
|
|
6
|
+
export * from './http-response.js';
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@zhin.js/gateway/http` — HTML / SSE response helpers shared by gateway and instance routes
|
|
3
|
+
* (JSON: `jsonResponse` from `@zhin.js/host-http`). The instance imports this subpath, so it must
|
|
4
|
+
* not load the Node HTTP stack; the gateway HTTP server is `@zhin.js/gateway/server`.
|
|
5
|
+
*/
|
|
6
|
+
export * from './http-response.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const PASSWORD_MIN_LENGTH = 10;
|
|
2
|
+
export declare const PASSWORD_MAX_LENGTH = 1024;
|
|
3
|
+
export declare function passwordProblem(password: unknown): string | null;
|
|
4
|
+
/** `scrypt$N$r$p$salt$hash` (base64url). */
|
|
5
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
6
|
+
export declare function verifyPassword(password: string, encoded: string): Promise<boolean>;
|
|
7
|
+
/** Burns the same work as a real check so unknown usernames are not faster. */
|
|
8
|
+
export declare function verifyAgainstDummy(password: string): Promise<false>;
|
|
9
|
+
/** 32 random bytes, base64url (ADR 0049 §2). */
|
|
10
|
+
export declare function generateToken(): string;
|
|
11
|
+
export declare function sha256(value: string): string;
|
|
12
|
+
export declare function newId(): string;
|