@cortexkit/aft-opencode 0.42.0 → 0.43.0
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/bg-notifications.d.ts +35 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/config.d.ts +7 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/configure-warnings.d.ts +3 -3
- package/dist/configure-warnings.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6092 -9528
- package/dist/notifications.d.ts +2 -2
- package/dist/notifications.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +6 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +29 -15
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +10 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/subc-tool-schemas.d.ts +1 -1
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tools/_shared.d.ts +12 -2
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/imports.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts +0 -1
- package/dist/tools/inspect.d.ts.map +1 -1
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/permissions.d.ts +23 -13
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +0 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui/notification-socket.d.ts +41 -0
- package/dist/tui/notification-socket.d.ts.map +1 -0
- package/dist/tui.js +2460 -187
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -9
- package/src/shared/rpc-client.ts +9 -0
- package/src/shared/rpc-notifications.ts +97 -41
- package/src/shared/rpc-server.ts +286 -67
- package/src/tui/index.tsx +77 -125
- package/src/tui/notification-socket.ts +421 -0
- package/src/tui/sidebar.tsx +38 -54
- package/dist/patch-parser.d.ts +0 -33
- package/dist/patch-parser.d.ts.map +0 -1
- package/dist/shared/opencode-config-dir.d.ts +0 -16
- package/dist/shared/opencode-config-dir.d.ts.map +0 -1
- package/dist/shared/pty-cache.d.ts +0 -18
- package/dist/shared/pty-cache.d.ts.map +0 -1
- package/dist/shared/tui-config.d.ts +0 -2
- package/dist/shared/tui-config.d.ts.map +0 -1
- package/src/shared/opencode-config-dir.ts +0 -46
- package/src/shared/pty-cache.ts +0 -113
- package/src/shared/tui-config.ts +0 -58
package/src/shared/rpc-server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
@@ -11,22 +11,92 @@ import {
|
|
|
11
11
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { log, warn } from "../logger";
|
|
14
|
+
import {
|
|
15
|
+
drainNotifications,
|
|
16
|
+
type NotificationSink,
|
|
17
|
+
registerNotificationSink,
|
|
18
|
+
registerStatusChangeSink,
|
|
19
|
+
type StatusChangeSink,
|
|
20
|
+
} from "./rpc-notifications";
|
|
14
21
|
import { isPidAlive, parseRpcPortRecord, rpcPortFileDir } from "./rpc-utils";
|
|
15
22
|
|
|
16
23
|
type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
17
24
|
|
|
25
|
+
type BunServe = <Data>(options: BunServeOptions<Data>) => BunServer<Data>;
|
|
26
|
+
|
|
27
|
+
interface BunRuntime {
|
|
28
|
+
serve: BunServe;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface BunServer<Data> {
|
|
32
|
+
port?: number;
|
|
33
|
+
stop(closeActiveConnections?: boolean): void;
|
|
34
|
+
upgrade(req: Request, options: { data: Data }): boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface BunServeOptions<Data> {
|
|
38
|
+
port: number;
|
|
39
|
+
hostname: string;
|
|
40
|
+
fetch(
|
|
41
|
+
req: Request,
|
|
42
|
+
server: BunServer<Data>,
|
|
43
|
+
): Response | Promise<Response | undefined> | undefined;
|
|
44
|
+
websocket: {
|
|
45
|
+
open(ws: BunServerWebSocket<Data>): void;
|
|
46
|
+
message(ws: BunServerWebSocket<Data>, raw: string | Buffer): void;
|
|
47
|
+
close(ws: BunServerWebSocket<Data>): void;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface BunServerWebSocket<Data> {
|
|
52
|
+
data: Data;
|
|
53
|
+
send(data: string): unknown;
|
|
54
|
+
close(code?: number, reason?: string): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface WsData {
|
|
58
|
+
authed: boolean;
|
|
59
|
+
sessionId?: string;
|
|
60
|
+
unregisterNotification?: () => void;
|
|
61
|
+
unregisterStatus?: () => void;
|
|
62
|
+
authTimer?: ReturnType<typeof setTimeout>;
|
|
63
|
+
}
|
|
64
|
+
|
|
18
65
|
const PORT_FILE_HEARTBEAT_MS = 15_000;
|
|
66
|
+
const MAX_BODY_BYTES = 1_048_576;
|
|
67
|
+
const WS_AUTH_TIMEOUT_MS = 5_000;
|
|
68
|
+
const WS_CLOSE_UNAUTHORIZED = 4401;
|
|
69
|
+
|
|
70
|
+
function bunRuntime(): BunRuntime | undefined {
|
|
71
|
+
return (globalThis as typeof globalThis & { Bun?: BunRuntime }).Bun;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tokensMatch(presented: string, expected: string): boolean {
|
|
75
|
+
const a = Buffer.from(presented, "utf8");
|
|
76
|
+
const b = Buffer.from(expected, "utf8");
|
|
77
|
+
if (a.length !== b.length) return false;
|
|
78
|
+
return timingSafeEqual(a, b);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function json(body: unknown, status = 200): Response {
|
|
82
|
+
return new Response(JSON.stringify(body), {
|
|
83
|
+
status,
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
19
87
|
|
|
20
88
|
export class AftRpcServer {
|
|
21
|
-
private
|
|
89
|
+
private nodeServer: Server | null = null;
|
|
90
|
+
private bunServer: BunServer<WsData> | null = null;
|
|
22
91
|
private port = 0;
|
|
23
|
-
private token
|
|
92
|
+
private token = randomBytes(32).toString("hex");
|
|
24
93
|
private handlers = new Map<string, RpcHandler>();
|
|
25
94
|
private portFilePath: string;
|
|
26
95
|
private portsDir: string;
|
|
27
96
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
28
97
|
/** Unique per-instance ID — distinguishes our entry from duplicate plugin loads. */
|
|
29
98
|
private instanceId: string;
|
|
99
|
+
private sockets = new Set<BunServerWebSocket<WsData>>();
|
|
30
100
|
|
|
31
101
|
constructor(storageDir: string, directory: string) {
|
|
32
102
|
this.portsDir = rpcPortFileDir(storageDir, directory);
|
|
@@ -41,6 +111,36 @@ export class AftRpcServer {
|
|
|
41
111
|
|
|
42
112
|
/** Start the server on a random port, write port to disk. */
|
|
43
113
|
async start(): Promise<number> {
|
|
114
|
+
const bun = bunRuntime();
|
|
115
|
+
if (bun) {
|
|
116
|
+
return this.startBun(bun);
|
|
117
|
+
}
|
|
118
|
+
return this.startNode();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async startBun(bun: BunRuntime): Promise<number> {
|
|
122
|
+
const server = bun.serve<WsData>({
|
|
123
|
+
port: 0,
|
|
124
|
+
hostname: "127.0.0.1",
|
|
125
|
+
fetch: (req, srv) => this.handleFetch(req, srv),
|
|
126
|
+
websocket: {
|
|
127
|
+
open: (ws) => {
|
|
128
|
+
ws.data.authTimer = setTimeout(() => {
|
|
129
|
+
if (!ws.data.authed) ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
130
|
+
}, WS_AUTH_TIMEOUT_MS);
|
|
131
|
+
},
|
|
132
|
+
message: (ws, raw) => this.handleWsMessage(ws, raw),
|
|
133
|
+
close: (ws) => this.closeWs(ws),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.bunServer = server;
|
|
138
|
+
this.port = server.port ?? 0;
|
|
139
|
+
this.afterServerStarted();
|
|
140
|
+
return this.port;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async startNode(): Promise<number> {
|
|
44
144
|
return new Promise((resolve, reject) => {
|
|
45
145
|
const server = createServer((req, res) => this.dispatch(req, res));
|
|
46
146
|
|
|
@@ -56,48 +156,8 @@ export class AftRpcServer {
|
|
|
56
156
|
return;
|
|
57
157
|
}
|
|
58
158
|
this.port = addr.port;
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
// Write port file atomically
|
|
63
|
-
try {
|
|
64
|
-
const dir = dirname(this.portFilePath);
|
|
65
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
66
|
-
const tmpPath = `${this.portFilePath}.tmp`;
|
|
67
|
-
// Record pid + started_at so the client can skip files whose owning
|
|
68
|
-
// process is dead (without a health-check round-trip) and prefer the
|
|
69
|
-
// freshest live server, instead of accumulating crash/restart leftovers.
|
|
70
|
-
writeFileSync(
|
|
71
|
-
tmpPath,
|
|
72
|
-
JSON.stringify({
|
|
73
|
-
port: this.port,
|
|
74
|
-
token: this.token,
|
|
75
|
-
pid: process.pid,
|
|
76
|
-
started_at: Date.now(),
|
|
77
|
-
}),
|
|
78
|
-
{ encoding: "utf-8", mode: 0o600 },
|
|
79
|
-
);
|
|
80
|
-
renameSync(tmpPath, this.portFilePath);
|
|
81
|
-
log(`RPC server listening on 127.0.0.1:${this.port}`);
|
|
82
|
-
// Self-heal: the port file is this server's only discoverability
|
|
83
|
-
// record, and historically it was written exactly once — anything
|
|
84
|
-
// that deleted it (a client misjudging a load-induced health-check
|
|
85
|
-
// timeout as staleness, cache cleanup, manual sweeps) silently
|
|
86
|
-
// orphaned the server until host restart (issue #110: sidebar stuck
|
|
87
|
-
// on the lazy placeholder forever). Recreate it if it goes missing.
|
|
88
|
-
this.heartbeatTimer = setInterval(() => this.ensurePortFile(), PORT_FILE_HEARTBEAT_MS);
|
|
89
|
-
this.heartbeatTimer.unref?.();
|
|
90
|
-
// Hygiene: sweep dead siblings while we're here. The client only
|
|
91
|
-
// reclaims dead-pid files on a cold port scan, and it caches the
|
|
92
|
-
// first warm port afterwards — so crash/restart leftovers piled up
|
|
93
|
-
// (dozens of dead files per project hash were observed). Server
|
|
94
|
-
// startup is the natural sweep point: it runs once per instance and
|
|
95
|
-
// already owns this directory.
|
|
96
|
-
this.sweepDeadPortFiles();
|
|
97
|
-
} catch (err) {
|
|
98
|
-
warn(`Failed to write RPC port file: ${err}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
159
|
+
this.nodeServer = server;
|
|
160
|
+
this.afterServerStarted();
|
|
101
161
|
resolve(this.port);
|
|
102
162
|
});
|
|
103
163
|
|
|
@@ -106,6 +166,40 @@ export class AftRpcServer {
|
|
|
106
166
|
});
|
|
107
167
|
}
|
|
108
168
|
|
|
169
|
+
private afterServerStarted(): void {
|
|
170
|
+
try {
|
|
171
|
+
this.writePortFile();
|
|
172
|
+
log(`RPC server listening on 127.0.0.1:${this.port}`);
|
|
173
|
+
// Self-heal: the port file is this server's only discoverability record.
|
|
174
|
+
// Anything that deletes it silently orphans the server until host restart.
|
|
175
|
+
// Recreate it if missing.
|
|
176
|
+
this.heartbeatTimer = setInterval(() => this.ensurePortFile(), PORT_FILE_HEARTBEAT_MS);
|
|
177
|
+
this.heartbeatTimer.unref?.();
|
|
178
|
+
// Hygiene: sweep dead port files from other instances while we're here.
|
|
179
|
+
// Server startup is the natural sweep point and already owns this directory.
|
|
180
|
+
this.sweepDeadPortFiles();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
warn(`Failed to write RPC port file: ${err}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private writePortFile(): void {
|
|
187
|
+
const dir = dirname(this.portFilePath);
|
|
188
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
189
|
+
const tmpPath = `${this.portFilePath}.tmp`;
|
|
190
|
+
writeFileSync(
|
|
191
|
+
tmpPath,
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
port: this.port,
|
|
194
|
+
token: this.token,
|
|
195
|
+
pid: process.pid,
|
|
196
|
+
started_at: Date.now(),
|
|
197
|
+
}),
|
|
198
|
+
{ encoding: "utf-8", mode: 0o600 },
|
|
199
|
+
);
|
|
200
|
+
renameSync(tmpPath, this.portFilePath);
|
|
201
|
+
}
|
|
202
|
+
|
|
109
203
|
/**
|
|
110
204
|
* Remove sibling port files whose owning process is provably dead.
|
|
111
205
|
* Only files with a recorded pid that no longer maps to a live process are
|
|
@@ -143,21 +237,10 @@ export class AftRpcServer {
|
|
|
143
237
|
|
|
144
238
|
/** Rewrite the port file if it disappeared (wrongful deletion recovery). */
|
|
145
239
|
private ensurePortFile(): void {
|
|
146
|
-
if (!this.
|
|
240
|
+
if ((!this.nodeServer && !this.bunServer) || this.port <= 0) return;
|
|
147
241
|
try {
|
|
148
242
|
if (existsSync(this.portFilePath)) return;
|
|
149
|
-
|
|
150
|
-
writeFileSync(
|
|
151
|
-
tmpPath,
|
|
152
|
-
JSON.stringify({
|
|
153
|
-
port: this.port,
|
|
154
|
-
token: this.token,
|
|
155
|
-
pid: process.pid,
|
|
156
|
-
started_at: Date.now(),
|
|
157
|
-
}),
|
|
158
|
-
{ encoding: "utf-8", mode: 0o600 },
|
|
159
|
-
);
|
|
160
|
-
renameSync(tmpPath, this.portFilePath);
|
|
243
|
+
this.writePortFile();
|
|
161
244
|
log(`RPC port file was missing; rewrote ${this.portFilePath}`);
|
|
162
245
|
} catch {
|
|
163
246
|
// best-effort; retried on the next heartbeat
|
|
@@ -170,11 +253,23 @@ export class AftRpcServer {
|
|
|
170
253
|
clearInterval(this.heartbeatTimer);
|
|
171
254
|
this.heartbeatTimer = null;
|
|
172
255
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
256
|
+
for (const ws of this.sockets) {
|
|
257
|
+
try {
|
|
258
|
+
this.closeWs(ws);
|
|
259
|
+
ws.close();
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore close errors during shutdown; remaining sockets are closed independently.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
this.sockets.clear();
|
|
265
|
+
if (this.nodeServer) {
|
|
266
|
+
this.nodeServer.close();
|
|
267
|
+
this.nodeServer = null;
|
|
268
|
+
}
|
|
269
|
+
if (this.bunServer) {
|
|
270
|
+
this.bunServer.stop(true);
|
|
271
|
+
this.bunServer = null;
|
|
176
272
|
}
|
|
177
|
-
this.token = null;
|
|
178
273
|
try {
|
|
179
274
|
unlinkSync(this.portFilePath);
|
|
180
275
|
} catch {
|
|
@@ -182,6 +277,132 @@ export class AftRpcServer {
|
|
|
182
277
|
}
|
|
183
278
|
}
|
|
184
279
|
|
|
280
|
+
private async handleFetch(req: Request, srv: BunServer<WsData>): Promise<Response | undefined> {
|
|
281
|
+
const url = new URL(req.url);
|
|
282
|
+
|
|
283
|
+
if (url.pathname === "/ws") {
|
|
284
|
+
const upgraded = srv.upgrade(req, { data: { authed: false } });
|
|
285
|
+
if (upgraded) return undefined;
|
|
286
|
+
return new Response("upgrade failed", { status: 400 });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
290
|
+
return json({ ok: true, pid: process.pid });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (req.method !== "POST" || !url.pathname.startsWith("/rpc/")) {
|
|
294
|
+
return new Response("Not Found", { status: 404 });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const method = url.pathname.slice(5);
|
|
298
|
+
const handler = this.handlers.get(method);
|
|
299
|
+
if (!handler) {
|
|
300
|
+
return json({ error: `Unknown method: ${method}` }, 404);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const bodyText = await req.text();
|
|
304
|
+
if (bodyText.length > MAX_BODY_BYTES) {
|
|
305
|
+
return new Response("Request too large", { status: 413 });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let params: Record<string, unknown> = {};
|
|
309
|
+
try {
|
|
310
|
+
if (bodyText.length > 0) {
|
|
311
|
+
params = JSON.parse(bodyText);
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
return json({ error: "Invalid JSON" }, 400);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!tokensMatch(typeof params.token === "string" ? params.token : "", this.token)) {
|
|
318
|
+
return json({ error: "Forbidden" }, 403);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const { token: _token, ...handlerParams } = params;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const result = await handler(handlerParams);
|
|
325
|
+
return json(result);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
log(`RPC error: ${method} => ${err}`);
|
|
328
|
+
return json({ error: String(err) }, 500);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private handleWsMessage(ws: BunServerWebSocket<WsData>, raw: string | Buffer): void {
|
|
333
|
+
let msg: { type?: string; token?: string; sessionId?: string; lastReceivedId?: number };
|
|
334
|
+
try {
|
|
335
|
+
msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
|
|
336
|
+
} catch {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (msg.type === "hello") {
|
|
341
|
+
if (!tokensMatch(typeof msg.token === "string" ? msg.token : "", this.token)) {
|
|
342
|
+
ws.send(JSON.stringify({ type: "error", error: "unauthorized" }));
|
|
343
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "bad token");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (ws.data.authTimer) {
|
|
347
|
+
clearTimeout(ws.data.authTimer);
|
|
348
|
+
ws.data.authTimer = undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
ws.data.unregisterNotification?.();
|
|
352
|
+
ws.data.unregisterStatus?.();
|
|
353
|
+
ws.data.authed = true;
|
|
354
|
+
ws.data.sessionId =
|
|
355
|
+
typeof msg.sessionId === "string" && msg.sessionId.length > 0 ? msg.sessionId : undefined;
|
|
356
|
+
|
|
357
|
+
const lastReceivedId = Number(msg.lastReceivedId ?? 0);
|
|
358
|
+
const backlog = drainNotifications(
|
|
359
|
+
Number.isFinite(lastReceivedId) ? lastReceivedId : 0,
|
|
360
|
+
ws.data.sessionId,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const notificationSink: NotificationSink = {
|
|
364
|
+
sessionId: ws.data.sessionId,
|
|
365
|
+
send: (notification) => {
|
|
366
|
+
ws.send(JSON.stringify({ type: "notification", notification }));
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
const statusSink: StatusChangeSink = {
|
|
370
|
+
sessionId: ws.data.sessionId,
|
|
371
|
+
send: (event) => {
|
|
372
|
+
ws.send(JSON.stringify({ type: "status-changed", ...event }));
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
ws.data.unregisterNotification = registerNotificationSink(notificationSink);
|
|
376
|
+
ws.data.unregisterStatus = registerStatusChangeSink(statusSink);
|
|
377
|
+
this.sockets.add(ws);
|
|
378
|
+
|
|
379
|
+
for (const notification of backlog) {
|
|
380
|
+
ws.send(JSON.stringify({ type: "notification", notification }));
|
|
381
|
+
}
|
|
382
|
+
ws.send(JSON.stringify({ type: "hello-ack" }));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (msg.type === "ack") {
|
|
387
|
+
const lastReceivedId = Number(msg.lastReceivedId ?? 0);
|
|
388
|
+
if (Number.isFinite(lastReceivedId) && lastReceivedId > 0) {
|
|
389
|
+
drainNotifications(lastReceivedId, ws.data.sessionId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private closeWs(ws: BunServerWebSocket<WsData>): void {
|
|
395
|
+
if (ws.data.authTimer) {
|
|
396
|
+
clearTimeout(ws.data.authTimer);
|
|
397
|
+
ws.data.authTimer = undefined;
|
|
398
|
+
}
|
|
399
|
+
ws.data.unregisterNotification?.();
|
|
400
|
+
ws.data.unregisterNotification = undefined;
|
|
401
|
+
ws.data.unregisterStatus?.();
|
|
402
|
+
ws.data.unregisterStatus = undefined;
|
|
403
|
+
this.sockets.delete(ws);
|
|
404
|
+
}
|
|
405
|
+
|
|
185
406
|
private dispatch(req: IncomingMessage, res: ServerResponse): void {
|
|
186
407
|
const url = req.url ?? "";
|
|
187
408
|
|
|
@@ -208,7 +429,7 @@ export class AftRpcServer {
|
|
|
208
429
|
let body = "";
|
|
209
430
|
req.on("data", (chunk: Buffer) => {
|
|
210
431
|
body += chunk.toString();
|
|
211
|
-
if (body.length >
|
|
432
|
+
if (body.length > MAX_BODY_BYTES) {
|
|
212
433
|
res.writeHead(413);
|
|
213
434
|
res.end("Request too large");
|
|
214
435
|
req.destroy();
|
|
@@ -227,7 +448,7 @@ export class AftRpcServer {
|
|
|
227
448
|
return;
|
|
228
449
|
}
|
|
229
450
|
|
|
230
|
-
if (params.token
|
|
451
|
+
if (!tokensMatch(typeof params.token === "string" ? params.token : "", this.token)) {
|
|
231
452
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
232
453
|
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
233
454
|
return;
|
|
@@ -235,9 +456,7 @@ export class AftRpcServer {
|
|
|
235
456
|
|
|
236
457
|
const { token: _token, ...handlerParams } = params;
|
|
237
458
|
|
|
238
|
-
// Successful RPC calls are
|
|
239
|
-
// TUI dialog polls every 1.5s while open, which used to spam the log
|
|
240
|
-
// with RPC call/result pairs. Errors are still logged (real signal).
|
|
459
|
+
// Successful RPC calls are not logged to avoid noise; errors are still logged.
|
|
241
460
|
handler(handlerParams)
|
|
242
461
|
.then((result) => {
|
|
243
462
|
res.writeHead(200, { "Content-Type": "application/json" });
|