@zhin.js/host-http 1.1.14 → 1.1.16
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/lib/console-rest-pages-helpers.d.ts +23 -0
- package/lib/console-rest-pages-helpers.js +570 -0
- package/lib/console-rest-pages.js +10 -570
- package/lib/console-rpc-handlers.d.ts +21 -0
- package/lib/console-rpc-handlers.js +659 -0
- package/lib/console-rpc.d.ts +1 -2
- package/lib/console-rpc.js +6 -620
- package/lib/host-installer.d.ts +8 -0
- package/lib/host-installer.js +211 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +4 -4
- package/src/console-rest-pages-helpers.ts +619 -0
- package/src/console-rest-pages.ts +12 -612
- package/src/console-rpc-handlers.ts +608 -0
- package/src/console-rpc.ts +10 -579
- package/src/host-installer.ts +253 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { bindGenerationAdmission, expandEnvironmentValue, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { httpHostToken, } from './http-host.js';
|
|
5
|
+
export async function resolveHttpConfig(config, envOverlay, projectRoot = process.cwd()) {
|
|
6
|
+
const document = await readConfigDocument(config);
|
|
7
|
+
if (!document || typeof document !== 'object')
|
|
8
|
+
return {};
|
|
9
|
+
const http = document.http;
|
|
10
|
+
if (!http || typeof http !== 'object')
|
|
11
|
+
return {};
|
|
12
|
+
const value = expandEnvironmentValue(http, (key) => envOverlay?.[key] ?? process.env[key]);
|
|
13
|
+
const primary = Object.freeze({
|
|
14
|
+
host: typeof value.host === 'string' ? value.host : undefined,
|
|
15
|
+
port: typeof value.port === 'number' ? value.port : undefined,
|
|
16
|
+
token: typeof value.token === 'string' ? value.token : undefined,
|
|
17
|
+
tokens: parseScopedTokens(value.tokens),
|
|
18
|
+
corsOrigins: injectDesktopWebviewOrigins(parseStringList(value.corsOrigins), envOverlay),
|
|
19
|
+
apiBase: typeof value.base === 'string' ? value.base : undefined,
|
|
20
|
+
tls: await parseTlsConfig(value.tls, projectRoot),
|
|
21
|
+
});
|
|
22
|
+
const configuredListeners = await parseAdditionalListeners(value.listeners, primary, projectRoot);
|
|
23
|
+
const listeners = await injectDesktopDeviceListener(configuredListeners, primary, envOverlay, projectRoot);
|
|
24
|
+
return Object.freeze({ ...primary, ...(listeners.length > 0 ? { listeners } : {}) });
|
|
25
|
+
}
|
|
26
|
+
const DESKTOP_WEBVIEW_ORIGINS = Object.freeze([
|
|
27
|
+
'tauri://localhost',
|
|
28
|
+
'http://tauri.localhost',
|
|
29
|
+
'https://tauri.localhost',
|
|
30
|
+
]);
|
|
31
|
+
function injectDesktopWebviewOrigins(configured, envOverlay) {
|
|
32
|
+
const origin = envOverlay?.ZHIN_DESKTOP_WEBVIEW_ORIGIN
|
|
33
|
+
?? process.env.ZHIN_DESKTOP_WEBVIEW_ORIGIN;
|
|
34
|
+
if (origin === undefined)
|
|
35
|
+
return configured;
|
|
36
|
+
if (!DESKTOP_WEBVIEW_ORIGINS.includes(origin)) {
|
|
37
|
+
throw new Error('ZHIN_DESKTOP_WEBVIEW_ORIGIN is not a trusted Tauri origin');
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze([...new Set([...(configured ?? []), ...DESKTOP_WEBVIEW_ORIGINS])]);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Desktop releases before the dual-listener migration may already have a
|
|
43
|
+
* user-owned zhin.config.yml without http.listeners. The native shell passes
|
|
44
|
+
* these trusted variables on every launch, so append the secure Device
|
|
45
|
+
* listener without replacing the user's persisted configuration.
|
|
46
|
+
*/
|
|
47
|
+
async function injectDesktopDeviceListener(configured, primary, envOverlay, projectRoot) {
|
|
48
|
+
const environment = (key) => envOverlay?.[key] ?? process.env[key];
|
|
49
|
+
if (!parseEnabled(environment('ZHIN_DESKTOP_DEVICE_LISTENER_ENABLED') ?? false, 'ZHIN_DESKTOP_DEVICE_LISTENER_ENABLED'))
|
|
50
|
+
return configured;
|
|
51
|
+
const host = environment('ZHIN_DESKTOP_DEVICE_HOST')?.trim() || '0.0.0.0';
|
|
52
|
+
const portValue = environment('ZHIN_DESKTOP_DEVICE_PORT')?.trim();
|
|
53
|
+
const port = portValue === undefined || portValue === '' ? 17_889 : Number(portValue);
|
|
54
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
|
|
55
|
+
throw new Error('ZHIN_DESKTOP_DEVICE_PORT must be a valid TCP port');
|
|
56
|
+
}
|
|
57
|
+
const existing = configured.find(listener => listener.host === host && listener.port === port);
|
|
58
|
+
if (existing?.tls)
|
|
59
|
+
return configured;
|
|
60
|
+
if (existing) {
|
|
61
|
+
throw new Error('Desktop Device listener conflicts with a non-TLS http.listeners entry');
|
|
62
|
+
}
|
|
63
|
+
const tls = await parseTlsConfig({
|
|
64
|
+
enabled: true,
|
|
65
|
+
keyFile: environment('ZHIN_DESKTOP_TLS_KEY_FILE'),
|
|
66
|
+
certFile: environment('ZHIN_DESKTOP_TLS_CERT_FILE'),
|
|
67
|
+
minVersion: 'TLSv1.2',
|
|
68
|
+
}, projectRoot);
|
|
69
|
+
return Object.freeze([...configured, Object.freeze({ ...primary, host, port, tls })]);
|
|
70
|
+
}
|
|
71
|
+
async function parseAdditionalListeners(value, primary, projectRoot) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return Object.freeze([]);
|
|
74
|
+
if (!Array.isArray(value))
|
|
75
|
+
throw new Error('http.listeners must be an array');
|
|
76
|
+
const listeners = [];
|
|
77
|
+
for (const [index, entry] of value.entries()) {
|
|
78
|
+
if (!entry || typeof entry !== 'object') {
|
|
79
|
+
throw new Error(`http.listeners[${index}] must be an object`);
|
|
80
|
+
}
|
|
81
|
+
const listener = entry;
|
|
82
|
+
if (!parseEnabled(listener.enabled, `http.listeners[${index}].enabled`))
|
|
83
|
+
continue;
|
|
84
|
+
if (typeof listener.host !== 'string' || !listener.host) {
|
|
85
|
+
throw new Error(`http.listeners[${index}].host is required`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof listener.port !== 'number') {
|
|
88
|
+
throw new Error(`http.listeners[${index}].port is required`);
|
|
89
|
+
}
|
|
90
|
+
listeners.push(Object.freeze({
|
|
91
|
+
...primary,
|
|
92
|
+
host: listener.host,
|
|
93
|
+
port: listener.port,
|
|
94
|
+
tls: await parseTlsConfig(listener.tls, projectRoot),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
return Object.freeze(listeners);
|
|
98
|
+
}
|
|
99
|
+
async function parseTlsConfig(value, projectRoot) {
|
|
100
|
+
if (value === undefined || value === null || value === false)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (!value || typeof value !== 'object')
|
|
103
|
+
throw new Error('http.tls must be an object');
|
|
104
|
+
const tls = value;
|
|
105
|
+
const enabled = parseTlsEnabled(tls.enabled);
|
|
106
|
+
if (!enabled)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (typeof tls.keyFile !== 'string' || !tls.keyFile
|
|
109
|
+
|| typeof tls.certFile !== 'string' || !tls.certFile) {
|
|
110
|
+
throw new Error('http.tls.keyFile and http.tls.certFile are required');
|
|
111
|
+
}
|
|
112
|
+
const minVersion = tls.minVersion;
|
|
113
|
+
if (minVersion !== undefined
|
|
114
|
+
&& !['TLSv1.2', 'TLSv1.3'].includes(String(minVersion))) {
|
|
115
|
+
throw new Error('http.tls.minVersion must be TLSv1.2 or TLSv1.3');
|
|
116
|
+
}
|
|
117
|
+
const [key, cert, ca] = await Promise.all([
|
|
118
|
+
readFile(resolveTlsPath(projectRoot, tls.keyFile)),
|
|
119
|
+
readFile(resolveTlsPath(projectRoot, tls.certFile)),
|
|
120
|
+
typeof tls.caFile === 'string' && tls.caFile
|
|
121
|
+
? readFile(resolveTlsPath(projectRoot, tls.caFile)) : undefined,
|
|
122
|
+
]);
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
key, cert,
|
|
125
|
+
...(ca ? { ca } : {}),
|
|
126
|
+
...(typeof tls.passphrase === 'string' ? { passphrase: tls.passphrase } : {}),
|
|
127
|
+
...(minVersion ? { minVersion: minVersion } : {}),
|
|
128
|
+
...(typeof tls.ciphers === 'string' && tls.ciphers ? { ciphers: tls.ciphers } : {}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function parseTlsEnabled(value) {
|
|
132
|
+
return parseEnabled(value, 'http.tls.enabled');
|
|
133
|
+
}
|
|
134
|
+
function parseEnabled(value, field) {
|
|
135
|
+
if (value === undefined || value === true)
|
|
136
|
+
return true;
|
|
137
|
+
if (value === false)
|
|
138
|
+
return false;
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
const normalized = value.trim().toLowerCase();
|
|
141
|
+
if (['true', '1', 'on'].includes(normalized))
|
|
142
|
+
return true;
|
|
143
|
+
if (['false', '0', 'off', ''].includes(normalized))
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`${field} must be a boolean`);
|
|
147
|
+
}
|
|
148
|
+
function resolveTlsPath(projectRoot, value) {
|
|
149
|
+
return isAbsolute(value) ? value : resolve(projectRoot, value);
|
|
150
|
+
}
|
|
151
|
+
export function installHttpHost(host) {
|
|
152
|
+
return ({ resources, admission }) => {
|
|
153
|
+
const port = host;
|
|
154
|
+
resources.provide(httpHostToken, bindGenerationAdmission(port, admission));
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function parseScopedTokens(value) {
|
|
158
|
+
if (!Array.isArray(value))
|
|
159
|
+
return undefined;
|
|
160
|
+
const tokens = [];
|
|
161
|
+
for (const entry of value) {
|
|
162
|
+
if (!entry || typeof entry !== 'object')
|
|
163
|
+
continue;
|
|
164
|
+
const record = entry;
|
|
165
|
+
if (typeof record.token !== 'string' || !record.token)
|
|
166
|
+
continue;
|
|
167
|
+
// `user` is an alias of `demo` (an ordinary Console user); anything but `full` is never the operator.
|
|
168
|
+
const scope = record.scope === 'full' ? 'full' : 'demo';
|
|
169
|
+
const principalId = typeof record.principalId === 'string' && record.principalId.trim() === record.principalId
|
|
170
|
+
&& record.principalId.length > 0
|
|
171
|
+
? record.principalId
|
|
172
|
+
: undefined;
|
|
173
|
+
const permissions = parseAccessList(record.permissions);
|
|
174
|
+
const roles = parseAccessList(record.roles);
|
|
175
|
+
tokens.push(Object.freeze({
|
|
176
|
+
token: record.token,
|
|
177
|
+
scope,
|
|
178
|
+
...(principalId ? { principalId } : {}),
|
|
179
|
+
...(permissions ? { permissions } : {}),
|
|
180
|
+
...(roles ? { roles } : {}),
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
return tokens.length > 0 ? Object.freeze(tokens) : undefined;
|
|
184
|
+
}
|
|
185
|
+
/** Console permissions / roles of a scoped token; blank or non-string entries are dropped. */
|
|
186
|
+
function parseAccessList(value) {
|
|
187
|
+
if (!Array.isArray(value))
|
|
188
|
+
return undefined;
|
|
189
|
+
const result = value
|
|
190
|
+
.filter((item) => typeof item === 'string')
|
|
191
|
+
.map((item) => item.trim())
|
|
192
|
+
.filter((item) => item.length > 0);
|
|
193
|
+
return result.length > 0 ? Object.freeze([...new Set(result)]) : undefined;
|
|
194
|
+
}
|
|
195
|
+
function parseStringList(value) {
|
|
196
|
+
if (!Array.isArray(value))
|
|
197
|
+
return undefined;
|
|
198
|
+
const result = value.filter((item) => typeof item === 'string' && item.length > 0);
|
|
199
|
+
return result.length > 0 ? Object.freeze(result) : undefined;
|
|
200
|
+
}
|
|
201
|
+
async function readConfigDocument(config) {
|
|
202
|
+
if (!isConfigDocumentPort(config))
|
|
203
|
+
return config;
|
|
204
|
+
return (await config.read()).document;
|
|
205
|
+
}
|
|
206
|
+
function isConfigDocumentPort(value) {
|
|
207
|
+
if (!value || typeof value !== 'object')
|
|
208
|
+
return false;
|
|
209
|
+
const candidate = value;
|
|
210
|
+
return typeof candidate.read === 'function';
|
|
211
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
|
|
10
10
|
export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, type FileTreeNode, } from './project-files.js';
|
|
11
11
|
export { registerConsoleRestPages, type ConsoleAgentIntrospection, type ConsoleAgentRuntime, type ConsoleAgentSessionTree, type ConsoleRestCtx, type ConsoleRestPagesOptions, } from './console-rest-pages.js';
|
|
12
12
|
export { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx, type ConsoleScheduleEngine, type ConsoleWorkroomProfileControlPort, type ConsoleWorkroomKnowledgeControlPort, type ExtendedRpcResult, } from './console-rpc-extended/index.js';
|
|
13
|
+
export * from './host-installer.js';
|
package/lib/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
|
|
10
10
|
export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, } from './project-files.js';
|
|
11
11
|
export { registerConsoleRestPages, } from './console-rest-pages.js';
|
|
12
12
|
export { dispatchExtendedConsoleRpc, } from './console-rpc-extended/index.js';
|
|
13
|
+
export * from './host-installer.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/host-http",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"ws": "^8.21.1",
|
|
20
|
-
"@zhin.js/console-protocol": "1.1.
|
|
20
|
+
"@zhin.js/console-protocol": "1.1.14",
|
|
21
21
|
"@zhin.js/host-http-contract": "1.1.1",
|
|
22
22
|
"@zhin.js/logger": "1.1.2",
|
|
23
|
-
"@zhin.js/schedule": "1.1.
|
|
24
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
23
|
+
"@zhin.js/schedule": "1.1.4",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.22"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|