@cjhyy/code-shell-server 0.9.28 → 0.9.30
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/dist/links/remote-catalog.d.ts +1 -1
- package/dist/links/remote-catalog.js +5 -1
- package/dist/mobile-remote/mobile-chat-turn.d.ts +3 -1
- package/dist/mobile-remote/mobile-chat-turn.js +12 -3
- package/dist/mobile-remote/mobile-client-event-validator.js +15 -0
- package/dist/mobile-remote/remote-host-manager.d.ts +4 -0
- package/dist/mobile-remote/remote-host-manager.js +37 -2
- package/dist/panels/browser-capture.d.ts +15 -0
- package/dist/panels/browser-capture.js +35 -0
- package/dist/panels/runtime.js +27 -1
- package/dist/remote-relay/connector.d.ts +3 -1
- package/dist/remote-relay/connector.js +56 -8
- package/dist/remote-relay/local-stream.d.ts +3 -1
- package/package.json +5 -5
|
@@ -8,7 +8,7 @@ export declare const REMOTE_LINK_ADAPTERS: ({
|
|
|
8
8
|
id: "github";
|
|
9
9
|
name: "GitHub";
|
|
10
10
|
group: "repositories";
|
|
11
|
-
actions: readonly ["list_repositories", "get_readme", "get_file", "list_issues", "get_issue", "list_pull_requests", "get_pull_request", "create_issue", "get_repository", "get_starred", "set_starred"];
|
|
11
|
+
actions: readonly ["list_repositories", "get_readme", "get_file", "list_issues", "get_issue", "list_pull_requests", "get_pull_request", "create_issue", "update_issue", "get_repository", "get_starred", "set_starred"];
|
|
12
12
|
} | {
|
|
13
13
|
methodId: string;
|
|
14
14
|
authModeId: string;
|
|
@@ -20,10 +20,14 @@ export async function readRemoteProviderCatalog(configuration) {
|
|
|
20
20
|
/** A legacy v1 service has no catalog. Its one already-reviewed adapter remains compatible. */
|
|
21
21
|
export async function readRemoteProviderCapabilities(configuration) {
|
|
22
22
|
const issuer = beginRemoteLinkAuthorization(configuration).configuration.issuer;
|
|
23
|
+
const scopes = REMOTE_LINK_ADAPTERS.flatMap((adapter) => adapter.scopes);
|
|
24
|
+
const capabilities = JSON.stringify({ version: 1, scopes });
|
|
25
|
+
if (scopes.length > 128 || Buffer.byteLength(capabilities) > 4_096)
|
|
26
|
+
throw new Error("Host Link capabilities exceed catalog negotiation limits");
|
|
23
27
|
const response = await fetch(new URL("/api/v1/links/providers", issuer), {
|
|
24
28
|
redirect: "error",
|
|
25
29
|
signal: AbortSignal.timeout(5_000),
|
|
26
|
-
headers: { Accept: "application/json" },
|
|
30
|
+
headers: { Accept: "application/json", "X-CodeShell-Link-Capabilities": capabilities },
|
|
27
31
|
});
|
|
28
32
|
if ([404, 501].includes(response.status)) {
|
|
29
33
|
await response.body?.cancel();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { markAttachmentsSent, type InputAttachmentMeta } from "../attachment-service.js";
|
|
2
|
-
import { type MobileRunBridge } from "./mobile-run-dispatch.js";
|
|
2
|
+
import { type MobileRunBridge, type MobileRunAcceptance, type MobileRunRequest } from "./mobile-run-dispatch.js";
|
|
3
3
|
import type { MobileUploadService } from "./mobile-upload-service.js";
|
|
4
4
|
import type { MobileAttachmentSummary, MobileImageAttachment, PermissionMode } from "./types.js";
|
|
5
5
|
import type { WorkerFrameMeta } from "../worker-bridge-core.js";
|
|
@@ -20,6 +20,8 @@ export interface DispatchMobileChatTurnInput {
|
|
|
20
20
|
uploads: ChatTurnUploads;
|
|
21
21
|
resolveWorkspace: (sessionId: string, fallbackCwd: string) => Promise<string>;
|
|
22
22
|
markSent?: typeof markAttachmentsSent;
|
|
23
|
+
/** Host-owned alternative producer; receives the same materialized input and ack boundary. */
|
|
24
|
+
submit?: (request: MobileRunRequest) => Promise<MobileRunAcceptance>;
|
|
23
25
|
}
|
|
24
26
|
export type DispatchMobileChatTurnResult = {
|
|
25
27
|
ok: true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { markAttachmentsSent } from "../attachment-service.js";
|
|
2
2
|
import { stablePromptHash } from "../client-message-id.js";
|
|
3
3
|
import { materializeMobileAttachments } from "./mobile-attachments.js";
|
|
4
|
-
import { injectMobileRunAndAwaitAcceptance } from "./mobile-run-dispatch.js";
|
|
4
|
+
import { injectMobileRunAndAwaitAcceptance, } from "./mobile-run-dispatch.js";
|
|
5
5
|
async function settleClaims(uploads, deviceId, claims, action) {
|
|
6
6
|
await Promise.allSettled(claims.map((claim) => uploads[action](deviceId, claim.uploadId, claim.claimId)));
|
|
7
7
|
}
|
|
@@ -40,7 +40,7 @@ export async function dispatchMobileChatTurn(input) {
|
|
|
40
40
|
}
|
|
41
41
|
const clientMessageId = suppliedClientMessageId ||
|
|
42
42
|
`mobile:${input.sessionId}:${input.runId}:${stablePromptHash(`${text}\0${attachmentHash}`)}`;
|
|
43
|
-
const
|
|
43
|
+
const request = {
|
|
44
44
|
id: input.runId,
|
|
45
45
|
params: {
|
|
46
46
|
task: text,
|
|
@@ -52,7 +52,16 @@ export async function dispatchMobileChatTurn(input) {
|
|
|
52
52
|
attachments: materialized.metas,
|
|
53
53
|
...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
|
|
54
54
|
},
|
|
55
|
-
}
|
|
55
|
+
};
|
|
56
|
+
let acceptance;
|
|
57
|
+
try {
|
|
58
|
+
acceptance = input.submit
|
|
59
|
+
? await input.submit(request)
|
|
60
|
+
: await injectMobileRunAndAwaitAcceptance(input.bridge, request, input.meta);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
acceptance = { ok: false, message: "The selected runtime rejected the submission" };
|
|
64
|
+
}
|
|
56
65
|
if (!acceptance.ok) {
|
|
57
66
|
await settleClaims(input.uploads, input.deviceId, materialized.claims, "release");
|
|
58
67
|
return { ok: false, message: acceptance.message };
|
|
@@ -41,6 +41,21 @@ export function parseMobileClientEvent(value) {
|
|
|
41
41
|
event.size <= MAX_MOBILE_IMAGE_BYTES;
|
|
42
42
|
break;
|
|
43
43
|
case "session.select":
|
|
44
|
+
valid =
|
|
45
|
+
boundedString(event.sessionId, MAX_ID_LENGTH) &&
|
|
46
|
+
optionalBoundedString(event.recoveryId, 128);
|
|
47
|
+
break;
|
|
48
|
+
case "session.recovery.cancel":
|
|
49
|
+
valid = true;
|
|
50
|
+
break;
|
|
51
|
+
case "session.outputJournal":
|
|
52
|
+
valid =
|
|
53
|
+
boundedString(event.sessionId, 128) &&
|
|
54
|
+
boundedString(event.recoveryId, 128) &&
|
|
55
|
+
boundedString(event.requestId, 128) &&
|
|
56
|
+
optionalBoundedString(event.after, 2048) &&
|
|
57
|
+
optionalBoundedString(event.through, 2048);
|
|
58
|
+
break;
|
|
44
59
|
case "session.history":
|
|
45
60
|
valid = boundedString(event.sessionId, MAX_ID_LENGTH);
|
|
46
61
|
break;
|
|
@@ -36,6 +36,7 @@ export interface RemoteHostStarted {
|
|
|
36
36
|
}
|
|
37
37
|
export interface RemoteHostManagerOptions {
|
|
38
38
|
devices: TrustedDeviceStore;
|
|
39
|
+
outputJournal?: boolean;
|
|
39
40
|
onClientEvent: (event: unknown, ws: WebSocket) => void | Promise<void>;
|
|
40
41
|
/**
|
|
41
42
|
* Absolute path to the built mobile app (out/mobile). Defaults to the
|
|
@@ -116,6 +117,9 @@ export declare class RemoteHostManager extends EventEmitter {
|
|
|
116
117
|
* one phone's session/permission state never leaks onto another's screen.
|
|
117
118
|
* A device with no live socket (raced a disconnect) is a silent no-op. */
|
|
118
119
|
sendToDevice(deviceId: string, event: MobileServerEvent): void;
|
|
120
|
+
/** Reply only to the currently authenticated tab; retired/revoked viewers receive nothing. */
|
|
121
|
+
hasAuthenticatedViewer(viewerId: string, deviceId: string): boolean;
|
|
122
|
+
sendToViewer(viewerId: string, event: MobileServerEvent): void;
|
|
119
123
|
/** Send a raw JSON-RPC worker line to ONLY one authenticated device. */
|
|
120
124
|
sendRawToDevice(deviceId: string, payload: string): void;
|
|
121
125
|
/** Broadcast a raw line (e.g. a mirrored worker→renderer JSON-RPC line) to
|
|
@@ -406,7 +406,11 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
406
406
|
if (!device) {
|
|
407
407
|
return { type: "pair.failed", message: "Pairing token expired or invalid" };
|
|
408
408
|
}
|
|
409
|
-
return {
|
|
409
|
+
return {
|
|
410
|
+
type: "pair.ok",
|
|
411
|
+
device,
|
|
412
|
+
...(this.opts.outputJournal ? { capabilities: { outputJournal: 1 } } : {}),
|
|
413
|
+
};
|
|
410
414
|
}
|
|
411
415
|
if (event.type === "auth.device") {
|
|
412
416
|
let device;
|
|
@@ -418,7 +422,11 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
418
422
|
}
|
|
419
423
|
if (!device)
|
|
420
424
|
return { type: "auth.failed", message: "Device is not trusted" };
|
|
421
|
-
return {
|
|
425
|
+
return {
|
|
426
|
+
type: "auth.ok",
|
|
427
|
+
device,
|
|
428
|
+
...(this.opts.outputJournal ? { capabilities: { outputJournal: 1 } } : {}),
|
|
429
|
+
};
|
|
422
430
|
}
|
|
423
431
|
return undefined;
|
|
424
432
|
}
|
|
@@ -438,6 +446,33 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
438
446
|
}
|
|
439
447
|
}
|
|
440
448
|
}
|
|
449
|
+
/** Reply only to the currently authenticated tab; retired/revoked viewers receive nothing. */
|
|
450
|
+
hasAuthenticatedViewer(viewerId, deviceId) {
|
|
451
|
+
for (const client of this.wss?.clients ?? []) {
|
|
452
|
+
if (client.readyState === client.OPEN &&
|
|
453
|
+
this.authed.get(client) === deviceId &&
|
|
454
|
+
this.viewers.get(client) === viewerId)
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
sendToViewer(viewerId, event) {
|
|
460
|
+
const payload = JSON.stringify(event);
|
|
461
|
+
for (const client of this.wss?.clients ?? []) {
|
|
462
|
+
if (client.readyState === client.OPEN &&
|
|
463
|
+
this.authed.has(client) &&
|
|
464
|
+
this.viewers.get(client) === viewerId) {
|
|
465
|
+
// A selected viewer cannot accumulate an unbounded outbound page queue
|
|
466
|
+
// by issuing new reads while refusing to consume prior replies.
|
|
467
|
+
if (client.bufferedAmount + Buffer.byteLength(payload) > 4 * 1024 * 1024) {
|
|
468
|
+
this.releaseSocket(client);
|
|
469
|
+
client.terminate();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
client.send(payload);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
441
476
|
/** Send a raw JSON-RPC worker line to ONLY one authenticated device. */
|
|
442
477
|
sendRawToDevice(deviceId, payload) {
|
|
443
478
|
for (const client of this.wss?.clients ?? []) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Bounded, domain-neutral requests for the trusted browser's recording chooser. */
|
|
2
|
+
export declare const panelVideoLimits: Readonly<{
|
|
3
|
+
maxDurationSeconds: 1200;
|
|
4
|
+
maxBytes: number;
|
|
5
|
+
defaultDurationSeconds: 300;
|
|
6
|
+
defaultBytes: number;
|
|
7
|
+
}>;
|
|
8
|
+
export interface PanelVideoOptions {
|
|
9
|
+
source: "camera" | "screen";
|
|
10
|
+
microphone: boolean;
|
|
11
|
+
systemAudio: boolean;
|
|
12
|
+
maxDurationSeconds: number;
|
|
13
|
+
maxBytes: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function panelVideoOptions(raw: unknown): PanelVideoOptions;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Bounded, domain-neutral requests for the trusted browser's recording chooser. */
|
|
2
|
+
export const panelVideoLimits = Object.freeze({
|
|
3
|
+
maxDurationSeconds: 1200,
|
|
4
|
+
maxBytes: 200 * 1024 * 1024,
|
|
5
|
+
defaultDurationSeconds: 300,
|
|
6
|
+
defaultBytes: 64 * 1024 * 1024,
|
|
7
|
+
});
|
|
8
|
+
export function panelVideoOptions(raw) {
|
|
9
|
+
const value = raw;
|
|
10
|
+
if (!value ||
|
|
11
|
+
typeof value !== "object" ||
|
|
12
|
+
Array.isArray(value) ||
|
|
13
|
+
Object.keys(value).some((key) => !["source", "microphone", "systemAudio", "maxDurationSeconds", "maxBytes"].includes(key)) ||
|
|
14
|
+
(value.source !== "camera" && value.source !== "screen") ||
|
|
15
|
+
(value.microphone !== undefined && typeof value.microphone !== "boolean") ||
|
|
16
|
+
(value.systemAudio !== undefined && typeof value.systemAudio !== "boolean") ||
|
|
17
|
+
(value.source !== "screen" && value.systemAudio === true))
|
|
18
|
+
throw new Error("视频录制请求无效。");
|
|
19
|
+
const maxDurationSeconds = value.maxDurationSeconds ?? panelVideoLimits.defaultDurationSeconds;
|
|
20
|
+
const maxBytes = value.maxBytes ?? panelVideoLimits.defaultBytes;
|
|
21
|
+
if (!Number.isSafeInteger(maxDurationSeconds) ||
|
|
22
|
+
Number(maxDurationSeconds) < 1 ||
|
|
23
|
+
Number(maxDurationSeconds) > panelVideoLimits.maxDurationSeconds ||
|
|
24
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
25
|
+
Number(maxBytes) < 1 ||
|
|
26
|
+
Number(maxBytes) > panelVideoLimits.maxBytes)
|
|
27
|
+
throw new Error("视频录制限制无效。");
|
|
28
|
+
return {
|
|
29
|
+
source: value.source,
|
|
30
|
+
microphone: value.microphone !== false,
|
|
31
|
+
systemAudio: value.systemAudio === true,
|
|
32
|
+
maxDurationSeconds: Number(maxDurationSeconds),
|
|
33
|
+
maxBytes: Number(maxBytes),
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/panels/runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { panelVideoLimits, panelVideoOptions } from "./browser-capture.js";
|
|
1
2
|
import { panelExecutionGate, PanelExecutionBusyError } from "./execution-gate.js";
|
|
2
3
|
import { randomBytes, createHash } from "node:crypto";
|
|
3
4
|
import { constants } from "node:fs";
|
|
@@ -43,6 +44,8 @@ const METHODS = [
|
|
|
43
44
|
"resources.open",
|
|
44
45
|
"resources.preview",
|
|
45
46
|
"resources.recordAudio",
|
|
47
|
+
"resources.recordVideo",
|
|
48
|
+
"resources.recordVideo.capabilities",
|
|
46
49
|
...panelToolJobMethods,
|
|
47
50
|
"credentials.connections.list",
|
|
48
51
|
"credentials.cookies.listForTask",
|
|
@@ -299,7 +302,7 @@ function bridgeScript(id, origin) {
|
|
|
299
302
|
const requestId = String(++next);
|
|
300
303
|
const timer = setTimeout(() => {
|
|
301
304
|
pending.delete(requestId); reject(new Error("Panel request timed out"));
|
|
302
|
-
}, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio"].includes(method) ? 30 * 60 * 1000 : 60000);
|
|
305
|
+
}, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio", "resources.recordVideo"].includes(method) ? 30 * 60 * 1000 : 60000);
|
|
303
306
|
pending.set(requestId, { resolve, reject, timer });
|
|
304
307
|
try {
|
|
305
308
|
parent.postMessage({ type: "codeshell-panel:call", instanceId: id, requestId, method, params }, origin);
|
|
@@ -1128,6 +1131,7 @@ export function createPanelRuntime(options) {
|
|
|
1128
1131
|
? {
|
|
1129
1132
|
methodLimits: {
|
|
1130
1133
|
"resources.recordAudio": { timeoutMs: 30 * 60 * 1000 },
|
|
1134
|
+
"resources.recordVideo": { timeoutMs: 30 * 60 * 1000 },
|
|
1131
1135
|
...(app.permissions.includes("process")
|
|
1132
1136
|
? {
|
|
1133
1137
|
"tasks.start": {
|
|
@@ -1326,6 +1330,28 @@ export function createPanelRuntime(options) {
|
|
|
1326
1330
|
send: (event, payload) => emit(grant, event, payload),
|
|
1327
1331
|
};
|
|
1328
1332
|
if (method.startsWith("resources.")) {
|
|
1333
|
+
if (method === "resources.recordVideo.capabilities") {
|
|
1334
|
+
if (params !== undefined &&
|
|
1335
|
+
(!params ||
|
|
1336
|
+
typeof params !== "object" ||
|
|
1337
|
+
Array.isArray(params) ||
|
|
1338
|
+
Object.keys(params).length))
|
|
1339
|
+
error(400, "视频录制能力请求无效。");
|
|
1340
|
+
// Device/encoder support belongs to the trusted browser, not this server.
|
|
1341
|
+
return {
|
|
1342
|
+
effect: method,
|
|
1343
|
+
maxDurationSeconds: panelVideoLimits.maxDurationSeconds,
|
|
1344
|
+
maxBytes: panelVideoLimits.maxBytes,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
if (method === "resources.recordVideo") {
|
|
1348
|
+
try {
|
|
1349
|
+
return { effect: method, ...panelVideoOptions(params) };
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
error(400, "视频录制请求或限制无效。");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1329
1355
|
if (method === "resources.recordAudio") {
|
|
1330
1356
|
const value = (params ?? {});
|
|
1331
1357
|
const maxDurationSeconds = value.maxDurationSeconds ?? 300;
|
|
@@ -11,7 +11,9 @@ export interface DeviceRelayConnectorOptions {
|
|
|
11
11
|
relayOrigin: string;
|
|
12
12
|
hostId: string;
|
|
13
13
|
publicOrigin: string;
|
|
14
|
-
credential
|
|
14
|
+
credential?: string;
|
|
15
|
+
/** Main-owned rotating credentials, resolved before each control connection. */
|
|
16
|
+
getCredential?: (signal: AbortSignal) => Promise<string>;
|
|
15
17
|
/** Target is always 127.0.0.1; stopping its Host revokes this capability. */
|
|
16
18
|
localHost: RelayLocalTarget;
|
|
17
19
|
/** Additional trusted CA for private installations; TLS verification stays enabled. */
|
|
@@ -5,7 +5,8 @@ export function createDeviceRelayConnector(options) {
|
|
|
5
5
|
if (!isRelayOrigin(options.relayOrigin) ||
|
|
6
6
|
!isRelayOrigin(options.publicOrigin) ||
|
|
7
7
|
!isRelayHostId(options.hostId) ||
|
|
8
|
-
!isRelayToken(options.credential) ||
|
|
8
|
+
(!options.getCredential && !isRelayToken(options.credential)) ||
|
|
9
|
+
(options.getCredential !== undefined && typeof options.getCredential !== "function") ||
|
|
9
10
|
!options.localHost ||
|
|
10
11
|
!Number.isInteger(options.localHost.port) ||
|
|
11
12
|
options.localHost.port < 1 ||
|
|
@@ -27,6 +28,7 @@ export function createDeviceRelayConnector(options) {
|
|
|
27
28
|
let control;
|
|
28
29
|
let reconnect;
|
|
29
30
|
let failures = 0;
|
|
31
|
+
let credentialAttempt;
|
|
30
32
|
const pending = new Set();
|
|
31
33
|
const streams = new Set();
|
|
32
34
|
const state = (value) => {
|
|
@@ -39,18 +41,62 @@ export function createDeviceRelayConnector(options) {
|
|
|
39
41
|
};
|
|
40
42
|
const track = (done) => {
|
|
41
43
|
pending.add(done);
|
|
42
|
-
void done.finally(() => pending.delete(done));
|
|
44
|
+
void done.finally(() => pending.delete(done)).catch(() => { });
|
|
43
45
|
};
|
|
44
|
-
const
|
|
46
|
+
const retry = (attempt) => {
|
|
47
|
+
if (!running || generation !== attempt || config.localHost.signal.aborted)
|
|
48
|
+
return;
|
|
49
|
+
const delay = Math.min(30_000, 500 * 2 ** Math.min(failures++, 6));
|
|
50
|
+
reconnect = setTimeout(() => track(connect(attempt)), delay * (0.75 + Math.random() * 0.5));
|
|
51
|
+
};
|
|
52
|
+
const connect = async (attempt) => {
|
|
45
53
|
if (!running || generation !== attempt || config.localHost.signal.aborted)
|
|
46
54
|
return;
|
|
47
55
|
state("connecting");
|
|
48
56
|
if (!running || generation !== attempt || config.localHost.signal.aborted)
|
|
49
57
|
return;
|
|
58
|
+
const acquisition = new AbortController();
|
|
59
|
+
credentialAttempt?.abort();
|
|
60
|
+
credentialAttempt = acquisition;
|
|
61
|
+
let credential;
|
|
62
|
+
try {
|
|
63
|
+
credential = config.getCredential
|
|
64
|
+
? await new Promise((resolve, reject) => {
|
|
65
|
+
const abort = () => reject(new Error("Relay credential acquisition cancelled"));
|
|
66
|
+
acquisition.signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
Promise.resolve()
|
|
68
|
+
.then(() => config.getCredential(acquisition.signal))
|
|
69
|
+
.then(resolve, reject)
|
|
70
|
+
.finally(() => acquisition.signal.removeEventListener("abort", abort))
|
|
71
|
+
.catch(() => { });
|
|
72
|
+
})
|
|
73
|
+
: config.credential;
|
|
74
|
+
if (!isRelayToken(credential))
|
|
75
|
+
throw new Error("Invalid relay credential");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!running || generation !== attempt || acquisition.signal.aborted)
|
|
79
|
+
return;
|
|
80
|
+
if (error?.status === 401 ||
|
|
81
|
+
error?.status === 403) {
|
|
82
|
+
running = false;
|
|
83
|
+
state("unauthorized");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
state("disconnected");
|
|
87
|
+
retry(attempt);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!running ||
|
|
92
|
+
generation !== attempt ||
|
|
93
|
+
acquisition.signal.aborted ||
|
|
94
|
+
config.localHost.signal.aborted)
|
|
95
|
+
return;
|
|
50
96
|
const url = new URL(RELAY_CONNECT_PATH, config.relayOrigin);
|
|
51
97
|
url.protocol = "wss:";
|
|
52
98
|
const ws = new WebSocket(url, {
|
|
53
|
-
headers: { Authorization: `Bearer ${
|
|
99
|
+
headers: { Authorization: `Bearer ${credential}` },
|
|
54
100
|
ca: config.ca,
|
|
55
101
|
followRedirects: false,
|
|
56
102
|
perMessageDeflate: false,
|
|
@@ -79,8 +125,7 @@ export function createDeviceRelayConnector(options) {
|
|
|
79
125
|
state("disconnected");
|
|
80
126
|
if (!running || generation !== attempt || control || config.localHost.signal.aborted)
|
|
81
127
|
return resolve();
|
|
82
|
-
|
|
83
|
-
reconnect = setTimeout(() => connect(attempt), delay * (0.75 + Math.random() * 0.5));
|
|
128
|
+
retry(attempt);
|
|
84
129
|
}
|
|
85
130
|
resolve();
|
|
86
131
|
})));
|
|
@@ -146,7 +191,7 @@ export function createDeviceRelayConnector(options) {
|
|
|
146
191
|
};
|
|
147
192
|
if (streams.size >= RELAY_MAX_STREAMS)
|
|
148
193
|
return failed();
|
|
149
|
-
const stream = openRelayStream(config, message, failed);
|
|
194
|
+
const stream = openRelayStream({ ...config, credential }, message, failed);
|
|
150
195
|
owned.set(message.streamId, stream);
|
|
151
196
|
streams.add(stream);
|
|
152
197
|
const done = stream.done.then(() => {
|
|
@@ -166,7 +211,8 @@ export function createDeviceRelayConnector(options) {
|
|
|
166
211
|
config.localHost.signal.addEventListener("abort", revoke, { once: true });
|
|
167
212
|
running = true;
|
|
168
213
|
failures = 0;
|
|
169
|
-
connect(++generation);
|
|
214
|
+
const acquisition = connect(++generation);
|
|
215
|
+
track(acquisition);
|
|
170
216
|
},
|
|
171
217
|
close() {
|
|
172
218
|
if (closing)
|
|
@@ -174,6 +220,8 @@ export function createDeviceRelayConnector(options) {
|
|
|
174
220
|
running = false;
|
|
175
221
|
config.localHost.signal.removeEventListener("abort", revoke);
|
|
176
222
|
generation++;
|
|
223
|
+
credentialAttempt?.abort();
|
|
224
|
+
credentialAttempt = undefined;
|
|
177
225
|
clearTimeout(reconnect);
|
|
178
226
|
reconnect = undefined;
|
|
179
227
|
control?.terminate();
|
|
@@ -5,6 +5,8 @@ export interface RelayLocalStream {
|
|
|
5
5
|
done: Promise<void>;
|
|
6
6
|
}
|
|
7
7
|
/** One fixed loopback connection. Never accept a target, URL, or port from control messages. */
|
|
8
|
-
export declare function openRelayStream(config: DeviceRelayConnectorOptions
|
|
8
|
+
export declare function openRelayStream(config: DeviceRelayConnectorOptions & {
|
|
9
|
+
credential: string;
|
|
10
|
+
}, message: Extract<RelayControlMessage, {
|
|
9
11
|
type: "open";
|
|
10
12
|
}>, failed: () => void): RelayLocalStream;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.30",
|
|
4
4
|
"description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@cjhyy/code-shell-capability-coding": "0.9.
|
|
64
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
65
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
63
|
+
"@cjhyy/code-shell-capability-coding": "0.9.30",
|
|
64
|
+
"@cjhyy/code-shell-core": "0.9.30",
|
|
65
|
+
"@cjhyy/code-shell-web": "0.9.30",
|
|
66
66
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
67
67
|
"@types/ws": "^8.18.1",
|
|
68
68
|
"ws": "^8.21.0",
|
|
69
|
-
"@cjhyy/code-shell-link": "0.9.
|
|
69
|
+
"@cjhyy/code-shell-link": "0.9.30"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=20.10"
|