@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.
- package/LICENSE +21 -0
- package/README.md +60 -2
- 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 +116 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local control socket of the gateway (ADR 0054 §6): `.zhin/gateway.sock` with mode 0600 in a
|
|
3
|
+
* directory only the current user can write (a named pipe on Windows). A second gateway on the
|
|
4
|
+
* same project is refused while the first answers; a socket left by a dead gateway is removed.
|
|
5
|
+
* Whoever can connect is the local owner (principal `'local'`).
|
|
6
|
+
*/
|
|
7
|
+
import { lstat, mkdir, chmod, rm, stat } from 'node:fs/promises';
|
|
8
|
+
import { connect, createServer } from 'node:net';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import { isNamedPipe } from './paths.js';
|
|
11
|
+
import { handshakeProof, LineDecoder, LOCAL_GATEWAY_RPC, MAX_LOCAL_LINE_BYTES, } from './protocol.js';
|
|
12
|
+
export class LocalSocketError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message, options) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = 'LocalSocketError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_MAX_CONNECTIONS = 32;
|
|
21
|
+
const defaultUid = () => (typeof process.getuid === 'function' ? process.getuid() : undefined);
|
|
22
|
+
/** Probes an existing endpoint: `live` when something accepts a connection. */
|
|
23
|
+
export function probeLocalSocket(path, timeoutMs = 1_000) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const socket = connect(path);
|
|
26
|
+
const timer = setTimeout(() => { socket.destroy(); resolve('live'); }, timeoutMs);
|
|
27
|
+
socket.once('connect', () => { clearTimeout(timer); socket.destroy(); resolve('live'); });
|
|
28
|
+
socket.once('error', (error) => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT' || error.code === 'ENOTSOCK')
|
|
31
|
+
resolve('dead');
|
|
32
|
+
else
|
|
33
|
+
reject(error);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Checks the socket's directory and any existing socket, removing a stale one. Throws
|
|
39
|
+
* {@link LocalSocketError} when another live gateway owns the path or ownership is wrong.
|
|
40
|
+
*/
|
|
41
|
+
export async function prepareLocalSocketPath(path, options = {}) {
|
|
42
|
+
const probe = () => probeLocalSocket(path, options.probeTimeoutMs);
|
|
43
|
+
if (isNamedPipe(path)) {
|
|
44
|
+
if (await probe() === 'live')
|
|
45
|
+
throw new LocalSocketError('in_use', `另一个网关正在使用 ${path}`);
|
|
46
|
+
return { removedStale: false };
|
|
47
|
+
}
|
|
48
|
+
const uid = (options.uid ?? defaultUid)();
|
|
49
|
+
const directory = dirname(path);
|
|
50
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
51
|
+
const dir = await lstat(directory);
|
|
52
|
+
if (!dir.isDirectory() || dir.isSymbolicLink())
|
|
53
|
+
throw new LocalSocketError('insecure_dir', `${directory} 不是目录`);
|
|
54
|
+
const sticky = (dir.mode & 0o1000) !== 0;
|
|
55
|
+
if (!sticky) {
|
|
56
|
+
if (uid !== undefined && dir.uid !== uid) {
|
|
57
|
+
throw new LocalSocketError('insecure_dir', `${directory} 属于其他用户(uid ${dir.uid}),拒绝在其中创建控制套接字`);
|
|
58
|
+
}
|
|
59
|
+
if ((dir.mode & 0o022) !== 0) {
|
|
60
|
+
throw new LocalSocketError('insecure_dir', `${directory} 可被其他用户写入(mode ${(dir.mode & 0o777).toString(8)});请执行 chmod go-w ${directory}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let existing;
|
|
64
|
+
try {
|
|
65
|
+
existing = await lstat(path);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code === 'ENOENT')
|
|
69
|
+
return { removedStale: false };
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if (!existing.isSocket())
|
|
73
|
+
throw new LocalSocketError('not_socket', `${path} 已存在且不是套接字,拒绝覆盖`);
|
|
74
|
+
if (uid !== undefined && existing.uid !== uid) {
|
|
75
|
+
throw new LocalSocketError('not_owner', `${path} 属于其他用户(uid ${existing.uid});请以启动网关的用户运行`);
|
|
76
|
+
}
|
|
77
|
+
if (await probe() === 'live')
|
|
78
|
+
throw new LocalSocketError('in_use', `另一个网关正在使用 ${path}`);
|
|
79
|
+
await rm(path, { force: true });
|
|
80
|
+
return { removedStale: true };
|
|
81
|
+
}
|
|
82
|
+
/** The listening control server. */
|
|
83
|
+
export class LocalControlServer {
|
|
84
|
+
path;
|
|
85
|
+
#server;
|
|
86
|
+
#sockets = new Set();
|
|
87
|
+
#handler;
|
|
88
|
+
#handshakeKey;
|
|
89
|
+
#inode;
|
|
90
|
+
#closed;
|
|
91
|
+
constructor(path, server, handler, handshakeKey) {
|
|
92
|
+
this.path = path;
|
|
93
|
+
this.#server = server;
|
|
94
|
+
this.#handler = handler;
|
|
95
|
+
this.#handshakeKey = handshakeKey;
|
|
96
|
+
}
|
|
97
|
+
/** Validates the path, removes a stale socket, then listens with mode 0600. */
|
|
98
|
+
static async open(options) {
|
|
99
|
+
await prepareLocalSocketPath(options.path, options);
|
|
100
|
+
await options.beforeListen?.();
|
|
101
|
+
const server = createServer();
|
|
102
|
+
server.maxConnections = options.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
103
|
+
const control = new LocalControlServer(options.path, server, options.handler, options.handshakeKey);
|
|
104
|
+
server.on('connection', (socket) => { control.#accept(socket); });
|
|
105
|
+
await control.#listen(options);
|
|
106
|
+
return control;
|
|
107
|
+
}
|
|
108
|
+
setHandler(handler) { this.#handler = handler; }
|
|
109
|
+
/** Stops accepting, drops connections and removes the socket file it created. */
|
|
110
|
+
close() {
|
|
111
|
+
this.#closed ??= (async () => {
|
|
112
|
+
for (const socket of this.#sockets)
|
|
113
|
+
socket.destroy();
|
|
114
|
+
await new Promise((resolve) => { this.#server.close(() => resolve()); });
|
|
115
|
+
if (isNamedPipe(this.path) || this.#inode === undefined)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
// Only remove the file this server bound (a successor may have replaced it).
|
|
119
|
+
if ((await lstat(this.path)).ino === this.#inode)
|
|
120
|
+
await rm(this.path, { force: true });
|
|
121
|
+
}
|
|
122
|
+
catch { /* already gone */ }
|
|
123
|
+
})();
|
|
124
|
+
return this.#closed;
|
|
125
|
+
}
|
|
126
|
+
async #listen(options) {
|
|
127
|
+
const pipe = isNamedPipe(this.path);
|
|
128
|
+
let previousMask;
|
|
129
|
+
// The bind happens synchronously inside listen(): the socket is created 0600.
|
|
130
|
+
if (!pipe) {
|
|
131
|
+
try {
|
|
132
|
+
previousMask = process.umask(0o177);
|
|
133
|
+
}
|
|
134
|
+
catch { /* workers cannot set umask */ }
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await new Promise((resolve, reject) => {
|
|
138
|
+
const onError = (error) => { reject(error); };
|
|
139
|
+
this.#server.once('error', onError);
|
|
140
|
+
try {
|
|
141
|
+
this.#server.listen(this.path, () => { this.#server.off('error', onError); resolve(); });
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (previousMask !== undefined) {
|
|
145
|
+
process.umask(previousMask);
|
|
146
|
+
previousMask = undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (previousMask !== undefined)
|
|
153
|
+
process.umask(previousMask);
|
|
154
|
+
}
|
|
155
|
+
if (pipe)
|
|
156
|
+
return;
|
|
157
|
+
await chmod(this.path, 0o600);
|
|
158
|
+
const bound = await stat(this.path);
|
|
159
|
+
if ((bound.mode & 0o077) !== 0) {
|
|
160
|
+
await this.close();
|
|
161
|
+
throw new LocalSocketError('insecure_socket', `无法把 ${this.path} 限制为仅当前用户可访问`);
|
|
162
|
+
}
|
|
163
|
+
this.#inode = bound.ino;
|
|
164
|
+
}
|
|
165
|
+
#accept(socket) {
|
|
166
|
+
this.#sockets.add(socket);
|
|
167
|
+
socket.setEncoding('utf8');
|
|
168
|
+
const decoder = new LineDecoder();
|
|
169
|
+
const write = (line) => {
|
|
170
|
+
if (socket.destroyed || !socket.writable)
|
|
171
|
+
return;
|
|
172
|
+
let text = JSON.stringify(line);
|
|
173
|
+
// The peer refuses longer lines (UTF-8 bytes): answer with an error instead of breaking the connection.
|
|
174
|
+
if (Buffer.byteLength(text) > MAX_LOCAL_LINE_BYTES) {
|
|
175
|
+
if ('event' in line)
|
|
176
|
+
return;
|
|
177
|
+
text = JSON.stringify({ id: line.id, ok: false, error: { code: 'unavailable', message: '回复超过本地套接字上限(1 MiB);改用 HTTP 请求', status: 502 } });
|
|
178
|
+
}
|
|
179
|
+
socket.write(`${text}\n`);
|
|
180
|
+
};
|
|
181
|
+
socket.on('data', (chunk) => {
|
|
182
|
+
let lines;
|
|
183
|
+
try {
|
|
184
|
+
lines = decoder.push(chunk);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
write({ id: 0, ok: false, error: { code: 'invalid_input', message: '请求过长', status: 400 } });
|
|
188
|
+
socket.end();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const line of lines)
|
|
192
|
+
void this.#handle(line, write);
|
|
193
|
+
});
|
|
194
|
+
socket.on('error', () => { socket.destroy(); });
|
|
195
|
+
socket.on('close', () => { this.#sockets.delete(socket); });
|
|
196
|
+
}
|
|
197
|
+
async #handle(line, write) {
|
|
198
|
+
let request;
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(line);
|
|
201
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.type !== 'string'
|
|
202
|
+
|| (typeof parsed.id !== 'number' && typeof parsed.id !== 'string'))
|
|
203
|
+
throw new Error('shape');
|
|
204
|
+
request = parsed;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
write({ id: 0, ok: false, error: { code: 'invalid_input', message: '请求不是有效的 JSON 行', status: 400 } });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (request.type === LOCAL_GATEWAY_RPC.HELLO) {
|
|
211
|
+
const nonce = request.data?.nonce;
|
|
212
|
+
if (typeof nonce !== 'string' || nonce.length === 0 || nonce.length > 256) {
|
|
213
|
+
write({ id: request.id, ok: false, error: { code: 'invalid_input', message: 'nonce 必须是非空字符串', status: 400 } });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
write({ id: request.id, ok: true, data: { proof: this.#handshakeKey ? handshakeProof(this.#handshakeKey, nonce) : null } });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const handler = this.#handler;
|
|
220
|
+
if (!handler) {
|
|
221
|
+
write({ id: request.id, ok: false, error: { code: 'unavailable', message: '网关正在启动', status: 503 } });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const result = await handler(request, (event) => { write({ id: request.id, event }); });
|
|
226
|
+
write({ id: request.id, ...result });
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
write({ id: request.id, ok: false, error: { code: 'internal', message: '网关内部错误', status: 500 } });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const GATEWAY_STATE_DIR = ".zhin";
|
|
2
|
+
export declare const GATEWAY_SOCKET_FILE = "gateway.sock";
|
|
3
|
+
export declare const GATEWAY_INFO_FILE = "gateway.json";
|
|
4
|
+
export declare const GATEWAY_LOG_FILE = "gateway.log";
|
|
5
|
+
/** `<root>/.zhin`. */
|
|
6
|
+
export declare function gatewayStateDir(root: string): string;
|
|
7
|
+
/** `<root>/.zhin/gateway.json`. */
|
|
8
|
+
export declare function gatewayInfoFile(root: string): string;
|
|
9
|
+
/** `<root>/.zhin/logs/gateway.log`: the background gateway's own output. */
|
|
10
|
+
export declare function gatewayLogFile(root: string): string;
|
|
11
|
+
/** Stable short hash of a project root; Windows paths are case-insensitive. */
|
|
12
|
+
export declare function projectRootHash(root: string, platform?: NodeJS.Platform): string;
|
|
13
|
+
/**
|
|
14
|
+
* `\\.\pipe\zhin-gateway-<hash of the project root>[-<nonce>]`. A running gateway adds a fresh
|
|
15
|
+
* random `nonce` per start: Node cannot set an owner or DACL on a named pipe, so the name keeps
|
|
16
|
+
* another local account from creating the pipe ahead of the gateway; only the gateway's state
|
|
17
|
+
* file (`.zhin/gateway.json`) records it, and clients also check the handshake proof keyed by
|
|
18
|
+
* that file ({@link LocalGatewayInfo.handshakeKey}).
|
|
19
|
+
*/
|
|
20
|
+
export declare function windowsPipeName(root: string, nonce?: string): string;
|
|
21
|
+
export interface GatewaySocketPathOptions {
|
|
22
|
+
readonly platform?: NodeJS.Platform;
|
|
23
|
+
/** Windows: random pipe-name component of a running gateway (see {@link windowsPipeName}). */
|
|
24
|
+
readonly nonce?: string;
|
|
25
|
+
/** Fallback directory for roots too deep for a Unix socket path. Default `os.tmpdir()`. */
|
|
26
|
+
readonly tmpdir?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The local control endpoint of a project. Unix: `<root>/.zhin/gateway.sock`, or
|
|
30
|
+
* `<tmpdir>/zhin-gateway-<hash>.sock` when that path would not fit `sun_path`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function gatewaySocketPath(root: string, options?: GatewaySocketPathOptions): string;
|
|
33
|
+
/** Whether an endpoint is a Windows named pipe (no file to stat, chmod or unlink). */
|
|
34
|
+
export declare function isNamedPipe(path: string): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a project's gateway keeps its local control endpoint and state (ADR 0054 §6):
|
|
3
|
+
* `.zhin/gateway.sock` (a named pipe `\\.\pipe\zhin-gateway-<hash>` on Windows) and
|
|
4
|
+
* `.zhin/gateway.json`.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join, resolve, win32 } from 'node:path';
|
|
9
|
+
export const GATEWAY_STATE_DIR = '.zhin';
|
|
10
|
+
export const GATEWAY_SOCKET_FILE = 'gateway.sock';
|
|
11
|
+
export const GATEWAY_INFO_FILE = 'gateway.json';
|
|
12
|
+
export const GATEWAY_LOG_FILE = 'gateway.log';
|
|
13
|
+
/** sockaddr_un.sun_path is 104 (macOS) / 108 (Linux) bytes including the terminator. */
|
|
14
|
+
const MAX_UNIX_SOCKET_PATH_BYTES = 100;
|
|
15
|
+
/** `<root>/.zhin`. */
|
|
16
|
+
export function gatewayStateDir(root) {
|
|
17
|
+
return join(resolve(root), GATEWAY_STATE_DIR);
|
|
18
|
+
}
|
|
19
|
+
/** `<root>/.zhin/gateway.json`. */
|
|
20
|
+
export function gatewayInfoFile(root) {
|
|
21
|
+
return join(gatewayStateDir(root), GATEWAY_INFO_FILE);
|
|
22
|
+
}
|
|
23
|
+
/** `<root>/.zhin/logs/gateway.log`: the background gateway's own output. */
|
|
24
|
+
export function gatewayLogFile(root) {
|
|
25
|
+
return join(gatewayStateDir(root), 'logs', GATEWAY_LOG_FILE);
|
|
26
|
+
}
|
|
27
|
+
/** Stable short hash of a project root; Windows paths are case-insensitive. */
|
|
28
|
+
export function projectRootHash(root, platform = process.platform) {
|
|
29
|
+
const normalized = platform === 'win32' ? win32.resolve(root).toLowerCase() : resolve(root);
|
|
30
|
+
return createHash('sha256').update(normalized).digest('hex').slice(0, 16);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `\\.\pipe\zhin-gateway-<hash of the project root>[-<nonce>]`. A running gateway adds a fresh
|
|
34
|
+
* random `nonce` per start: Node cannot set an owner or DACL on a named pipe, so the name keeps
|
|
35
|
+
* another local account from creating the pipe ahead of the gateway; only the gateway's state
|
|
36
|
+
* file (`.zhin/gateway.json`) records it, and clients also check the handshake proof keyed by
|
|
37
|
+
* that file ({@link LocalGatewayInfo.handshakeKey}).
|
|
38
|
+
*/
|
|
39
|
+
export function windowsPipeName(root, nonce) {
|
|
40
|
+
return `\\\\.\\pipe\\zhin-gateway-${projectRootHash(root, 'win32')}${nonce ? `-${nonce}` : ''}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The local control endpoint of a project. Unix: `<root>/.zhin/gateway.sock`, or
|
|
44
|
+
* `<tmpdir>/zhin-gateway-<hash>.sock` when that path would not fit `sun_path`.
|
|
45
|
+
*/
|
|
46
|
+
export function gatewaySocketPath(root, options = {}) {
|
|
47
|
+
const platform = options.platform ?? process.platform;
|
|
48
|
+
if (platform === 'win32')
|
|
49
|
+
return windowsPipeName(root, options.nonce);
|
|
50
|
+
const preferred = join(gatewayStateDir(root), GATEWAY_SOCKET_FILE);
|
|
51
|
+
if (Buffer.byteLength(preferred) <= MAX_UNIX_SOCKET_PATH_BYTES)
|
|
52
|
+
return preferred;
|
|
53
|
+
return join(options.tmpdir ?? tmpdir(), `zhin-gateway-${projectRootHash(root, platform)}.sock`);
|
|
54
|
+
}
|
|
55
|
+
/** Whether an endpoint is a Windows named pipe (no file to stat, chmod or unlink). */
|
|
56
|
+
export function isNamedPipe(path) {
|
|
57
|
+
return path.startsWith('\\\\.\\pipe\\') || path.startsWith('\\\\?\\pipe\\');
|
|
58
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { GatewayInstanceStatus, GatewayPackageEvent, GatewayRpcErrorCode, GatewayRpcErrorDetails } from '@zhin.js/console-protocol';
|
|
2
|
+
/** Socket-only RPC: not reachable over HTTP. */
|
|
3
|
+
export declare const LOCAL_GATEWAY_RPC: Readonly<{
|
|
4
|
+
/**
|
|
5
|
+
* `{ nonce }` → `{ proof }`: HMAC-SHA256 of `nonce` under the run's handshake key (recorded only
|
|
6
|
+
* in the 0600 `.zhin/gateway.json`). The CLI sends it first and refuses an endpoint that cannot
|
|
7
|
+
* prove it is this project's gateway (a squatted Windows named pipe). Answered before any other
|
|
8
|
+
* request, also while the gateway is still starting.
|
|
9
|
+
*/
|
|
10
|
+
readonly HELLO: "gateway.hello";
|
|
11
|
+
/** Gateway process info and instance status. */
|
|
12
|
+
readonly INFO: "gateway.info";
|
|
13
|
+
/** Stops the instance, then the gateway. */
|
|
14
|
+
readonly SHUTDOWN: "gateway.shutdown";
|
|
15
|
+
/**
|
|
16
|
+
* `zhin console init` beside a running gateway (ADR 0049 §7): the gateway's own
|
|
17
|
+
* `ConsoleSetupService` answers, so a database switch is applied live (reconnect + instance
|
|
18
|
+
* restart) and the owner state changes at once.
|
|
19
|
+
*/
|
|
20
|
+
readonly CONSOLE_SETUP_STATUS: "gateway.console.setup.status";
|
|
21
|
+
readonly CONSOLE_SETUP_DRIVERS: "gateway.console.setup.drivers";
|
|
22
|
+
/** `{ dialect, connection }` → the probe result. */
|
|
23
|
+
readonly CONSOLE_SETUP_DATABASE_TEST: "gateway.console.setup.database.test";
|
|
24
|
+
/** `{ dialect, connection }`: writes the config, reconnects the gateway, restarts the instance. */
|
|
25
|
+
readonly CONSOLE_SETUP_DATABASE: "gateway.console.setup.database";
|
|
26
|
+
/** `{ username, password, displayName? }`: the first owner. */
|
|
27
|
+
readonly CONSOLE_SETUP_OWNER: "gateway.console.setup.owner";
|
|
28
|
+
/** `zhin console user …` beside a running gateway: the gateway's `ConsoleIamService` answers. */
|
|
29
|
+
readonly CONSOLE_USER_LIST: "gateway.console.user.list";
|
|
30
|
+
/** `{ username, password, displayName?, role }`. */
|
|
31
|
+
readonly CONSOLE_USER_CREATE: "gateway.console.user.create";
|
|
32
|
+
/** `{ username, password }`: resets the password (revokes the user's sessions). */
|
|
33
|
+
readonly CONSOLE_USER_PASSWD: "gateway.console.user.passwd";
|
|
34
|
+
/** `{ username, disabled }`. */
|
|
35
|
+
readonly CONSOLE_USER_DISABLE: "gateway.console.user.disable";
|
|
36
|
+
/**
|
|
37
|
+
* `{base}/…` request on behalf of the local owner ({@link LocalGatewayHttpRequest}): what
|
|
38
|
+
* `zhin config` / `schedule` / `send` / `watch` use while a gateway runs, so they need no
|
|
39
|
+
* Console token (ADR 0054 §6). Answered like the HTTP route ({@link LocalGatewayHttpResponse}).
|
|
40
|
+
*/
|
|
41
|
+
readonly HTTP_REQUEST: "gateway.http.request";
|
|
42
|
+
}>;
|
|
43
|
+
export type LocalGatewayHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
44
|
+
export interface LocalGatewayHttpRequest {
|
|
45
|
+
readonly method: LocalGatewayHttpMethod;
|
|
46
|
+
/** Path and query under the API base, e.g. `/api/assistant/jobs` (the gateway's `http.base`). */
|
|
47
|
+
readonly path: string;
|
|
48
|
+
/** Request body (JSON text); sent as `application/json`. Not allowed with `GET`. */
|
|
49
|
+
readonly body?: string;
|
|
50
|
+
/**
|
|
51
|
+
* An explicit Console credential (`ZHIN_CONSOLE_TOKEN`): the gateway authenticates it like the
|
|
52
|
+
* HTTP route does and runs the request as that principal (permissions, audit). Without it the
|
|
53
|
+
* request runs as the local owner.
|
|
54
|
+
*/
|
|
55
|
+
readonly token?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface LocalGatewayHttpResponse {
|
|
58
|
+
readonly status: number;
|
|
59
|
+
/** `content-type` and `retry-after` when present. */
|
|
60
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
61
|
+
/** Response body as text. */
|
|
62
|
+
readonly body: string;
|
|
63
|
+
}
|
|
64
|
+
export type LocalGatewayRpcType = (typeof LOCAL_GATEWAY_RPC)[keyof typeof LOCAL_GATEWAY_RPC];
|
|
65
|
+
/** Longest accepted line in either direction (UTF-8 bytes). */
|
|
66
|
+
export declare const MAX_LOCAL_LINE_BYTES: number;
|
|
67
|
+
/**
|
|
68
|
+
* Largest `gateway.http.request` body / response body, in UTF-8 bytes of its JSON-encoded form:
|
|
69
|
+
* the line limit minus room for the envelope (`id`, `type`, path, headers…). Larger ones get an
|
|
70
|
+
* error telling the caller to use HTTP instead.
|
|
71
|
+
*/
|
|
72
|
+
export declare const MAX_LOCAL_HTTP_BODY_BYTES: number;
|
|
73
|
+
export interface LocalGatewayRequest {
|
|
74
|
+
readonly id: number | string;
|
|
75
|
+
readonly type: string;
|
|
76
|
+
readonly data?: unknown;
|
|
77
|
+
}
|
|
78
|
+
/** Gateway RPC codes, plus the Console account codes of the `gateway.console.*` RPC. */
|
|
79
|
+
export type LocalGatewayErrorCode = GatewayRpcErrorCode | 'not_found' | 'last_owner' | 'unauthorized' | 'rate_limited' | 'database_busy';
|
|
80
|
+
/** `errors` / `missingEnv` as in the HTTP reply ({@link GatewayRpcErrorDetails}), only when non-empty. */
|
|
81
|
+
export interface LocalGatewayError extends GatewayRpcErrorDetails {
|
|
82
|
+
readonly code: LocalGatewayErrorCode;
|
|
83
|
+
readonly message: string;
|
|
84
|
+
readonly status: number;
|
|
85
|
+
}
|
|
86
|
+
export type LocalGatewayResponse = Readonly<{
|
|
87
|
+
id: number | string;
|
|
88
|
+
ok: true;
|
|
89
|
+
data: unknown;
|
|
90
|
+
}> | Readonly<{
|
|
91
|
+
id: number | string;
|
|
92
|
+
ok: false;
|
|
93
|
+
error: LocalGatewayError;
|
|
94
|
+
}>;
|
|
95
|
+
export interface LocalGatewayEventLine {
|
|
96
|
+
readonly id: number | string;
|
|
97
|
+
readonly event: GatewayPackageEvent;
|
|
98
|
+
}
|
|
99
|
+
export type LocalGatewayServerLine = LocalGatewayResponse | LocalGatewayEventLine;
|
|
100
|
+
/** `.zhin/gateway.json` and the `gateway.info` answer. */
|
|
101
|
+
export interface LocalGatewayInfo {
|
|
102
|
+
readonly pid: number;
|
|
103
|
+
/** Ports of the gateway's HTTP listeners. */
|
|
104
|
+
readonly ports: readonly number[];
|
|
105
|
+
/** Listener origins (`http://127.0.0.1:8068`). */
|
|
106
|
+
readonly origins: readonly string[];
|
|
107
|
+
/** Local control endpoint (socket path or pipe name). */
|
|
108
|
+
readonly socket: string;
|
|
109
|
+
/** Epoch ms. */
|
|
110
|
+
readonly startedAt: number;
|
|
111
|
+
/** `@zhin.js/gateway` version. */
|
|
112
|
+
readonly version: string;
|
|
113
|
+
/** `foreground` (attached to a terminal / service manager) or `daemon` (`zhin start`). */
|
|
114
|
+
readonly mode: 'foreground' | 'daemon';
|
|
115
|
+
/** Start options the instance runs with (re-used by `zhin restart --gateway`). */
|
|
116
|
+
readonly args: readonly string[];
|
|
117
|
+
/** Instance log file. */
|
|
118
|
+
readonly logFile?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Key of the {@link LOCAL_GATEWAY_RPC.HELLO} handshake (hex). Only in the state file (mode
|
|
121
|
+
* 0600), never in the `gateway.info` answer.
|
|
122
|
+
*/
|
|
123
|
+
readonly handshakeKey?: string;
|
|
124
|
+
}
|
|
125
|
+
export interface LocalGatewayInfoResult {
|
|
126
|
+
readonly gateway: LocalGatewayInfo;
|
|
127
|
+
readonly instance: GatewayInstanceStatus;
|
|
128
|
+
/** False until the first Console owner exists (the bootstrap token still works). */
|
|
129
|
+
readonly consoleInitialized: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** The {@link LOCAL_GATEWAY_RPC.HELLO} proof for `nonce` under a hex `key`. */
|
|
132
|
+
export declare function handshakeProof(key: string, nonce: string): string;
|
|
133
|
+
/** Splits a stream into lines, refusing lines longer than {@link MAX_LOCAL_LINE_BYTES} (UTF-8). */
|
|
134
|
+
export declare class LineDecoder {
|
|
135
|
+
#private;
|
|
136
|
+
readonly maxBytes: number;
|
|
137
|
+
constructor(maxBytes?: number);
|
|
138
|
+
/** Returns the complete lines in `chunk`; throws when a line exceeds the limit. */
|
|
139
|
+
push(chunk: string): string[];
|
|
140
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-lines protocol of the local control socket (ADR 0054 §6). One request per line:
|
|
3
|
+
* `{ id, type, data? }`; the gateway answers each with `{ id, ok, data | error }` and may send
|
|
4
|
+
* `{ id, event }` lines before it (package transaction progress). `type` is a `gateway.*` RPC
|
|
5
|
+
* type (answered with principal `'local'`) or one of the local-only {@link LOCAL_GATEWAY_RPC}.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac } from 'node:crypto';
|
|
8
|
+
/** Socket-only RPC: not reachable over HTTP. */
|
|
9
|
+
export const LOCAL_GATEWAY_RPC = Object.freeze({
|
|
10
|
+
/**
|
|
11
|
+
* `{ nonce }` → `{ proof }`: HMAC-SHA256 of `nonce` under the run's handshake key (recorded only
|
|
12
|
+
* in the 0600 `.zhin/gateway.json`). The CLI sends it first and refuses an endpoint that cannot
|
|
13
|
+
* prove it is this project's gateway (a squatted Windows named pipe). Answered before any other
|
|
14
|
+
* request, also while the gateway is still starting.
|
|
15
|
+
*/
|
|
16
|
+
HELLO: 'gateway.hello',
|
|
17
|
+
/** Gateway process info and instance status. */
|
|
18
|
+
INFO: 'gateway.info',
|
|
19
|
+
/** Stops the instance, then the gateway. */
|
|
20
|
+
SHUTDOWN: 'gateway.shutdown',
|
|
21
|
+
/**
|
|
22
|
+
* `zhin console init` beside a running gateway (ADR 0049 §7): the gateway's own
|
|
23
|
+
* `ConsoleSetupService` answers, so a database switch is applied live (reconnect + instance
|
|
24
|
+
* restart) and the owner state changes at once.
|
|
25
|
+
*/
|
|
26
|
+
CONSOLE_SETUP_STATUS: 'gateway.console.setup.status',
|
|
27
|
+
CONSOLE_SETUP_DRIVERS: 'gateway.console.setup.drivers',
|
|
28
|
+
/** `{ dialect, connection }` → the probe result. */
|
|
29
|
+
CONSOLE_SETUP_DATABASE_TEST: 'gateway.console.setup.database.test',
|
|
30
|
+
/** `{ dialect, connection }`: writes the config, reconnects the gateway, restarts the instance. */
|
|
31
|
+
CONSOLE_SETUP_DATABASE: 'gateway.console.setup.database',
|
|
32
|
+
/** `{ username, password, displayName? }`: the first owner. */
|
|
33
|
+
CONSOLE_SETUP_OWNER: 'gateway.console.setup.owner',
|
|
34
|
+
/** `zhin console user …` beside a running gateway: the gateway's `ConsoleIamService` answers. */
|
|
35
|
+
CONSOLE_USER_LIST: 'gateway.console.user.list',
|
|
36
|
+
/** `{ username, password, displayName?, role }`. */
|
|
37
|
+
CONSOLE_USER_CREATE: 'gateway.console.user.create',
|
|
38
|
+
/** `{ username, password }`: resets the password (revokes the user's sessions). */
|
|
39
|
+
CONSOLE_USER_PASSWD: 'gateway.console.user.passwd',
|
|
40
|
+
/** `{ username, disabled }`. */
|
|
41
|
+
CONSOLE_USER_DISABLE: 'gateway.console.user.disable',
|
|
42
|
+
/**
|
|
43
|
+
* `{base}/…` request on behalf of the local owner ({@link LocalGatewayHttpRequest}): what
|
|
44
|
+
* `zhin config` / `schedule` / `send` / `watch` use while a gateway runs, so they need no
|
|
45
|
+
* Console token (ADR 0054 §6). Answered like the HTTP route ({@link LocalGatewayHttpResponse}).
|
|
46
|
+
*/
|
|
47
|
+
HTTP_REQUEST: 'gateway.http.request',
|
|
48
|
+
});
|
|
49
|
+
/** Longest accepted line in either direction (UTF-8 bytes). */
|
|
50
|
+
export const MAX_LOCAL_LINE_BYTES = 1024 * 1024;
|
|
51
|
+
/**
|
|
52
|
+
* Largest `gateway.http.request` body / response body, in UTF-8 bytes of its JSON-encoded form:
|
|
53
|
+
* the line limit minus room for the envelope (`id`, `type`, path, headers…). Larger ones get an
|
|
54
|
+
* error telling the caller to use HTTP instead.
|
|
55
|
+
*/
|
|
56
|
+
export const MAX_LOCAL_HTTP_BODY_BYTES = MAX_LOCAL_LINE_BYTES - 16 * 1024;
|
|
57
|
+
/** The {@link LOCAL_GATEWAY_RPC.HELLO} proof for `nonce` under a hex `key`. */
|
|
58
|
+
export function handshakeProof(key, nonce) {
|
|
59
|
+
return createHmac('sha256', Buffer.from(key, 'hex')).update(nonce).digest('hex');
|
|
60
|
+
}
|
|
61
|
+
/** Splits a stream into lines, refusing lines longer than {@link MAX_LOCAL_LINE_BYTES} (UTF-8). */
|
|
62
|
+
export class LineDecoder {
|
|
63
|
+
maxBytes;
|
|
64
|
+
#buffer = '';
|
|
65
|
+
constructor(maxBytes = MAX_LOCAL_LINE_BYTES) {
|
|
66
|
+
this.maxBytes = maxBytes;
|
|
67
|
+
}
|
|
68
|
+
/** Returns the complete lines in `chunk`; throws when a line exceeds the limit. */
|
|
69
|
+
push(chunk) {
|
|
70
|
+
this.#buffer += chunk;
|
|
71
|
+
const lines = [];
|
|
72
|
+
for (;;) {
|
|
73
|
+
const newline = this.#buffer.indexOf('\n');
|
|
74
|
+
if (newline < 0)
|
|
75
|
+
break;
|
|
76
|
+
const line = this.#buffer.slice(0, newline);
|
|
77
|
+
this.#buffer = this.#buffer.slice(newline + 1);
|
|
78
|
+
if (Buffer.byteLength(line) > this.maxBytes) {
|
|
79
|
+
this.#buffer = '';
|
|
80
|
+
throw new Error('line too long');
|
|
81
|
+
}
|
|
82
|
+
lines.push(line);
|
|
83
|
+
}
|
|
84
|
+
if (Buffer.byteLength(this.#buffer) > this.maxBytes) {
|
|
85
|
+
this.#buffer = '';
|
|
86
|
+
throw new Error('line too long');
|
|
87
|
+
}
|
|
88
|
+
return lines.filter((line) => line.trim().length > 0);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LocalGatewayInfo } from './protocol.js';
|
|
2
|
+
/** Atomically writes the info file with mode 0600. */
|
|
3
|
+
export declare function writeGatewayInfo(root: string, info: LocalGatewayInfo): Promise<string>;
|
|
4
|
+
/** The info file, or undefined when missing or unreadable. */
|
|
5
|
+
export declare function readGatewayInfo(root: string): Promise<LocalGatewayInfo | undefined>;
|
|
6
|
+
export declare function readGatewayInfoSync(root: string): LocalGatewayInfo | undefined;
|
|
7
|
+
/** Removes the info file when it still belongs to `pid`. */
|
|
8
|
+
export declare function removeGatewayInfo(root: string, pid?: number): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** `.zhin/gateway.json`: written by a running gateway, removed when it stops. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { gatewayInfoFile } from './paths.js';
|
|
6
|
+
/** Atomically writes the info file with mode 0600. */
|
|
7
|
+
export async function writeGatewayInfo(root, info) {
|
|
8
|
+
const file = gatewayInfoFile(root);
|
|
9
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
10
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
11
|
+
await writeFile(temporary, `${JSON.stringify(info, null, 2)}\n`, { mode: 0o600 });
|
|
12
|
+
await chmod(temporary, 0o600).catch(() => undefined);
|
|
13
|
+
await rename(temporary, file);
|
|
14
|
+
return file;
|
|
15
|
+
}
|
|
16
|
+
/** The info file, or undefined when missing or unreadable. */
|
|
17
|
+
export async function readGatewayInfo(root) {
|
|
18
|
+
try {
|
|
19
|
+
return parseInfo(await readFile(gatewayInfoFile(root), 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function readGatewayInfoSync(root) {
|
|
26
|
+
try {
|
|
27
|
+
return parseInfo(readFileSync(gatewayInfoFile(root), 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Removes the info file when it still belongs to `pid`. */
|
|
34
|
+
export async function removeGatewayInfo(root, pid = process.pid) {
|
|
35
|
+
const current = await readGatewayInfo(root);
|
|
36
|
+
if (current && current.pid !== pid)
|
|
37
|
+
return;
|
|
38
|
+
await rm(gatewayInfoFile(root), { force: true });
|
|
39
|
+
}
|
|
40
|
+
function parseInfo(text) {
|
|
41
|
+
const value = JSON.parse(text);
|
|
42
|
+
if (!value || typeof value !== 'object' || typeof value.pid !== 'number' || typeof value.socket !== 'string')
|
|
43
|
+
return undefined;
|
|
44
|
+
return value;
|
|
45
|
+
}
|
package/lib/local.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@zhin.js/gateway/local` — the gateway's local control socket (ADR 0054 §6): paths, the
|
|
3
|
+
* JSON-lines protocol, the server, the CLI client and `.zhin/gateway.json`. No Node HTTP stack.
|
|
4
|
+
*/
|
|
5
|
+
export * from './local/paths.js';
|
|
6
|
+
export * from './local/protocol.js';
|
|
7
|
+
export * from './local/local-socket.js';
|
|
8
|
+
export * from './local/handler.js';
|
|
9
|
+
export * from './local/client.js';
|
|
10
|
+
export * from './local/state-file.js';
|
package/lib/local.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@zhin.js/gateway/local` — the gateway's local control socket (ADR 0054 §6): paths, the
|
|
3
|
+
* JSON-lines protocol, the server, the CLI client and `.zhin/gateway.json`. No Node HTTP stack.
|
|
4
|
+
*/
|
|
5
|
+
export * from './local/paths.js';
|
|
6
|
+
export * from './local/protocol.js';
|
|
7
|
+
export * from './local/local-socket.js';
|
|
8
|
+
export * from './local/handler.js';
|
|
9
|
+
export * from './local/client.js';
|
|
10
|
+
export * from './local/state-file.js';
|