@cjhyy/code-shell-server 0.9.8 → 0.9.10

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.
Files changed (111) hide show
  1. package/README.md +85 -23
  2. package/dist/bin/code-shell-project-runtime.d.ts +2 -0
  3. package/dist/bin/code-shell-project-runtime.js +8 -0
  4. package/dist/desktop-web/http-api.d.ts +43 -0
  5. package/dist/desktop-web/http-api.js +376 -0
  6. package/dist/hub/approval-lease.d.ts +30 -0
  7. package/dist/hub/approval-lease.js +89 -0
  8. package/dist/hub/auth-http.d.ts +23 -0
  9. package/dist/hub/auth-http.js +259 -0
  10. package/dist/hub/auth-store.d.ts +52 -0
  11. package/dist/hub/auth-store.js +316 -0
  12. package/dist/hub/configuration.d.ts +64 -0
  13. package/dist/hub/configuration.js +555 -0
  14. package/dist/hub/files.d.ts +9 -0
  15. package/dist/hub/files.js +278 -0
  16. package/dist/hub/mcp-configuration.d.ts +51 -0
  17. package/dist/hub/mcp-configuration.js +558 -0
  18. package/dist/hub/mcp-probe.d.ts +24 -0
  19. package/dist/hub/mcp-probe.js +392 -0
  20. package/dist/hub/model-probe.d.ts +20 -0
  21. package/dist/hub/model-probe.js +193 -0
  22. package/dist/hub/outbound-transport.d.ts +36 -0
  23. package/dist/hub/outbound-transport.js +122 -0
  24. package/dist/hub/run-replay.d.ts +42 -0
  25. package/dist/hub/run-replay.js +89 -0
  26. package/dist/hub/session-management.d.ts +36 -0
  27. package/dist/hub/session-management.js +405 -0
  28. package/dist/hub/skills-management.d.ts +43 -0
  29. package/dist/hub/skills-management.js +487 -0
  30. package/dist/hub/uploads.d.ts +22 -0
  31. package/dist/hub/uploads.js +241 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.desktop-web.d.ts +2 -0
  34. package/dist/index.desktop-web.js +2 -0
  35. package/dist/index.js +3 -0
  36. package/dist/index.links.d.ts +6 -0
  37. package/dist/index.links.js +6 -0
  38. package/dist/index.panels.d.ts +9 -0
  39. package/dist/index.panels.js +8 -0
  40. package/dist/index.serve.d.ts +1 -0
  41. package/dist/index.serve.js +1 -0
  42. package/dist/index.storage.d.ts +2 -0
  43. package/dist/index.storage.js +2 -0
  44. package/dist/links/catalog.d.ts +10 -0
  45. package/dist/links/catalog.js +41 -0
  46. package/dist/links/device-oauth.d.ts +52 -0
  47. package/dist/links/device-oauth.js +255 -0
  48. package/dist/links/http.d.ts +34 -0
  49. package/dist/links/http.js +144 -0
  50. package/dist/links/service.d.ts +54 -0
  51. package/dist/links/service.js +578 -0
  52. package/dist/links/types.d.ts +6 -0
  53. package/dist/links/types.js +1 -0
  54. package/dist/mobile-remote/mobile-client-event-validator.js +5 -2
  55. package/dist/mobile-remote/pending-approvals.js +2 -0
  56. package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
  57. package/dist/mobile-remote/remote-host-manager.js +90 -15
  58. package/dist/mobile-remote/room-manager.js +17 -5
  59. package/dist/panels/agent-task-host.d.ts +32 -0
  60. package/dist/panels/agent-task-host.js +457 -0
  61. package/dist/panels/agent-task-models.d.ts +27 -0
  62. package/dist/panels/agent-task-models.js +51 -0
  63. package/dist/panels/agent-task-service.d.ts +105 -0
  64. package/dist/panels/agent-task-service.js +283 -0
  65. package/dist/panels/http.d.ts +34 -0
  66. package/dist/panels/http.js +37 -0
  67. package/dist/panels/hub-binding.d.ts +12 -0
  68. package/dist/panels/hub-binding.js +174 -0
  69. package/dist/panels/management-http.d.ts +29 -0
  70. package/dist/panels/management-http.js +119 -0
  71. package/dist/panels/management.d.ts +39 -0
  72. package/dist/panels/management.js +494 -0
  73. package/dist/panels/process-files.d.ts +14 -0
  74. package/dist/panels/process-files.js +299 -0
  75. package/dist/panels/process-service.d.ts +96 -0
  76. package/dist/panels/process-service.js +501 -0
  77. package/dist/panels/runtime-services.d.ts +24 -0
  78. package/dist/panels/runtime-services.js +462 -0
  79. package/dist/panels/runtime.d.ts +51 -0
  80. package/dist/panels/runtime.js +1063 -0
  81. package/dist/panels/storage-store.d.ts +9 -0
  82. package/dist/panels/storage-store.js +154 -0
  83. package/dist/panels/types.d.ts +52 -0
  84. package/dist/panels/types.js +1 -0
  85. package/dist/project-runtime/control-server.d.ts +19 -0
  86. package/dist/project-runtime/control-server.js +300 -0
  87. package/dist/project-runtime/docker-provider.d.ts +33 -0
  88. package/dist/project-runtime/docker-provider.js +487 -0
  89. package/dist/project-runtime/lock.d.ts +6 -0
  90. package/dist/project-runtime/lock.js +43 -0
  91. package/dist/project-runtime/managed-entry.d.ts +10 -0
  92. package/dist/project-runtime/managed-entry.js +113 -0
  93. package/dist/project-runtime/manager.d.ts +40 -0
  94. package/dist/project-runtime/manager.js +249 -0
  95. package/dist/project-runtime/proxy.d.ts +22 -0
  96. package/dist/project-runtime/proxy.js +706 -0
  97. package/dist/project-runtime/registry.d.ts +54 -0
  98. package/dist/project-runtime/registry.js +286 -0
  99. package/dist/project-runtime/types.d.ts +43 -0
  100. package/dist/project-runtime/types.js +1 -0
  101. package/dist/serve/cli.d.ts +5 -0
  102. package/dist/serve/cli.js +82 -23
  103. package/dist/serve/headless-server.d.ts +11 -0
  104. package/dist/serve/headless-server.js +737 -83
  105. package/dist/session-titles-store.d.ts +17 -0
  106. package/dist/session-titles-store.js +174 -0
  107. package/dist/sessions-service.d.ts +2 -0
  108. package/dist/sessions-service.js +8 -0
  109. package/dist/static-files.js +1 -0
  110. package/dist/worker-bridge-core.js +17 -6
  111. package/package.json +19 -5
