@ai-workbench/network 0.4.8 → 0.4.9
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/NOTICE.md +4 -3
- package/README.md +32 -20
- package/client/index.cjs +15 -33
- package/helpers/darwin-arm64/NOTICE.md +4 -3
- package/helpers/darwin-arm64/bin/aw-net +0 -0
- package/helpers/darwin-arm64/helper-manifest.json +1 -1
- package/helpers/darwin-arm64/licenses-manifest.json +4 -4
- package/helpers/darwin-arm64/package.json +1 -1
- package/helpers/darwin-x64/NOTICE.md +4 -3
- package/helpers/darwin-x64/bin/aw-net +0 -0
- package/helpers/darwin-x64/helper-manifest.json +1 -1
- package/helpers/darwin-x64/licenses-manifest.json +4 -4
- package/helpers/darwin-x64/package.json +1 -1
- package/lib/activation.js +66 -22
- package/lib/api.d.ts +0 -17
- package/lib/control.d.ts +0 -1
- package/lib/control.js +0 -32
- package/lib/employee-endpoint.js +5 -5
- package/lib/index.js +8 -10
- package/lib/network.d.ts +4 -2
- package/lib/network.js +67 -102
- package/lib/policy-management.d.ts +0 -37
- package/lib/policy-management.js +7 -59
- package/lib/private.d.ts +1 -1
- package/lib/private.js +6 -2
- package/lib/settings-host.js +1 -1
- package/lib/settings.d.ts +3 -3
- package/lib/settings.js +3 -3
- package/package.json +1 -1
- package/lib/download.d.ts +0 -2
- package/lib/download.js +0 -128
package/lib/private.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare function privateFile(path: string, write?: boolean): Promise<import("fs/promises").FileHandle>;
|
|
1
|
+
export declare function privateFile(path: string, write?: boolean, onDiscardCloseFailure?: () => void): Promise<import("fs/promises").FileHandle>;
|
|
2
2
|
export declare function privateSocket(path: string): Promise<void>;
|
|
3
3
|
export declare function verifyExecutable(path: string, sha256: string): Promise<void>;
|
package/lib/private.js
CHANGED
|
@@ -2,7 +2,7 @@ import { constants } from 'node:fs';
|
|
|
2
2
|
import { open, lstat } from 'node:fs/promises';
|
|
3
3
|
import { isAbsolute, dirname } from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
export async function privateFile(path, write = false) {
|
|
5
|
+
export async function privateFile(path, write = false, onDiscardCloseFailure) {
|
|
6
6
|
if (!isAbsolute(path))
|
|
7
7
|
throw new Error('LOCAL_PATH_INVALID');
|
|
8
8
|
await privateDirectory(dirname(path));
|
|
@@ -13,8 +13,12 @@ export async function privateFile(path, write = false) {
|
|
|
13
13
|
throw new Error('LOCAL_PERMISSION_DENIED');
|
|
14
14
|
return f;
|
|
15
15
|
}
|
|
16
|
+
// Optional diagnostic callers discard invalid handles without waiting on cleanup.
|
|
16
17
|
catch (e) {
|
|
17
|
-
|
|
18
|
+
if (onDiscardCloseFailure)
|
|
19
|
+
void f.close().catch(onDiscardCloseFailure);
|
|
20
|
+
else
|
|
21
|
+
await f.close();
|
|
18
22
|
throw e;
|
|
19
23
|
}
|
|
20
24
|
}
|
package/lib/settings-host.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createPolicyEditor, registerPolicyRoutes } from "./policy-management.js";
|
|
2
2
|
export const name = 'ai-workbench-network-settings';
|
|
3
|
-
//
|
|
3
|
+
// 伴生行不持有入网身份,只读取官方配置及当前状态,不提供保存入口。
|
|
4
4
|
export function apply(ctx) {
|
|
5
5
|
ctx.inject(['webServer', 'connection', 'configEditor'], host => {
|
|
6
6
|
host.effect(() => registerPolicyRoutes(host.get('webServer'), host.get('connection'), createPolicyEditor(host.get('configEditor'), () => {
|
package/lib/settings.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { PluginSettings, HelperConfig } from './api.ts';
|
|
2
|
-
export declare const helperPolicyFields: readonly ["statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes"
|
|
2
|
+
export declare const helperPolicyFields: readonly ["statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes"];
|
|
3
3
|
export declare const recoveryPolicyFields: readonly ["helperRecoveryMaxAttempts", "helperRecoveryDelaySeconds", "helperRecoveryStableSeconds"];
|
|
4
4
|
export declare const helperRecoveryDefaults: Readonly<{
|
|
5
5
|
helperRecoveryMaxAttempts: 3;
|
|
6
6
|
helperRecoveryDelaySeconds: 1;
|
|
7
7
|
helperRecoveryStableSeconds: 60;
|
|
8
8
|
}>;
|
|
9
|
-
export declare const pluginPolicyFields: readonly ["startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "
|
|
10
|
-
export declare const settingsFields: readonly ["configFile", "helperLogFile", "startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "
|
|
9
|
+
export declare const pluginPolicyFields: readonly ["startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "helperRecoveryMaxAttempts", "helperRecoveryDelaySeconds", "helperRecoveryStableSeconds"];
|
|
10
|
+
export declare const settingsFields: readonly ["configFile", "helperLogFile", "startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "helperRecoveryMaxAttempts", "helperRecoveryDelaySeconds", "helperRecoveryStableSeconds", "statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes"];
|
|
11
11
|
export declare const timerSeconds: (v: unknown) => v is number;
|
|
12
12
|
export declare const positiveInteger: (v: unknown) => v is number;
|
|
13
13
|
export declare function validRecoveryPolicy(settings: PluginSettings): boolean;
|
package/lib/settings.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export const helperPolicyFields = ['statusRefreshIntervalSeconds', 'dialTimeoutSeconds', 'headerTimeoutSeconds', 'enrollmentObservationSeconds', 'httpMaxHeaderBytes'
|
|
1
|
+
export const helperPolicyFields = ['statusRefreshIntervalSeconds', 'dialTimeoutSeconds', 'headerTimeoutSeconds', 'enrollmentObservationSeconds', 'httpMaxHeaderBytes'];
|
|
2
2
|
export const recoveryPolicyFields = ['helperRecoveryMaxAttempts', 'helperRecoveryDelaySeconds', 'helperRecoveryStableSeconds'];
|
|
3
3
|
export const helperRecoveryDefaults = Object.freeze({ helperRecoveryMaxAttempts: 3, helperRecoveryDelaySeconds: 1, helperRecoveryStableSeconds: 60 });
|
|
4
|
-
export const pluginPolicyFields = ['startupObservationSeconds', 'shutdownObservationSeconds', 'controlResponseMaxBytes',
|
|
4
|
+
export const pluginPolicyFields = ['startupObservationSeconds', 'shutdownObservationSeconds', 'controlResponseMaxBytes', ...recoveryPolicyFields];
|
|
5
5
|
export const settingsFields = ['configFile', 'helperLogFile', ...pluginPolicyFields, ...helperPolicyFields];
|
|
6
6
|
export const timerSeconds = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0.001 && v * 1000 <= 2147483647;
|
|
7
7
|
export const positiveInteger = (v) => Number.isSafeInteger(v) && Number(v) > 0;
|
|
@@ -17,7 +17,7 @@ export function validHelperPolicy(settings) {
|
|
|
17
17
|
const v = settings[key];
|
|
18
18
|
if (v === undefined)
|
|
19
19
|
return true;
|
|
20
|
-
if (key === 'httpMaxHeaderBytes'
|
|
20
|
+
if (key === 'httpMaxHeaderBytes')
|
|
21
21
|
return positiveInteger(v);
|
|
22
22
|
if (key === 'dialTimeoutSeconds' || key === 'statusRefreshIntervalSeconds')
|
|
23
23
|
return timerSeconds(v);
|
package/package.json
CHANGED
package/lib/download.d.ts
DELETED
package/lib/download.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import http from 'node:http';
|
|
2
|
-
import https from 'node:https';
|
|
3
|
-
import tls from 'node:tls';
|
|
4
|
-
import { isIP } from 'node:net';
|
|
5
|
-
import { Readable } from 'node:stream';
|
|
6
|
-
import { fail, ok } from "./api.js";
|
|
7
|
-
import { ObservationExpired, observationNeed } from "./observation.js";
|
|
8
|
-
function sourceURL(source) { const u = new URL(source); if (u.protocol !== 'https:' || u.username || u.password)
|
|
9
|
-
throw new Error('DOWNLOAD_SOURCE_INVALID'); return u; }
|
|
10
|
-
async function requestViaProxy(target, lease, signal, seconds, maxHeaderBytes) {
|
|
11
|
-
const proxy = new URL(lease.credentials.url);
|
|
12
|
-
if (proxy.protocol !== 'http:' || proxy.hostname !== '127.0.0.1' || proxy.username || proxy.password)
|
|
13
|
-
throw new Error('CONTROL_PROTOCOL_INVALID');
|
|
14
|
-
const hostname = target.hostname.replace(/^\[|\]$/g, '');
|
|
15
|
-
const authority = (isIP(hostname) === 6 ? '[' + hostname + ']' : hostname) + ':' + (target.port || '443');
|
|
16
|
-
const tunnel = await new Promise((resolve, reject) => {
|
|
17
|
-
const req = http.request({ hostname: proxy.hostname, port: proxy.port, method: 'CONNECT', path: authority, agent: false, signal, maxHeaderSize: maxHeaderBytes, headers: { 'Proxy-Authorization': 'Basic ' + Buffer.from(lease.credentials.username + ':' + lease.credentials.password).toString('base64') } });
|
|
18
|
-
const started = performance.now();
|
|
19
|
-
req.setTimeout(seconds * 1000, () => req.destroy(new ObservationExpired('DOWNLOAD_CONNECT_EXPIRED', 'dialTimeoutSeconds', seconds, started, '主机代理CONNECT响应头', '检查网络组件私有日志、主机SSH状态及代理握手;不重试或改走本机出口')));
|
|
20
|
-
req.on('connect', (response, socket, head) => { req.setTimeout(0); if (response.statusCode !== 200) {
|
|
21
|
-
socket.destroy();
|
|
22
|
-
reject(new Error('HOST_PROXY_REJECTED'));
|
|
23
|
-
return;
|
|
24
|
-
} if (head.length)
|
|
25
|
-
socket.unshift(head); resolve(socket); });
|
|
26
|
-
req.on('error', reject);
|
|
27
|
-
req.end();
|
|
28
|
-
});
|
|
29
|
-
// The only TCP socket supplied to TLS is the authenticated local proxy tunnel.
|
|
30
|
-
const agent = new class extends https.Agent {
|
|
31
|
-
createConnection(options) { return tls.connect({ ...options, socket: tunnel, servername: isIP(hostname) ? undefined : hostname }); }
|
|
32
|
-
}({ keepAlive: false });
|
|
33
|
-
try {
|
|
34
|
-
return await new Promise((resolve, reject) => {
|
|
35
|
-
const req = https.request(target, { agent, signal, maxHeaderSize: maxHeaderBytes }, response => { req.setTimeout(0); response.once('close', () => agent.destroy()); resolve(response); });
|
|
36
|
-
const started = performance.now();
|
|
37
|
-
req.setTimeout(seconds * 1000, () => req.destroy(new ObservationExpired('DOWNLOAD_HEADERS_EXPIRED', 'dialTimeoutSeconds', seconds, started, '经主机下载的TLS握手及HTTP响应头', '检查主机出口、TLS与下载源响应;不重试或静默换源')));
|
|
38
|
-
req.on('error', reject);
|
|
39
|
-
req.end();
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
tunnel.destroy();
|
|
44
|
-
agent.destroy();
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export async function downloadThroughProxy(source, lease, signal, maxRedirects, seconds, maxHeaderBytes) {
|
|
49
|
-
let current;
|
|
50
|
-
try {
|
|
51
|
-
current = sourceURL(source);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
await lease.release();
|
|
55
|
-
return fail('DOWNLOAD_SOURCE_INVALID', '通过远程主机下载只支持不含凭据的HTTPS地址', '管理员检查下载源');
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
for (let redirects = 0;; redirects++) {
|
|
59
|
-
signal.throwIfAborted();
|
|
60
|
-
const response = await requestViaProxy(current, lease, signal, seconds, maxHeaderBytes);
|
|
61
|
-
if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
62
|
-
response.destroy();
|
|
63
|
-
if (redirects >= maxRedirects)
|
|
64
|
-
throw new Error('DOWNLOAD_REDIRECT_LIMIT');
|
|
65
|
-
current = sourceURL(new URL(response.headers.location, current).href);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const headers = new Headers();
|
|
69
|
-
for (const [key, value] of Object.entries(response.headers))
|
|
70
|
-
if (value !== undefined) {
|
|
71
|
-
for (const v of Array.isArray(value) ? value : [value])
|
|
72
|
-
headers.append(key, v);
|
|
73
|
-
}
|
|
74
|
-
response.once('end', () => void lease.release());
|
|
75
|
-
response.once('error', () => void lease.release());
|
|
76
|
-
response.once('close', () => void lease.release());
|
|
77
|
-
const empty = [204, 205, 304].includes(response.statusCode);
|
|
78
|
-
let body = null;
|
|
79
|
-
if (!empty) {
|
|
80
|
-
const reader = Readable.toWeb(response).getReader();
|
|
81
|
-
let closed = false;
|
|
82
|
-
body = new ReadableStream({
|
|
83
|
-
async pull(controller) {
|
|
84
|
-
try {
|
|
85
|
-
const chunk = await reader.read();
|
|
86
|
-
if (closed)
|
|
87
|
-
return;
|
|
88
|
-
if (chunk.done) {
|
|
89
|
-
closed = true;
|
|
90
|
-
await lease.release();
|
|
91
|
-
controller.close();
|
|
92
|
-
}
|
|
93
|
-
else
|
|
94
|
-
controller.enqueue(chunk.value);
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
await lease.release();
|
|
98
|
-
if (!closed) {
|
|
99
|
-
closed = true;
|
|
100
|
-
controller.error(new Error('HOST_DOWNLOAD_STREAM_FAILED'));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
async cancel() { closed = true; try {
|
|
105
|
-
await reader.cancel();
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
await lease.release();
|
|
109
|
-
} },
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
const result = new Response(body, { status: response.statusCode, headers });
|
|
113
|
-
if (empty) {
|
|
114
|
-
response.resume();
|
|
115
|
-
await lease.release();
|
|
116
|
-
}
|
|
117
|
-
return ok(result);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
await lease.release();
|
|
122
|
-
const observed = observationNeed(error);
|
|
123
|
-
if (observed)
|
|
124
|
-
return { ok: false, error: observed };
|
|
125
|
-
const code = error instanceof Error && ['DOWNLOAD_REDIRECT_LIMIT', 'DOWNLOAD_SOURCE_INVALID'].includes(error.message) ? error.message : 'HOST_DOWNLOAD_FAILED';
|
|
126
|
-
return fail(code, '经主机下载失败,根因未查明', '检查主机出口、下载源和具名重定向上限;无本机直出替代');
|
|
127
|
-
}
|
|
128
|
-
}
|