orbit-codex-bridge 0.1.3 → 0.1.7
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/README.md +23 -5
- package/cli.mjs +20 -3
- package/lib/approvals.mjs +97 -0
- package/lib/client-files.mjs +37 -0
- package/lib/connector-service.mjs +7 -3
- package/lib/desktop.mjs +28 -25
- package/lib/resume-manager.mjs +209 -0
- package/lib/rpc.mjs +68 -23
- package/lib/store.mjs +6 -0
- package/lib/updates.mjs +97 -0
- package/package.json +19 -18
- package/relay-agent.mjs +2 -4
- package/scripts/verify-client-package.mjs +34 -0
- package/server.mjs +186 -142
- package/lib/device-auth.mjs +0 -69
- package/public/admin.css +0 -67
- package/public/admin.html +0 -23
- package/public/admin.js +0 -186
- package/public/app.js +0 -1248
- package/public/connect.html +0 -56
- package/public/connect.js +0 -126
- package/public/icon.svg +0 -1
- package/public/index.html +0 -58
- package/public/layout.css +0 -73
- package/public/manifest.webmanifest +0 -1
- package/public/style.css +0 -70
- package/relay-server.mjs +0 -703
- package/relay-store.mjs +0 -368
package/lib/rpc.mjs
CHANGED
|
@@ -3,7 +3,9 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
6
7
|
import WebSocket from 'ws';
|
|
8
|
+
import { approvalResponse } from './approvals.mjs';
|
|
7
9
|
|
|
8
10
|
export function findCodexBinary() {
|
|
9
11
|
if (process.env.ORBIT_CODEX_BIN) return process.env.ORBIT_CODEX_BIN;
|
|
@@ -25,6 +27,7 @@ export class CodexRpc extends EventEmitter {
|
|
|
25
27
|
this.tokenFile = tokenFile;
|
|
26
28
|
this.pending = new Map();
|
|
27
29
|
this.approvals = new Map();
|
|
30
|
+
this.approvalNamespace = crypto.randomUUID();
|
|
28
31
|
this.threads = new Map();
|
|
29
32
|
this.starting = new Set();
|
|
30
33
|
this.sequence = 0;
|
|
@@ -106,16 +109,21 @@ export class CodexRpc extends EventEmitter {
|
|
|
106
109
|
return;
|
|
107
110
|
}
|
|
108
111
|
if (message.id !== undefined && message.method) {
|
|
109
|
-
const supported = ['item/commandExecution/requestApproval','item/fileChange/requestApproval','item/tool/requestUserInput','item/permissions/requestApproval'];
|
|
112
|
+
const supported = ['item/commandExecution/requestApproval','item/fileChange/requestApproval','item/tool/requestUserInput','item/permissions/requestApproval','mcpServer/elicitation/request'];
|
|
110
113
|
if (!supported.includes(message.method) || !this.threads.has(message.params?.threadId)) {
|
|
111
114
|
this.send({ id: message.id, error: { code: -32601, message: 'This client does not implement the requested host tool.' } });
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
117
|
+
const requestId = `${this.approvalNamespace}:${message.id}`;
|
|
118
|
+
this.approvals.set(requestId, message);
|
|
119
|
+
this.emit('event', { method: 'orbit/approval', params: { ...message.params, requestId, method: message.method } });
|
|
116
120
|
return;
|
|
117
121
|
}
|
|
118
|
-
if (message.method === 'serverRequest/resolved')
|
|
122
|
+
if (message.method === 'serverRequest/resolved') {
|
|
123
|
+
const requestId = `${this.approvalNamespace}:${message.params?.requestId}`;
|
|
124
|
+
this.approvals.delete(requestId);
|
|
125
|
+
message = { ...message, params:{ ...message.params, requestId } };
|
|
126
|
+
}
|
|
119
127
|
if (message.method === 'turn/started' && this.threads.has(message.params?.threadId)) this.threads.get(message.params.threadId).turnId = message.params.turn.id;
|
|
120
128
|
if (message.method === 'turn/completed' && this.threads.has(message.params?.threadId)) {
|
|
121
129
|
const thread = this.threads.get(message.params.threadId);
|
|
@@ -130,25 +138,71 @@ export class CodexRpc extends EventEmitter {
|
|
|
130
138
|
if (this.threads.has(id)) return;
|
|
131
139
|
const result = await this.request('thread/resume', { threadId: id, excludeTurns: true, sandbox: writable ? 'workspace-write' : 'read-only', approvalPolicy: 'on-request' });
|
|
132
140
|
if (result.thread.id !== id) throw new Error('返回的会话 ID 不一致,已拒绝发送消息。');
|
|
133
|
-
this.threads.set(id, {
|
|
141
|
+
this.threads.set(id, {
|
|
142
|
+
turnId:null,lastCompletedTurnId:null,writable:Boolean(writable),
|
|
143
|
+
model:result.model||result.thread.model||null,
|
|
144
|
+
effort:result.reasoningEffort??result.thread.reasoningEffort??null,
|
|
145
|
+
collaborationMode:result.collaborationMode||{mode:'default',settings:{}}
|
|
146
|
+
});
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
async
|
|
149
|
+
async startThread({ cwd, model = null, projectId = null, writable = true } = {}) {
|
|
150
|
+
await this.connect();
|
|
151
|
+
const result = await this.request('thread/start', {
|
|
152
|
+
cwd,
|
|
153
|
+
projectId,
|
|
154
|
+
approvalPolicy: 'on-request',
|
|
155
|
+
sandbox: writable ? 'workspace-write' : 'read-only',
|
|
156
|
+
...(model ? { model } : {})
|
|
157
|
+
});
|
|
158
|
+
const thread = result?.thread;
|
|
159
|
+
if (typeof thread?.id !== 'string' || !/^[a-zA-Z0-9_-]{8,120}$/.test(thread.id)) {
|
|
160
|
+
throw new Error('Codex 未返回有效的新会话 ID。');
|
|
161
|
+
}
|
|
162
|
+
this.threads.set(thread.id, {
|
|
163
|
+
turnId:null,lastCompletedTurnId:null,writable:Boolean(writable),
|
|
164
|
+
model:result.model||thread.model||null,
|
|
165
|
+
effort:result.reasoningEffort??thread.reasoningEffort??null,
|
|
166
|
+
collaborationMode:result.collaborationMode||{mode:'default',settings:{}}
|
|
167
|
+
});
|
|
168
|
+
return { threadId:thread.id, model:result.model||thread.model||null, cwd:thread.cwd||cwd, projectId:thread.projectId||projectId };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async startTurn(id, text, writable, settings = null, options = {}) {
|
|
137
172
|
if (this.starting.has(id)) throw new Error('此会话正在发送,请勿并发提交。');
|
|
138
173
|
this.starting.add(id);
|
|
139
174
|
try {
|
|
140
175
|
await this.resume(id, writable);
|
|
141
176
|
const thread = this.threads.get(id);
|
|
142
177
|
if (thread.turnId) throw new Error('此会话仍在执行,请等待完成或停止后再发送。');
|
|
178
|
+
const baseMode=thread.collaborationMode||{mode:'default',settings:{}};
|
|
179
|
+
const collaborationMode={
|
|
180
|
+
mode:options.mode==='plan'?'plan':baseMode.mode==='plan'?'default':baseMode.mode||'default',
|
|
181
|
+
settings:{
|
|
182
|
+
...baseMode.settings,
|
|
183
|
+
model:settings?.model||baseMode.settings?.model||thread.model||'',
|
|
184
|
+
reasoning_effort:settings?.effort??baseMode.settings?.reasoning_effort??thread.effort??null,
|
|
185
|
+
developer_instructions:options.mode==='plan'||baseMode.mode==='plan'
|
|
186
|
+
? null
|
|
187
|
+
: baseMode.settings?.developer_instructions??null
|
|
188
|
+
}
|
|
189
|
+
};
|
|
143
190
|
const result = await this.request('turn/start', {
|
|
144
191
|
threadId: id, input: [{ type: 'text', text }], approvalPolicy: 'on-request',
|
|
145
192
|
sandboxPolicy: writable ? { type: 'workspaceWrite', networkAccess: false, excludeTmpdirEnvVar: true, excludeSlashTmp: true } : { type: 'readOnly' },
|
|
193
|
+
collaborationMode,
|
|
146
194
|
...(settings ? { model: settings.model, effort: settings.effort } : {})
|
|
147
195
|
});
|
|
148
196
|
const completed = thread.lastCompletedTurnId === result.turn.id;
|
|
149
197
|
thread.turnId = completed ? null : result.turn.id;
|
|
198
|
+
thread.collaborationMode=collaborationMode;
|
|
199
|
+
if(settings?.model)thread.model=settings.model;
|
|
200
|
+
if(settings&&Object.hasOwn(settings,'effort'))thread.effort=settings.effort;
|
|
150
201
|
return { threadId: id, turnId: result.turn.id, completed };
|
|
151
|
-
} finally {
|
|
202
|
+
} finally {
|
|
203
|
+
this.starting.delete(id);
|
|
204
|
+
this.emit('event', { method:'orbit/turn-settled', params:{ threadId:id } });
|
|
205
|
+
}
|
|
152
206
|
}
|
|
153
207
|
|
|
154
208
|
async interrupt(id) {
|
|
@@ -160,24 +214,10 @@ export class CodexRpc extends EventEmitter {
|
|
|
160
214
|
replyApproval(id, body) {
|
|
161
215
|
const request = this.approvals.get(String(id));
|
|
162
216
|
if (!request) throw new Error('审批已结束或不属于当前桥接实例。');
|
|
163
|
-
|
|
164
|
-
if (request.method === 'item/tool/requestUserInput') {
|
|
165
|
-
const answers = {};
|
|
166
|
-
for (const question of request.params.questions || []) {
|
|
167
|
-
const value = body.answers?.[question.id];
|
|
168
|
-
if (typeof value !== 'string' || !value.trim() || value.length > 4000) throw new Error('请回答所有问题。');
|
|
169
|
-
answers[question.id] = { answers: [value] };
|
|
170
|
-
}
|
|
171
|
-
result = { answers };
|
|
172
|
-
} else if (request.method === 'item/permissions/requestApproval') {
|
|
173
|
-
if (body.decision !== 'decline') throw new Error('首版暂不支持扩大权限,请在本机处理。');
|
|
174
|
-
result = { permissions: {}, scope: 'turn' };
|
|
175
|
-
} else {
|
|
176
|
-
if (!['accept','decline'].includes(body.decision)) throw new Error('无效审批决定。');
|
|
177
|
-
result = { decision: body.decision };
|
|
178
|
-
}
|
|
217
|
+
const result = approvalResponse(request, body);
|
|
179
218
|
this.send({ id: request.id, result });
|
|
180
219
|
this.approvals.delete(String(id));
|
|
220
|
+
this.emit('event', { method:'serverRequest/resolved', params:{ threadId:request.params.threadId, requestId:String(id) } });
|
|
181
221
|
return { resolved: true };
|
|
182
222
|
}
|
|
183
223
|
|
|
@@ -198,6 +238,11 @@ export class CodexRpc extends EventEmitter {
|
|
|
198
238
|
}
|
|
199
239
|
|
|
200
240
|
close() {
|
|
241
|
+
const child = this.child;
|
|
242
|
+
const exited = !child || child.exitCode !== null || child.signalCode !== null
|
|
243
|
+
? Promise.resolve()
|
|
244
|
+
: new Promise(resolve => child.once('exit', resolve));
|
|
201
245
|
this.disconnect(new Error('服务已停止。'));
|
|
246
|
+
return exited;
|
|
202
247
|
}
|
|
203
248
|
}
|
package/lib/store.mjs
CHANGED
|
@@ -92,6 +92,7 @@ export class NativeStore {
|
|
|
92
92
|
id: row.id, title: row.name || row.title || '未命名会话',
|
|
93
93
|
preview: cleanText(row.preview || row.first_user_message || '').slice(0, 220), cwd,
|
|
94
94
|
projectId: row.project_id || cwd || 'ungrouped', projectName: projects.get(row.project_id)?.name || leaf,
|
|
95
|
+
nativeProjectId: row.project_id || null,
|
|
95
96
|
source: row.source, provider: row.model_provider, model: row.model || null,
|
|
96
97
|
archived: Boolean(row.archived), pinned: Boolean(row.is_pinned), locked: this.locked(row.id),
|
|
97
98
|
createdAt: row.created_at_ms || row.created_at * 1000, updatedAt: row.updated_at_ms || row.updated_at * 1000,
|
|
@@ -103,6 +104,11 @@ export class NativeStore {
|
|
|
103
104
|
try { projects = new Map(this.state.prepare('SELECT id,name FROM projects').all().map(project => [project.id, project])); } catch {}
|
|
104
105
|
return this.state.prepare('SELECT * FROM threads ORDER BY updated_at DESC').all().map(row => this.summary(row, projects));
|
|
105
106
|
}
|
|
107
|
+
projectWorkspaces() {
|
|
108
|
+
try {
|
|
109
|
+
return this.state.prepare('SELECT projects.id AS projectId,projects.name AS label,project_roots.path AS cwd FROM projects JOIN project_roots ON project_roots.project_id=projects.id ORDER BY projects.position,project_roots.position').all();
|
|
110
|
+
} catch { return []; }
|
|
111
|
+
}
|
|
106
112
|
historyFor(id, before = null, limit = 60) {
|
|
107
113
|
const row = this.row(id);
|
|
108
114
|
if (!row) return null;
|
package/lib/updates.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const registryUrl = 'https://registry.npmjs.org/orbit-codex-bridge/latest';
|
|
2
|
+
const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)?$/;
|
|
3
|
+
|
|
4
|
+
function parseVersion(value) {
|
|
5
|
+
if (typeof value !== 'string' || value.length > 120) throw new Error('版本格式无效。');
|
|
6
|
+
const match = value.match(versionPattern);
|
|
7
|
+
if (!match) throw new Error('版本格式无效。');
|
|
8
|
+
return { core:match.slice(1, 4).map(part => BigInt(part)), prerelease:match[4]?.split('.') || [] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function compareVersions(left, right) {
|
|
12
|
+
const first = parseVersion(left);
|
|
13
|
+
const second = parseVersion(right);
|
|
14
|
+
for (let index = 0; index < 3; index++) {
|
|
15
|
+
if (first.core[index] !== second.core[index]) return first.core[index] > second.core[index] ? 1 : -1;
|
|
16
|
+
}
|
|
17
|
+
if (!first.prerelease.length || !second.prerelease.length) {
|
|
18
|
+
return first.prerelease.length === second.prerelease.length ? 0 : first.prerelease.length ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
for (let index = 0; index < Math.max(first.prerelease.length, second.prerelease.length); index++) {
|
|
21
|
+
const firstPart = first.prerelease[index];
|
|
22
|
+
const secondPart = second.prerelease[index];
|
|
23
|
+
if (firstPart === secondPart) continue;
|
|
24
|
+
if (firstPart === undefined || secondPart === undefined) return firstPart === undefined ? -1 : 1;
|
|
25
|
+
const firstNumeric = /^\d+$/.test(firstPart);
|
|
26
|
+
const secondNumeric = /^\d+$/.test(secondPart);
|
|
27
|
+
if (firstNumeric !== secondNumeric) return firstNumeric ? -1 : 1;
|
|
28
|
+
return firstNumeric ? BigInt(firstPart) > BigInt(secondPart) ? 1 : -1 : firstPart > secondPart ? 1 : -1;
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function updateCommand(platform = process.platform) {
|
|
34
|
+
return `${platform === 'win32' ? 'npx.cmd' : 'npx'} --yes --package=orbit-codex-bridge@latest orbit login`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readMetadata(response) {
|
|
38
|
+
if (!response.ok || !response.body) throw new Error('更新服务暂不可用。');
|
|
39
|
+
const reader = response.body.getReader();
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
bytes += value.byteLength;
|
|
47
|
+
if (bytes > 256 * 1024) throw new Error('版本信息过大。');
|
|
48
|
+
chunks.push(Buffer.from(value));
|
|
49
|
+
}
|
|
50
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
51
|
+
} finally { await reader.cancel().catch(() => {}); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createUpdateChecker(currentVersion, {
|
|
55
|
+
fetchImpl = globalThis.fetch,
|
|
56
|
+
now = Date.now,
|
|
57
|
+
platform = process.platform,
|
|
58
|
+
timeout = 5000,
|
|
59
|
+
cacheLifetime = 6 * 60 * 60 * 1000,
|
|
60
|
+
failureLifetime = 5 * 60 * 1000,
|
|
61
|
+
minimumInterval = 60000
|
|
62
|
+
} = {}) {
|
|
63
|
+
parseVersion(currentVersion);
|
|
64
|
+
let result = { currentVersion, latestVersion:null, available:false, status:'unchecked', checkedAt:null, command:updateCommand(platform) };
|
|
65
|
+
let inFlight = null;
|
|
66
|
+
const snapshot = () => ({ ...result });
|
|
67
|
+
async function check({ force = false } = {}) {
|
|
68
|
+
if (inFlight) return inFlight;
|
|
69
|
+
const age = result.checkedAt === null ? Infinity : Math.max(0, now() - result.checkedAt);
|
|
70
|
+
const lifetime = force ? minimumInterval : result.status === 'unavailable' ? failureLifetime : cacheLifetime;
|
|
71
|
+
if (age < lifetime) return snapshot();
|
|
72
|
+
inFlight = (async () => {
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetchImpl(registryUrl, {
|
|
75
|
+
headers:{ Accept:'application/json' }, signal:AbortSignal.timeout(timeout), redirect:'error', cache:'no-store'
|
|
76
|
+
});
|
|
77
|
+
const metadata = await readMetadata(response);
|
|
78
|
+
if (metadata.name !== 'orbit-codex-bridge') throw new Error('包信息不一致。');
|
|
79
|
+
const comparison = compareVersions(metadata.version, currentVersion);
|
|
80
|
+
result = {
|
|
81
|
+
currentVersion, latestVersion:metadata.version, available:comparison > 0,
|
|
82
|
+
status:comparison > 0 ? 'update-available' : comparison < 0 ? 'ahead' : 'up-to-date',
|
|
83
|
+
checkedAt:now(), command:updateCommand(platform)
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
result = {
|
|
87
|
+
currentVersion, latestVersion:null, available:false, status:'unavailable', checkedAt:now(),
|
|
88
|
+
command:updateCommand(platform), message:'暂时无法检查更新,请稍后重试。'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return snapshot();
|
|
92
|
+
})();
|
|
93
|
+
try { return await inFlight; }
|
|
94
|
+
finally { inFlight = null; }
|
|
95
|
+
}
|
|
96
|
+
return { check, snapshot };
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orbit-codex-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Connect your original Codex
|
|
5
|
+
"description": "Connect your computer and original Codex conversations to Orbit.",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24"
|
|
8
8
|
},
|
|
@@ -14,25 +14,26 @@
|
|
|
14
14
|
"orbit",
|
|
15
15
|
"server.mjs",
|
|
16
16
|
"relay-agent.mjs",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"lib/",
|
|
20
|
-
"
|
|
17
|
+
"lib/approvals.mjs",
|
|
18
|
+
"lib/client-files.mjs",
|
|
19
|
+
"lib/connector-service.mjs",
|
|
20
|
+
"lib/desktop.mjs",
|
|
21
|
+
"lib/resume-manager.mjs",
|
|
22
|
+
"lib/rpc.mjs",
|
|
23
|
+
"lib/store.mjs",
|
|
24
|
+
"lib/updates.mjs",
|
|
25
|
+
"scripts/verify-client-package.mjs"
|
|
21
26
|
],
|
|
22
27
|
"scripts": {
|
|
23
|
-
"start": "node
|
|
24
|
-
"
|
|
25
|
-
"relay:agent": "node --env-file-if-exists=.env.local relay-agent.mjs",
|
|
26
|
-
"prepublishOnly": "npm run check && npm test",
|
|
27
|
-
"test": "node --test test/*.test.mjs",
|
|
28
|
-
"test:browser": "node --test test/browser/*.test.mjs",
|
|
29
|
-
"test:native": "node test/native-smoke.mjs",
|
|
30
|
-
"check": "node --check cli.mjs && node --check server.mjs && node --check lib/store.mjs && node --check lib/rpc.mjs && node --check lib/desktop.mjs && node --check lib/device-auth.mjs && node --check lib/connector-service.mjs && node --check public/app.js && node --check public/connect.js && node --check relay-server.mjs && node --check relay-store.mjs && node --check relay-agent.mjs && node --check public/admin.js"
|
|
28
|
+
"start": "node server.mjs",
|
|
29
|
+
"prepublishOnly": "node scripts/verify-client-package.mjs"
|
|
31
30
|
},
|
|
32
31
|
"dependencies": {
|
|
33
|
-
"dompurify": "^3.3.0",
|
|
34
|
-
"marked": "^17.0.0",
|
|
35
|
-
"qrcode": "^1.5.4",
|
|
36
32
|
"ws": "^8.18.3"
|
|
37
|
-
}
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://registry.npmjs.org/",
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"orbitDistribution": "client"
|
|
38
39
|
}
|
package/relay-agent.mjs
CHANGED
|
@@ -69,9 +69,7 @@ export function createRelayAgent(options = {}) {
|
|
|
69
69
|
requests.delete(request);
|
|
70
70
|
resolve({statusCode:response.statusCode || 502,headers:{
|
|
71
71
|
'content-type':response.headers['content-type'] || 'application/json',
|
|
72
|
-
...(response.headers['set-cookie'] ? {'set-cookie':response.headers['set-cookie']} : {})
|
|
73
|
-
...Object.fromEntries(['content-disposition','content-length','accept-ranges','content-range','etag']
|
|
74
|
-
.filter(name=>typeof response.headers[name]==='string').map(name=>[name,response.headers[name]]))
|
|
72
|
+
...(response.headers['set-cookie'] ? {'set-cookie':response.headers['set-cookie']} : {})
|
|
75
73
|
},body:Buffer.concat(chunks)});
|
|
76
74
|
});
|
|
77
75
|
});
|
|
@@ -99,7 +97,7 @@ export function createRelayAgent(options = {}) {
|
|
|
99
97
|
const body = Buffer.from(message.body || '', 'base64');
|
|
100
98
|
if (body.length > maxBody) return;
|
|
101
99
|
const headers = {};
|
|
102
|
-
for (const name of ['accept','content-type','cookie','user-agent'
|
|
100
|
+
for (const name of ['accept','content-type','cookie','user-agent']) {
|
|
103
101
|
const value = message.headers?.[name];
|
|
104
102
|
if (typeof value === 'string') headers[name] = value;
|
|
105
103
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { clientFiles, clientManifest } from '../lib/client-files.mjs';
|
|
6
|
+
|
|
7
|
+
export function verifyClientPackage(directory) {
|
|
8
|
+
const root=fs.realpathSync(directory);
|
|
9
|
+
const manifest=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
|
|
10
|
+
if (!isDeepStrictEqual(manifest,clientManifest(manifest))) throw new Error('客户端 package.json 不符合发布白名单。');
|
|
11
|
+
const allowed=new Set(['package.json','package-lock.json','README.md',...clientFiles]);
|
|
12
|
+
const found=new Set();
|
|
13
|
+
const visit=(current,prefix='')=>{
|
|
14
|
+
for(const entry of fs.readdirSync(current,{withFileTypes:true})){
|
|
15
|
+
if(!prefix&&entry.name==='node_modules'){
|
|
16
|
+
if(!entry.isDirectory()||entry.isSymbolicLink())throw new Error('node_modules 必须是普通目录。');
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const relative=prefix+entry.name;
|
|
20
|
+
if(entry.isSymbolicLink())throw new Error(`客户端发布目录不允许符号链接:${relative}`);
|
|
21
|
+
if(entry.isDirectory())visit(path.join(current,entry.name),`${relative}/`);
|
|
22
|
+
else if(entry.isFile()&&allowed.has(relative))found.add(relative);
|
|
23
|
+
else throw new Error(`客户端包含非白名单文件:${relative}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
visit(root);
|
|
27
|
+
for(const filename of ['package.json','README.md',...clientFiles])if(!found.has(filename))throw new Error(`客户端缺少文件:${filename}`);
|
|
28
|
+
return {version:manifest.version,files:[...found].sort()};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
|
|
32
|
+
const result=verifyClientPackage(path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'));
|
|
33
|
+
console.log(`Client package verified: ${result.version} · ${result.files.length} files`);
|
|
34
|
+
}
|