@@ -0,0 +1,555 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { SettingsManager, invalidateSkillCache, mergePluginMcpServers, scanSkills, userHome, } from "@cjhyy/code-shell-core";
4
+ import { computeEffectiveDisabledLists, getMergedCatalog } from "@cjhyy/code-shell-core/internal";
5
+ import { probeConfiguredModel } from "./model-probe.js";
6
+ const ROOT = "/api/v1/configuration";
7
+ const MAX_BODY_BYTES = 64 * 1024;
8
+ const MAX_SKILL_BYTES = 2 * 1024 * 1024;
9
+ const MAX_CONNECTIONS = 512;
10
+ const FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
11
+ export class HubConfigurationError extends Error {
12
+ status;
13
+ constructor(status, message) {
14
+ super(message);
15
+ this.status = status;
16
+ this.name = "HubConfigurationError";
17
+ }
18
+ }
19
+ /** The caller must authenticate and check Origin before dispatching any request here. */
20
+ export function createHubConfiguration(options) {
21
+ const settingsManager = () => new SettingsManager(options.cwd, "full");
22
+ let closed = false;
23
+ const probes = new Set();
24
+ const probeOwners = new Map();
25
+ const namedProbes = new Map();
26
+ const cancelledProbes = new Map();
27
+ function pruneCancelled() {
28
+ for (const [id, expires] of cancelledProbes)
29
+ if (expires <= Date.now())
30
+ cancelledProbes.delete(id);
31
+ while (cancelledProbes.size > 256)
32
+ cancelledProbes.delete(cancelledProbes.keys().next().value);
33
+ }
34
+ const connectionRevisions = new Map();
35
+ async function authorize(req) {
36
+ if (closed)
37
+ throw new HubConfigurationError(503, "服务正在关闭。");
38
+ if (options.isAuthorized && !(await options.isAuthorized(req)))
39
+ throw new HubConfigurationError(401, "login required");
40
+ if (closed)
41
+ throw new HubConfigurationError(503, "服务正在关闭。");
42
+ }
43
+ async function ownerFor(req) {
44
+ const owner = options.ownerId ? await options.ownerId(req) : "administrator";
45
+ if (!owner)
46
+ throw new HubConfigurationError(401, "login required");
47
+ if (closed)
48
+ throw new HubConfigurationError(503, "服务正在关闭。");
49
+ return owner;
50
+ }
51
+ function abortProbe(controller) {
52
+ controller.abort();
53
+ probes.delete(controller);
54
+ probeOwners.delete(controller);
55
+ for (const [id, probe] of namedProbes)
56
+ if (probe === controller)
57
+ namedProbes.delete(id);
58
+ }
59
+ function revisionFor(connection, settings) {
60
+ const template = getMergedCatalog().find((entry) => entry.id === connection.catalogId);
61
+ const credential = settings.credentials.find((entry) => entry.id === connection.credentialId);
62
+ // Only an opaque random revision leaves the server; credential hashes stay private.
63
+ const signature = createHash("sha256")
64
+ .update(JSON.stringify({
65
+ connection,
66
+ credential,
67
+ template: {
68
+ protocol: template?.protocol,
69
+ adapterKind: template?.adapterKind,
70
+ defaultBaseUrl: template?.defaultBaseUrl,
71
+ },
72
+ }))
73
+ .digest("hex");
74
+ let revision = connectionRevisions.get(connection.id);
75
+ if (revision?.signature !== signature) {
76
+ revision = { signature, revision: randomUUID() };
77
+ connectionRevisions.set(connection.id, revision);
78
+ }
79
+ return revision.revision;
80
+ }
81
+ function checkConnectionRevision(input, id, settings) {
82
+ if (input.expectedRevision === undefined)
83
+ return;
84
+ const existing = settings.modelConnections.find((entry) => entry.id === id);
85
+ if (input.expectedRevision === null) {
86
+ if (existing)
87
+ throw new HubConfigurationError(409, "连接名称已存在,请更换名称或编辑现有连接。");
88
+ return;
89
+ }
90
+ const expected = boundedString(input.expectedRevision, "expectedRevision", 64);
91
+ if (!existing || revisionFor(existing, settings) !== expected)
92
+ throw new HubConfigurationError(409, "这个连接已被其他设备修改或删除。请保留草稿,载入最新设置后再保存。");
93
+ }
94
+ function snapshot() {
95
+ const manager = settingsManager();
96
+ const settings = manager.get();
97
+ const connectionIds = new Set(settings.modelConnections.map((item) => item.id));
98
+ for (const id of connectionRevisions.keys())
99
+ if (!connectionIds.has(id))
100
+ connectionRevisions.delete(id);
101
+ const catalog = getMergedCatalog();
102
+ const disabled = computeEffectiveDisabledLists(manager, options.cwd);
103
+ invalidateSkillCache();
104
+ const allSkills = scanSkills(options.cwd);
105
+ const enabledSkills = new Set(scanSkills(options.cwd, disabled).map((skill) => skill.name));
106
+ const mcpServers = mergePluginMcpServers(settings.mcpServers, disabled.disabledPlugins, settings.mcpServerOverrides);
107
+ return {
108
+ workspace: {
109
+ path: options.cwd,
110
+ settingsScope: "local",
111
+ skillDirectories: [
112
+ join(options.cwd, ".code-shell", "skills"),
113
+ join(options.cwd, ".agents", "skills"),
114
+ join(userHome(), ".code-shell", "skills"),
115
+ ],
116
+ },
117
+ defaults: {
118
+ ...(settings.defaults.text ? { text: settings.defaults.text } : {}),
119
+ ...(settings.defaults.auxText ? { auxText: settings.defaults.auxText } : {}),
120
+ },
121
+ connections: settings.modelConnections.map((connection) => {
122
+ const template = catalog.find((entry) => entry.id === connection.catalogId);
123
+ const credential = settings.credentials.find((entry) => entry.id === connection.credentialId);
124
+ return {
125
+ id: connection.id,
126
+ revision: revisionFor(connection, settings),
127
+ catalogId: connection.catalogId,
128
+ tag: connection.tag,
129
+ model: connection.model,
130
+ baseUrl: displayUrl(connection.baseUrl ?? credential?.baseUrl ?? template?.defaultBaseUrl),
131
+ hasApiKey: Boolean(credential?.apiKey),
132
+ needsKey: template?.needsKey !== false,
133
+ };
134
+ }),
135
+ catalog: catalog
136
+ .filter((entry) => entry.tag === "text")
137
+ .map((entry) => ({
138
+ id: entry.id,
139
+ displayName: entry.displayName,
140
+ adapterKind: entry.adapterKind,
141
+ defaultBaseUrl: displayUrl(entry.defaultBaseUrl),
142
+ needsKey: entry.needsKey !== false,
143
+ })),
144
+ skills: allSkills.map((skill) => ({
145
+ name: skill.name,
146
+ description: skill.description,
147
+ source: skill.source,
148
+ enabled: enabledSkills.has(skill.name),
149
+ ...(skill.source === "plugin" &&
150
+ disabled.disabledPlugins.includes(skill.name.split(":", 1)[0])
151
+ ? { disabledReason: "所属插件已停用,请先在服务端启用插件。" }
152
+ : {}),
153
+ })),
154
+ mcpServers: Object.entries(mcpServers).map(([name, server]) => ({
155
+ name,
156
+ transport: String(server.transport ?? (server.url ? "streamable-http" : "stdio")),
157
+ enabled: server.enabled !== false,
158
+ })),
159
+ restartRequired: false,
160
+ };
161
+ }
162
+ function readSkill(name) {
163
+ const selectedName = boundedString(name, "name", 256);
164
+ invalidateSkillCache();
165
+ // Resolve only from the same catalog the engine uses. A client never supplies a path.
166
+ const skill = scanSkills(options.cwd).find((entry) => entry.name === selectedName);
167
+ if (!skill)
168
+ throw new HubConfigurationError(404, "skill not found");
169
+ if (Buffer.byteLength(skill.content) > MAX_SKILL_BYTES) {
170
+ throw new HubConfigurationError(413, "skill is too large to display");
171
+ }
172
+ return { name: skill.name, content: skill.content };
173
+ }
174
+ function updateDefaults(input) {
175
+ knownFields(input, ["text", "auxText"]);
176
+ const text = boundedString(input.text, "text", 128);
177
+ const auxText = input.auxText === undefined || input.auxText === null
178
+ ? input.auxText
179
+ : boundedString(input.auxText, "auxText", 128);
180
+ const manager = settingsManager();
181
+ manager.mutateSettingsForScope("local", options.cwd, (current) => {
182
+ const effective = settingsManager().get();
183
+ requireTextConnection(effective, text);
184
+ if (auxText)
185
+ requireTextConnection(effective, auxText);
186
+ current.defaults = {
187
+ ...objectOrEmpty(current.defaults),
188
+ text,
189
+ // Empty means the core background-model resolver follows the main model.
190
+ ...(auxText !== undefined ? { auxText: auxText ?? "" } : {}),
191
+ };
192
+ });
193
+ }
194
+ function updateConnection(input) {
195
+ knownFields(input, ["id", "catalogId", "model", "baseUrl", "apiKey", "expectedRevision"]);
196
+ const id = boundedString(input.id, "id", 128);
197
+ const catalogId = boundedString(input.catalogId, "catalogId", 128);
198
+ const model = boundedString(input.model, "model", 256);
199
+ const baseUrl = input.baseUrl === undefined ? undefined : input.baseUrl === "" ? "" : inputUrl(input.baseUrl);
200
+ const apiKey = input.apiKey === undefined ? undefined : boundedString(input.apiKey, "apiKey", 16_384);
201
+ const template = getMergedCatalog().find((entry) => entry.id === catalogId && entry.tag === "text");
202
+ if (!template)
203
+ throw new HubConfigurationError(400, "unknown text provider");
204
+ settingsManager().mutateSettingsForScope("local", options.cwd, (current) => {
205
+ const effective = settingsManager().get();
206
+ checkConnectionRevision(input, id, effective);
207
+ const existing = effective.modelConnections.find((entry) => entry.id === id);
208
+ if (existing && (existing.tag !== "text" || existing.catalogId !== catalogId)) {
209
+ throw new HubConfigurationError(400, "existing connection provider cannot be changed");
210
+ }
211
+ if (!existing && effective.modelConnections.length >= MAX_CONNECTIONS) {
212
+ throw new HubConfigurationError(400, "too many model connections");
213
+ }
214
+ const credentials = [...effective.credentials];
215
+ let credentialId = existing?.credentialId;
216
+ const existingCredential = credentials.find((entry) => entry.id === credentialId);
217
+ const previousUrl = existing?.baseUrl ?? existingCredential?.baseUrl ?? template.defaultBaseUrl;
218
+ const nextUrl = baseUrl === undefined ? previousUrl : baseUrl || template.defaultBaseUrl;
219
+ // Editing a URL must never forward a stored key to a different endpoint implicitly.
220
+ if (existingCredential?.apiKey && originOf(previousUrl) !== originOf(nextUrl) && !apiKey) {
221
+ throw new HubConfigurationError(400, "provide an API key when changing the endpoint origin");
222
+ }
223
+ if (apiKey !== undefined) {
224
+ if (credentials.length >= 1_024) {
225
+ throw new HubConfigurationError(400, "too many stored credentials");
226
+ }
227
+ // A private credential per update prevents changing keys on other connections
228
+ // that intentionally share the previous user-level credential.
229
+ credentialId = `hub-${randomUUID()}`;
230
+ credentials.push({ id: credentialId, catalogId, apiKey });
231
+ current.credentials = credentials;
232
+ }
233
+ const connection = {
234
+ ...existing,
235
+ id,
236
+ catalogId,
237
+ tag: "text",
238
+ model,
239
+ ...(credentialId ? { credentialId } : {}),
240
+ ...(baseUrl !== undefined ? { baseUrl: baseUrl || template.defaultBaseUrl } : {}),
241
+ };
242
+ current.modelConnections = [
243
+ ...effective.modelConnections.filter((entry) => entry.id !== id),
244
+ connection,
245
+ ];
246
+ });
247
+ }
248
+ function deleteConnection(input) {
249
+ knownFields(input, ["id", "replacementText", "expectedRevision"]);
250
+ const id = boundedString(input.id, "id", 128);
251
+ const replacement = input.replacementText === undefined
252
+ ? undefined
253
+ : boundedString(input.replacementText, "replacementText", 128);
254
+ settingsManager().mutateSettingsForScope("local", options.cwd, (current) => {
255
+ const effective = settingsManager().get();
256
+ checkConnectionRevision(input, id, effective);
257
+ requireTextConnection(effective, id);
258
+ const remaining = effective.modelConnections.filter((item) => item.id !== id);
259
+ const replacingDefault = effective.defaults.text === id;
260
+ if (replacement &&
261
+ (!replacingDefault ||
262
+ replacement === id ||
263
+ !remaining.some((item) => item.id === replacement && item.tag === "text")))
264
+ throw new HubConfigurationError(400, "请选择其他可用连接作为默认模型。");
265
+ if (replacingDefault && remaining.some((item) => item.tag === "text") && !replacement)
266
+ throw new HubConfigurationError(409, "请先为当前默认模型选择替代连接。");
267
+ current.modelConnections = remaining;
268
+ const defaults = { ...objectOrEmpty(current.defaults) };
269
+ if (replacingDefault)
270
+ defaults.text = replacement ?? "";
271
+ if (effective.defaults.auxText === id)
272
+ defaults.auxText = "";
273
+ if (!remaining.some((item) => item.tag === "text")) {
274
+ defaults.text = "";
275
+ defaults.auxText = "";
276
+ }
277
+ current.defaults = defaults;
278
+ // Delete only unused private keys created by this Hub, preserving shared credentials.
279
+ const removed = effective.modelConnections.find((item) => item.id === id);
280
+ if (removed.credentialId?.startsWith("hub-") &&
281
+ !remaining.some((item) => item.credentialId === removed.credentialId) &&
282
+ Array.isArray(current.credentials))
283
+ current.credentials = current.credentials.filter((item) => item.id !== removed.credentialId);
284
+ });
285
+ }
286
+ function updateSkill(input) {
287
+ knownFields(input, ["name", "enabled"]);
288
+ const name = boundedString(input.name, "name", 256);
289
+ if (typeof input.enabled !== "boolean")
290
+ throw new HubConfigurationError(400, "enabled must be boolean");
291
+ const enabled = input.enabled;
292
+ settingsManager().mutateSettingsForScope("local", options.cwd, (current) => {
293
+ invalidateSkillCache();
294
+ const skill = scanSkills(options.cwd).find((entry) => entry.name === name);
295
+ if (!skill)
296
+ throw new HubConfigurationError(404, "skill not found");
297
+ const disabled = computeEffectiveDisabledLists(settingsManager(), options.cwd);
298
+ if (enabled &&
299
+ skill.source === "plugin" &&
300
+ disabled.disabledPlugins.includes(name.split(":", 1)[0])) {
301
+ throw new HubConfigurationError(400, "enable the owning plugin on the server first");
302
+ }
303
+ const overrides = objectOrEmpty(current.capabilityOverrides);
304
+ current.capabilityOverrides = {
305
+ ...overrides,
306
+ skills: { ...objectOrEmpty(overrides.skills), [name]: enabled ? "on" : "off" },
307
+ };
308
+ });
309
+ invalidateSkillCache();
310
+ }
311
+ async function handle(req, res) {
312
+ const url = new URL(req.url ?? "/", "http://localhost");
313
+ if (url.pathname !== ROOT && !url.pathname.startsWith(`${ROOT}/`))
314
+ return false;
315
+ try {
316
+ if (closed)
317
+ throw new HubConfigurationError(503, "服务正在关闭。");
318
+ if (req.method === "GET" && url.pathname === ROOT) {
319
+ json(res, 200, snapshot());
320
+ }
321
+ else if (req.method === "GET" && url.pathname === `${ROOT}/skill`) {
322
+ json(res, 200, readSkill(url.searchParams.get("name")));
323
+ }
324
+ else if (url.pathname === `${ROOT}/connections/probe/cancel`) {
325
+ if (req.method !== "POST")
326
+ throw new HubConfigurationError(405, "method not allowed");
327
+ const input = await readJson(req);
328
+ knownFields(input, ["requestId"]);
329
+ const requestId = probeRequestId(input.requestId);
330
+ await authorize(req);
331
+ const owner = await ownerFor(req);
332
+ await authorize(req);
333
+ const key = `${owner}:${requestId}`;
334
+ cancelledProbes.set(key, Date.now() + 30_000);
335
+ pruneCancelled();
336
+ const probe = namedProbes.get(key);
337
+ if (probe)
338
+ abortProbe(probe);
339
+ json(res, 200, { ok: true });
340
+ }
341
+ else if (url.pathname === `${ROOT}/connections/probe`) {
342
+ if (req.method !== "POST")
343
+ throw new HubConfigurationError(405, "method not allowed");
344
+ const input = await readJson(req);
345
+ knownFields(input, ["id", "requestId"]);
346
+ const id = boundedString(input.id, "id", 128);
347
+ const requestId = input.requestId === undefined ? undefined : probeRequestId(input.requestId);
348
+ pruneCancelled();
349
+ await authorize(req);
350
+ const owner = await ownerFor(req);
351
+ await authorize(req);
352
+ const probeKey = requestId ? `${owner}:${requestId}` : undefined;
353
+ const settings = settingsManager().get();
354
+ requireTextConnection(settings, id);
355
+ if (probeKey && cancelledProbes.has(probeKey)) {
356
+ json(res, 200, {
357
+ ok: false,
358
+ connectionId: id,
359
+ model: settings.modelConnections.find((item) => item.id === id).model,
360
+ latencyMs: 0,
361
+ checkedAt: new Date().toISOString(),
362
+ code: "cancelled",
363
+ message: "已取消连接测试。",
364
+ });
365
+ return true;
366
+ }
367
+ if (probes.size)
368
+ throw new HubConfigurationError(429, "已有连接测试正在进行,请稍后重试。");
369
+ const controller = new AbortController();
370
+ probes.add(controller);
371
+ probeOwners.set(controller, owner);
372
+ if (probeKey)
373
+ namedProbes.set(probeKey, controller);
374
+ const cancel = () => controller.abort();
375
+ res.once("close", cancel);
376
+ req.socket.once("close", cancel);
377
+ try {
378
+ // Register before this final auth await, so a concurrent revoke also aborts this probe.
379
+ await authorize(req);
380
+ const result = await (options.probe ?? probeConfiguredModel)(settings, id, {
381
+ signal: controller.signal,
382
+ });
383
+ await authorize(req);
384
+ json(res, 200, result);
385
+ }
386
+ finally {
387
+ res.off("close", cancel);
388
+ req.socket.off("close", cancel);
389
+ abortProbe(controller);
390
+ }
391
+ }
392
+ else {
393
+ const update = url.pathname === `${ROOT}/defaults`
394
+ ? updateDefaults
395
+ : url.pathname === `${ROOT}/connections`
396
+ ? req.method === "DELETE"
397
+ ? deleteConnection
398
+ : updateConnection
399
+ : url.pathname === `${ROOT}/skills`
400
+ ? updateSkill
401
+ : undefined;
402
+ if (!update)
403
+ throw new HubConfigurationError(404, "configuration endpoint not found");
404
+ if (req.method !== "PUT" &&
405
+ !(req.method === "DELETE" && url.pathname === `${ROOT}/connections`))
406
+ throw new HubConfigurationError(405, "method not allowed");
407
+ const input = await readJson(req);
408
+ await authorize(req);
409
+ const write = async () => {
410
+ await authorize(req);
411
+ update(input);
412
+ return snapshot();
413
+ };
414
+ const result = options.withMutation ? await options.withMutation(write) : await write();
415
+ await authorize(req);
416
+ json(res, 200, result);
417
+ }
418
+ }
419
+ catch (error) {
420
+ if (error instanceof HubConfigurationError)
421
+ json(res, error.status, { error: error.message });
422
+ else
423
+ json(res, 500, { error: "configuration request failed" });
424
+ }
425
+ return true;
426
+ }
427
+ return {
428
+ handle,
429
+ snapshot,
430
+ cancelOwner: (owner) => {
431
+ for (const [probe, id] of probeOwners)
432
+ if (id === owner)
433
+ abortProbe(probe);
434
+ },
435
+ close: () => {
436
+ closed = true;
437
+ for (const probe of probes)
438
+ abortProbe(probe);
439
+ cancelledProbes.clear();
440
+ connectionRevisions.clear();
441
+ },
442
+ };
443
+ }
444
+ function probeRequestId(value) {
445
+ const id = boundedString(value, "requestId", 36);
446
+ if (!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(id))
447
+ throw new HubConfigurationError(400, "invalid probe request ID");
448
+ return id;
449
+ }
450
+ function requireTextConnection(settings, id) {
451
+ if (!settings.modelConnections.some((connection) => connection.id === id && connection.tag === "text")) {
452
+ throw new HubConfigurationError(400, "select an existing text connection");
453
+ }
454
+ }
455
+ function objectOrEmpty(value) {
456
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
457
+ }
458
+ function boundedString(value, label, max) {
459
+ if (typeof value !== "string" ||
460
+ !value.trim() ||
461
+ value.length > max ||
462
+ /[\x00-\x1f\x7f]/.test(value) ||
463
+ FORBIDDEN_KEYS.has(value)) {
464
+ throw new HubConfigurationError(400, `${label} is invalid`);
465
+ }
466
+ return value;
467
+ }
468
+ function knownFields(input, fields) {
469
+ if (Object.keys(input).some((key) => !fields.includes(key))) {
470
+ throw new HubConfigurationError(400, "unsupported configuration field");
471
+ }
472
+ }
473
+ function inputUrl(value) {
474
+ const raw = boundedString(value, "baseUrl", 4_096);
475
+ try {
476
+ const url = new URL(raw);
477
+ if (!["http:", "https:"].includes(url.protocol) ||
478
+ url.username ||
479
+ url.password ||
480
+ url.search ||
481
+ url.hash)
482
+ throw new Error();
483
+ return raw;
484
+ }
485
+ catch {
486
+ throw new HubConfigurationError(400, "baseUrl must be an HTTP(S) URL without credentials, query or fragment");
487
+ }
488
+ }
489
+ function displayUrl(value) {
490
+ if (!value)
491
+ return "";
492
+ try {
493
+ const url = new URL(value);
494
+ if (!["http:", "https:"].includes(url.protocol))
495
+ return "";
496
+ url.username = "";
497
+ url.password = "";
498
+ url.search = "";
499
+ url.hash = "";
500
+ return url.toString();
501
+ }
502
+ catch {
503
+ return "";
504
+ }
505
+ }
506
+ function originOf(value) {
507
+ try {
508
+ return new URL(value).origin;
509
+ }
510
+ catch {
511
+ return "";
512
+ }
513
+ }
514
+ async function readJson(req) {
515
+ if (req.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
516
+ throw new HubConfigurationError(415, "application/json required");
517
+ }
518
+ const declaredLength = Number(req.headers["content-length"] ?? 0);
519
+ if (!Number.isFinite(declaredLength) || declaredLength > MAX_BODY_BYTES)
520
+ throw new HubConfigurationError(413, "request body is too large");
521
+ let bytes = 0;
522
+ const chunks = [];
523
+ const timer = setTimeout(() => req.destroy(), 10_000);
524
+ timer.unref();
525
+ try {
526
+ for await (const chunk of req) {
527
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
528
+ bytes += buffer.length;
529
+ if (bytes > MAX_BODY_BYTES)
530
+ throw new HubConfigurationError(413, "request body is too large");
531
+ chunks.push(buffer);
532
+ }
533
+ }
534
+ finally {
535
+ clearTimeout(timer);
536
+ }
537
+ try {
538
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
539
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
540
+ throw new Error();
541
+ return parsed;
542
+ }
543
+ catch {
544
+ throw new HubConfigurationError(400, "JSON object required");
545
+ }
546
+ }
547
+ function json(res, status, value) {
548
+ if (res.headersSent || res.destroyed)
549
+ return;
550
+ res.writeHead(status, {
551
+ "content-type": "application/json; charset=utf-8",
552
+ "cache-control": "no-store",
553
+ });
554
+ res.end(JSON.stringify(value));
555
+ }
@@ -0,0 +1,9 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ /** Authenticated, read-only workspace files. Downloads never execute as Hub pages. */
3
+ export declare function createHubFiles(options: {
4
+ cwd: string;
5
+ isAuthorized: (request: IncomingMessage) => Promise<boolean>;
6
+ }): {
7
+ handle: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
8
+ close(): void;
9
+ };