@lelouchhe/webagent 0.6.0 → 0.7.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/index.html +3 -3
- package/dist/js/app.KFQAFHA3.js +4 -0
- package/dist/js/{chunk.ON2W5X2T.js → chunk.3CCHZCG7.js} +1 -1
- package/dist/js/chunk.UMQMOGWO.js +1 -0
- package/dist/js/{login.PYIK52HN.js → login.2WA6DTGM.js} +1 -1
- package/dist/js/viewer.T32M5AZZ.js +1 -0
- package/dist/login.html +1 -1
- package/dist/share-viewer.html +3 -3
- package/lib/agent-commands.js +27 -0
- package/lib/bridge.js +11 -0
- package/lib/event-handler.js +15 -0
- package/lib/http-status.js +17 -0
- package/lib/push-service.js +2 -1
- package/lib/routes.js +278 -161
- package/lib/session-manager.js +43 -1
- package/lib/share/routes.js +106 -72
- package/lib/share/sanitize.js +2 -1
- package/package.json +1 -1
- package/dist/js/app.HJKQURHR.js +0 -4
- package/dist/js/chunk.D4ZYHJAM.js +0 -1
- package/dist/js/viewer.VY4PO2O5.js +0 -1
package/lib/routes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createWriteStream } from "node:fs";
|
|
|
10
10
|
import { handleShareRoutes } from "./share/routes.js";
|
|
11
11
|
import { authenticate, isWhitelistedPath } from "./auth-middleware.js";
|
|
12
12
|
import { enrichStoredEventsForDisplay } from "./attachment-labels.js";
|
|
13
|
+
import { agentCommandToken, resolveAgentCommand } from "./agent-commands.js";
|
|
13
14
|
import { log } from "./log.js";
|
|
14
15
|
const rlog = log.scope("routes");
|
|
15
16
|
const plog = rlog.scope("prompt");
|
|
@@ -18,6 +19,7 @@ const mlog = rlog.scope("msg");
|
|
|
18
19
|
import { signAttachmentUrl, verifyAttachmentSig, reSignAttachmentUrlsInJson, } from "./auth.js";
|
|
19
20
|
import { buildContentDisposition, classifyKind, isInlineMime, mimeToExt, normalizeDisplayName, sniffMime, } from "./attachments.js";
|
|
20
21
|
import { readImageDimensions } from "./image-dimensions.js";
|
|
22
|
+
import { HTTP_STATUS } from "./http-status.js";
|
|
21
23
|
const IS_WIN = process.platform === "win32";
|
|
22
24
|
const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
23
25
|
const MIME = {
|
|
@@ -172,7 +174,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
172
174
|
const { store, dataDir, limits, sessions } = deps;
|
|
173
175
|
const fileUploadLimit = limits.file_upload ?? 52_428_800;
|
|
174
176
|
if (!store.getSession(sessionId)) {
|
|
175
|
-
json(res,
|
|
177
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
176
178
|
return;
|
|
177
179
|
}
|
|
178
180
|
const dir = join(dataDir, "sessions", sessionId, "attachments");
|
|
@@ -233,7 +235,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
233
235
|
});
|
|
234
236
|
}
|
|
235
237
|
catch {
|
|
236
|
-
void finish(
|
|
238
|
+
void finish(HTTP_STATUS.BAD_REQUEST, { error: "Invalid multipart" });
|
|
237
239
|
return;
|
|
238
240
|
}
|
|
239
241
|
bb.on("file", (fieldName, stream, info) => {
|
|
@@ -246,7 +248,9 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
246
248
|
sawFile = true;
|
|
247
249
|
if (fieldName !== "file") {
|
|
248
250
|
stream.resume();
|
|
249
|
-
void finish(
|
|
251
|
+
void finish(HTTP_STATUS.BAD_REQUEST, {
|
|
252
|
+
error: "Unexpected field name",
|
|
253
|
+
});
|
|
250
254
|
return;
|
|
251
255
|
}
|
|
252
256
|
fileMime = (info.mimeType || "application/octet-stream").toLowerCase();
|
|
@@ -295,22 +299,28 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
295
299
|
await writeDone;
|
|
296
300
|
if (aborted) {
|
|
297
301
|
await cleanupTmp();
|
|
298
|
-
await finish(
|
|
302
|
+
await finish(HTTP_STATUS.BAD_REQUEST, { error: "Upload aborted" });
|
|
299
303
|
return;
|
|
300
304
|
}
|
|
301
305
|
if (limitExceeded) {
|
|
302
306
|
await cleanupTmp();
|
|
303
|
-
await finish(
|
|
307
|
+
await finish(HTTP_STATUS.PAYLOAD_TOO_LARGE, {
|
|
308
|
+
error: "Upload too large",
|
|
309
|
+
});
|
|
304
310
|
return;
|
|
305
311
|
}
|
|
306
312
|
if (writeError) {
|
|
307
313
|
await cleanupTmp();
|
|
308
|
-
await finish(
|
|
314
|
+
await finish(HTTP_STATUS.INTERNAL_SERVER_ERROR, {
|
|
315
|
+
error: "Upload failed",
|
|
316
|
+
});
|
|
309
317
|
return;
|
|
310
318
|
}
|
|
311
319
|
if (!sawFile || !tmpPath || !finalPath || !displayName) {
|
|
312
320
|
await cleanupTmp();
|
|
313
|
-
await finish(
|
|
321
|
+
await finish(HTTP_STATUS.BAD_REQUEST, {
|
|
322
|
+
error: "Missing file part",
|
|
323
|
+
});
|
|
314
324
|
return;
|
|
315
325
|
}
|
|
316
326
|
// Sniff the actual mime from file content (magic bytes + UTF-8
|
|
@@ -362,7 +372,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
362
372
|
const fileUrl = deps.attachmentSecret
|
|
363
373
|
? `${basePath}?${signAttachmentUrl(basePath, deps.attachmentSecret, 3600)}`
|
|
364
374
|
: basePath;
|
|
365
|
-
await finish(
|
|
375
|
+
await finish(HTTP_STATUS.OK, {
|
|
366
376
|
attachmentId: row.id,
|
|
367
377
|
displayName: row.name,
|
|
368
378
|
mimeType: row.mime,
|
|
@@ -376,7 +386,9 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
|
|
|
376
386
|
}
|
|
377
387
|
catch (err) {
|
|
378
388
|
await cleanupTmp();
|
|
379
|
-
await finish(
|
|
389
|
+
await finish(HTTP_STATUS.INTERNAL_SERVER_ERROR, {
|
|
390
|
+
error: errorMessage(err),
|
|
391
|
+
});
|
|
380
392
|
}
|
|
381
393
|
})();
|
|
382
394
|
});
|
|
@@ -395,7 +407,7 @@ export function createRequestHandler(deps) {
|
|
|
395
407
|
if (!isWhitelistedPath(method, path)) {
|
|
396
408
|
const result = authenticate(req.headers, deps.authStore);
|
|
397
409
|
if (!result.ok) {
|
|
398
|
-
res.writeHead(
|
|
410
|
+
res.writeHead(HTTP_STATUS.UNAUTHORIZED, {
|
|
399
411
|
"Content-Type": "application/json",
|
|
400
412
|
"WWW-Authenticate": "Bearer",
|
|
401
413
|
});
|
|
@@ -427,7 +439,7 @@ export function createRequestHandler(deps) {
|
|
|
427
439
|
res.setHeader("Content-Type", "application/json");
|
|
428
440
|
// GET /api/v1 — discovery endpoint
|
|
429
441
|
if (url === "/api/v1" && req.method === "GET") {
|
|
430
|
-
json(res,
|
|
442
|
+
json(res, HTTP_STATUS.OK, {
|
|
431
443
|
version: "v1",
|
|
432
444
|
endpoints: {
|
|
433
445
|
sessions: "/api/v1/sessions",
|
|
@@ -452,7 +464,7 @@ export function createRequestHandler(deps) {
|
|
|
452
464
|
}
|
|
453
465
|
// --- GET /api/v1/config ---
|
|
454
466
|
if (url === "/api/v1/config" && req.method === "GET") {
|
|
455
|
-
json(res,
|
|
467
|
+
json(res, HTTP_STATUS.OK, {
|
|
456
468
|
configOptions: sessions?.cachedConfigOptions ?? [],
|
|
457
469
|
cancelTimeout: deps.limits.cancel_timeout ?? 0,
|
|
458
470
|
recentPathsLimit: deps.limits.recent_paths ?? 10,
|
|
@@ -469,12 +481,12 @@ export function createRequestHandler(deps) {
|
|
|
469
481
|
limit: isNaN(limit) ? 0 : limit,
|
|
470
482
|
ttlDays,
|
|
471
483
|
});
|
|
472
|
-
json(res,
|
|
484
|
+
json(res, HTTP_STATUS.OK, paths);
|
|
473
485
|
return;
|
|
474
486
|
}
|
|
475
487
|
// GET /api/v1/version
|
|
476
488
|
if (url === "/api/v1/version" && req.method === "GET") {
|
|
477
|
-
json(res,
|
|
489
|
+
json(res, HTTP_STATUS.OK, {
|
|
478
490
|
server: deps.serverVersion ?? "unknown",
|
|
479
491
|
agent: sessions?.agentInfo ?? null,
|
|
480
492
|
});
|
|
@@ -484,10 +496,10 @@ export function createRequestHandler(deps) {
|
|
|
484
496
|
if (url === "/api/v1/auth/verify" && req.method === "GET") {
|
|
485
497
|
const principal = principalByRequest.get(req);
|
|
486
498
|
if (!principal) {
|
|
487
|
-
json(res,
|
|
499
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
|
|
488
500
|
return;
|
|
489
501
|
}
|
|
490
|
-
json(res,
|
|
502
|
+
json(res, HTTP_STATUS.OK, {
|
|
491
503
|
ok: true,
|
|
492
504
|
name: principal.name,
|
|
493
505
|
scope: principal.scope,
|
|
@@ -498,18 +510,20 @@ export function createRequestHandler(deps) {
|
|
|
498
510
|
if (url === "/api/v1/sse-ticket" && req.method === "POST") {
|
|
499
511
|
const principal = principalByRequest.get(req);
|
|
500
512
|
if (!principal) {
|
|
501
|
-
json(res,
|
|
513
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
|
|
502
514
|
return;
|
|
503
515
|
}
|
|
504
516
|
if (!deps.ticketStore) {
|
|
505
|
-
json(res,
|
|
517
|
+
json(res, HTTP_STATUS.NOT_IMPLEMENTED, {
|
|
518
|
+
error: "SSE not available",
|
|
519
|
+
});
|
|
506
520
|
return;
|
|
507
521
|
}
|
|
508
522
|
const ticket = deps.ticketStore.mint({
|
|
509
523
|
tokenName: principal.name,
|
|
510
524
|
scope: principal.scope,
|
|
511
525
|
});
|
|
512
|
-
json(res,
|
|
526
|
+
json(res, HTTP_STATUS.OK, { ticket, expiresIn: 60 });
|
|
513
527
|
return;
|
|
514
528
|
}
|
|
515
529
|
// --- Token management (admin scope) ---
|
|
@@ -519,7 +533,7 @@ export function createRequestHandler(deps) {
|
|
|
519
533
|
if (url === "/api/v1/tokens" && req.method === "GET") {
|
|
520
534
|
const principal = principalByRequest.get(req);
|
|
521
535
|
if (!deps.authStore || !principal) {
|
|
522
|
-
json(res,
|
|
536
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
|
|
523
537
|
return;
|
|
524
538
|
}
|
|
525
539
|
const all = deps.authStore.list();
|
|
@@ -533,18 +547,18 @@ export function createRequestHandler(deps) {
|
|
|
533
547
|
lastUsedAt: t.lastUsedAt,
|
|
534
548
|
isSelf: t.name === principal.name,
|
|
535
549
|
}));
|
|
536
|
-
json(res,
|
|
550
|
+
json(res, HTTP_STATUS.OK, list);
|
|
537
551
|
return;
|
|
538
552
|
}
|
|
539
553
|
// POST /api/v1/tokens — create new api-scope token, return raw value once
|
|
540
554
|
if (url === "/api/v1/tokens" && req.method === "POST") {
|
|
541
555
|
const principal = principalByRequest.get(req);
|
|
542
556
|
if (!deps.authStore || !principal) {
|
|
543
|
-
json(res,
|
|
557
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
|
|
544
558
|
return;
|
|
545
559
|
}
|
|
546
560
|
if (principal.scope !== "admin") {
|
|
547
|
-
json(res,
|
|
561
|
+
json(res, HTTP_STATUS.FORBIDDEN, { error: "Forbidden" });
|
|
548
562
|
return;
|
|
549
563
|
}
|
|
550
564
|
let body;
|
|
@@ -552,13 +566,13 @@ export function createRequestHandler(deps) {
|
|
|
552
566
|
body = JSON.parse(await readBody(req));
|
|
553
567
|
}
|
|
554
568
|
catch {
|
|
555
|
-
json(res,
|
|
569
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
556
570
|
return;
|
|
557
571
|
}
|
|
558
572
|
const name = typeof body.name === "string" ? body.name : "";
|
|
559
573
|
try {
|
|
560
574
|
const created = await deps.authStore.addToken(name, "api");
|
|
561
|
-
json(res,
|
|
575
|
+
json(res, HTTP_STATUS.CREATED, {
|
|
562
576
|
token: created.token,
|
|
563
577
|
name: created.record.name,
|
|
564
578
|
scope: created.record.scope,
|
|
@@ -567,10 +581,10 @@ export function createRequestHandler(deps) {
|
|
|
567
581
|
catch (err) {
|
|
568
582
|
const msg = errorMessage(err);
|
|
569
583
|
if (/already exists|duplicate/i.test(msg)) {
|
|
570
|
-
json(res,
|
|
584
|
+
json(res, HTTP_STATUS.CONFLICT, { error: msg });
|
|
571
585
|
}
|
|
572
586
|
else {
|
|
573
|
-
json(res,
|
|
587
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: msg });
|
|
574
588
|
}
|
|
575
589
|
}
|
|
576
590
|
return;
|
|
@@ -580,33 +594,35 @@ export function createRequestHandler(deps) {
|
|
|
580
594
|
if (tokenDelMatch && req.method === "DELETE") {
|
|
581
595
|
const principal = principalByRequest.get(req);
|
|
582
596
|
if (!deps.authStore || !principal) {
|
|
583
|
-
json(res,
|
|
597
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
|
|
584
598
|
return;
|
|
585
599
|
}
|
|
586
600
|
if (principal.scope !== "admin") {
|
|
587
|
-
json(res,
|
|
601
|
+
json(res, HTTP_STATUS.FORBIDDEN, { error: "Forbidden" });
|
|
588
602
|
return;
|
|
589
603
|
}
|
|
590
604
|
const name = decodeURIComponent(tokenDelMatch[1]);
|
|
591
605
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
|
|
592
|
-
json(res,
|
|
606
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid token name" });
|
|
593
607
|
return;
|
|
594
608
|
}
|
|
595
609
|
if (name === principal.name) {
|
|
596
|
-
json(res,
|
|
610
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
611
|
+
error: "Cannot revoke the token you are using",
|
|
612
|
+
});
|
|
597
613
|
return;
|
|
598
614
|
}
|
|
599
615
|
try {
|
|
600
616
|
const ok = await deps.authStore.revokeToken(name);
|
|
601
617
|
if (!ok) {
|
|
602
|
-
json(res,
|
|
618
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Token not found" });
|
|
603
619
|
return;
|
|
604
620
|
}
|
|
605
|
-
res.writeHead(
|
|
621
|
+
res.writeHead(HTTP_STATUS.NO_CONTENT);
|
|
606
622
|
res.end();
|
|
607
623
|
}
|
|
608
624
|
catch (err) {
|
|
609
|
-
json(res,
|
|
625
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: errorMessage(err) });
|
|
610
626
|
}
|
|
611
627
|
return;
|
|
612
628
|
}
|
|
@@ -614,19 +630,23 @@ export function createRequestHandler(deps) {
|
|
|
614
630
|
if (url === "/api/v1/bridge/reload" && req.method === "POST") {
|
|
615
631
|
const bridge = getBridge?.();
|
|
616
632
|
if (!bridge) {
|
|
617
|
-
json(res,
|
|
633
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
634
|
+
error: "Agent not ready yet",
|
|
635
|
+
});
|
|
618
636
|
return;
|
|
619
637
|
}
|
|
620
638
|
if (bridge.reloading) {
|
|
621
|
-
json(res,
|
|
639
|
+
json(res, HTTP_STATUS.CONFLICT, { error: "Already reloading" });
|
|
622
640
|
return;
|
|
623
641
|
}
|
|
624
642
|
try {
|
|
625
643
|
await bridge.restart(sessions, titleService);
|
|
626
|
-
json(res,
|
|
644
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
627
645
|
}
|
|
628
646
|
catch (err) {
|
|
629
|
-
json(res,
|
|
647
|
+
json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, {
|
|
648
|
+
error: errorMessage(err),
|
|
649
|
+
});
|
|
630
650
|
}
|
|
631
651
|
return;
|
|
632
652
|
}
|
|
@@ -636,7 +656,7 @@ export function createRequestHandler(deps) {
|
|
|
636
656
|
if (permListMatch && req.method === "GET") {
|
|
637
657
|
const sessionId = decodeURIComponent(permListMatch[1]);
|
|
638
658
|
const perms = sessions?.getPendingPermissions(sessionId) ?? [];
|
|
639
|
-
json(res,
|
|
659
|
+
json(res, HTTP_STATUS.OK, perms);
|
|
640
660
|
return;
|
|
641
661
|
}
|
|
642
662
|
// POST /api/v1/sessions/:id/permissions/:reqId
|
|
@@ -649,16 +669,18 @@ export function createRequestHandler(deps) {
|
|
|
649
669
|
return;
|
|
650
670
|
const perm = sessions?.pendingPermissions.get(requestId);
|
|
651
671
|
if (!perm) {
|
|
652
|
-
json(res,
|
|
672
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Permission not found" });
|
|
653
673
|
return;
|
|
654
674
|
}
|
|
655
675
|
if (perm.sessionId !== sessionId) {
|
|
656
|
-
json(res,
|
|
676
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Session ID mismatch" });
|
|
657
677
|
return;
|
|
658
678
|
}
|
|
659
679
|
const bridge = getBridge?.();
|
|
660
680
|
if (!bridge) {
|
|
661
|
-
json(res,
|
|
681
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
682
|
+
error: "Agent not ready yet",
|
|
683
|
+
});
|
|
662
684
|
return;
|
|
663
685
|
}
|
|
664
686
|
let body;
|
|
@@ -666,11 +688,13 @@ export function createRequestHandler(deps) {
|
|
|
666
688
|
body = JSON.parse(await readBody(req));
|
|
667
689
|
}
|
|
668
690
|
catch {
|
|
669
|
-
json(res,
|
|
691
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
670
692
|
return;
|
|
671
693
|
}
|
|
672
694
|
if (!body.optionId && !body.denied) {
|
|
673
|
-
json(res,
|
|
695
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
696
|
+
error: "Provide optionId or denied:true",
|
|
697
|
+
});
|
|
674
698
|
return;
|
|
675
699
|
}
|
|
676
700
|
const denied = Boolean(body.denied);
|
|
@@ -699,8 +723,8 @@ export function createRequestHandler(deps) {
|
|
|
699
723
|
void deps.pushService.sendClose(`sess-${perm.sessionId}-perm-${requestId}`);
|
|
700
724
|
}
|
|
701
725
|
const okBody = { ok: true };
|
|
702
|
-
saveClientOpResult(store, opId, sessionId,
|
|
703
|
-
json(res,
|
|
726
|
+
saveClientOpResult(store, opId, sessionId, HTTP_STATUS.OK, okBody);
|
|
727
|
+
json(res, HTTP_STATUS.OK, okBody);
|
|
704
728
|
return;
|
|
705
729
|
}
|
|
706
730
|
// --- POST /api/v1/sessions/:id/cancel ---
|
|
@@ -709,12 +733,14 @@ export function createRequestHandler(deps) {
|
|
|
709
733
|
const sessionId = decodeURIComponent(cancelMatch[1]);
|
|
710
734
|
const session = store.getSession(sessionId);
|
|
711
735
|
if (!session) {
|
|
712
|
-
json(res,
|
|
736
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
713
737
|
return;
|
|
714
738
|
}
|
|
715
739
|
const bridge = getBridge?.();
|
|
716
740
|
if (!bridge) {
|
|
717
|
-
json(res,
|
|
741
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
742
|
+
error: "Agent not ready yet",
|
|
743
|
+
});
|
|
718
744
|
return;
|
|
719
745
|
}
|
|
720
746
|
const { opId, replayed } = tryReplayClientOp(req, res, store, sessionId);
|
|
@@ -739,8 +765,8 @@ export function createRequestHandler(deps) {
|
|
|
739
765
|
sessions.state.armCancelSafety(sessionId, cancelTimeout);
|
|
740
766
|
sessions?.syncBusy(sessionId);
|
|
741
767
|
const okBody = { ok: true };
|
|
742
|
-
saveClientOpResult(store, opId, sessionId,
|
|
743
|
-
json(res,
|
|
768
|
+
saveClientOpResult(store, opId, sessionId, HTTP_STATUS.OK, okBody);
|
|
769
|
+
json(res, HTTP_STATUS.OK, okBody);
|
|
744
770
|
return;
|
|
745
771
|
}
|
|
746
772
|
// --- GET /api/v1/sessions/:id/status ---
|
|
@@ -749,12 +775,12 @@ export function createRequestHandler(deps) {
|
|
|
749
775
|
const sessionId = decodeURIComponent(statusMatch[1]);
|
|
750
776
|
const session = store.getSession(sessionId);
|
|
751
777
|
if (!session) {
|
|
752
|
-
json(res,
|
|
778
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
753
779
|
return;
|
|
754
780
|
}
|
|
755
781
|
const busyKind = sessions?.getBusyKind(sessionId) ?? null;
|
|
756
782
|
const pendingPerms = sessions?.getPendingPermissions(sessionId) ?? [];
|
|
757
|
-
json(res,
|
|
783
|
+
json(res, HTTP_STATUS.OK, {
|
|
758
784
|
busy: busyKind != null,
|
|
759
785
|
busyKind,
|
|
760
786
|
pendingPermissions: pendingPerms,
|
|
@@ -771,20 +797,37 @@ export function createRequestHandler(deps) {
|
|
|
771
797
|
const sessionId = decodeURIComponent(snapshotMatch[1]);
|
|
772
798
|
const session = store.getSession(sessionId);
|
|
773
799
|
if (!session) {
|
|
774
|
-
json(res,
|
|
800
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
775
801
|
return;
|
|
776
802
|
}
|
|
777
803
|
if (!sessions) {
|
|
778
|
-
json(res,
|
|
804
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
805
|
+
error: "Session manager not available",
|
|
806
|
+
});
|
|
779
807
|
return;
|
|
780
808
|
}
|
|
809
|
+
const bridge = getBridge?.();
|
|
810
|
+
if (bridge && !sessions.liveSessions.has(sessionId)) {
|
|
811
|
+
try {
|
|
812
|
+
// Command discovery happens during session/load. Snapshot is the
|
|
813
|
+
// authoritative hydration boundary, so it must join any in-flight
|
|
814
|
+
// restore before reading the per-session command state.
|
|
815
|
+
await sessions.ensureResumed(bridge, sessionId);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
819
|
+
error: "Failed to restore session",
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
781
824
|
// Make sure runtime reflects the current activePrompts/bash state even
|
|
782
825
|
// if no patch has been emitted yet for this session.
|
|
783
826
|
sessions.syncBusy(sessionId);
|
|
784
827
|
sessions.syncPendingPermissions(sessionId);
|
|
785
828
|
const runtimeState = sessions.state.getState(sessionId);
|
|
786
829
|
const lastEventSeq = store.getLastEventSeq(sessionId);
|
|
787
|
-
json(res,
|
|
830
|
+
json(res, HTTP_STATUS.OK, {
|
|
788
831
|
version: 1,
|
|
789
832
|
seq: runtimeState.seq,
|
|
790
833
|
session: {
|
|
@@ -797,6 +840,7 @@ export function createRequestHandler(deps) {
|
|
|
797
840
|
lastEventSeq,
|
|
798
841
|
},
|
|
799
842
|
runtime: runtimeState.runtime,
|
|
843
|
+
agentCommands: sessions.getAgentCommands(sessionId),
|
|
800
844
|
}, req);
|
|
801
845
|
return;
|
|
802
846
|
}
|
|
@@ -809,32 +853,36 @@ export function createRequestHandler(deps) {
|
|
|
809
853
|
if (!session) {
|
|
810
854
|
logPromptRejectBeforeSave({
|
|
811
855
|
sessionId,
|
|
812
|
-
status:
|
|
856
|
+
status: HTTP_STATUS.NOT_FOUND,
|
|
813
857
|
reason: "session_not_found",
|
|
814
858
|
opId: requestOpId,
|
|
815
859
|
});
|
|
816
|
-
json(res,
|
|
860
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
817
861
|
return;
|
|
818
862
|
}
|
|
819
863
|
const bridge = getBridge?.();
|
|
820
864
|
if (!bridge) {
|
|
821
865
|
logPromptRejectBeforeSave({
|
|
822
866
|
sessionId,
|
|
823
|
-
status:
|
|
867
|
+
status: HTTP_STATUS.SERVICE_UNAVAILABLE,
|
|
824
868
|
reason: "agent_not_ready",
|
|
825
869
|
opId: requestOpId,
|
|
826
870
|
});
|
|
827
|
-
json(res,
|
|
871
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
872
|
+
error: "Agent not ready yet",
|
|
873
|
+
});
|
|
828
874
|
return;
|
|
829
875
|
}
|
|
830
876
|
if (!sessions) {
|
|
831
877
|
logPromptRejectBeforeSave({
|
|
832
878
|
sessionId,
|
|
833
|
-
status:
|
|
879
|
+
status: HTTP_STATUS.SERVICE_UNAVAILABLE,
|
|
834
880
|
reason: "session_manager_unavailable",
|
|
835
881
|
opId: requestOpId,
|
|
836
882
|
});
|
|
837
|
-
json(res,
|
|
883
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
884
|
+
error: "Session manager not available",
|
|
885
|
+
});
|
|
838
886
|
return;
|
|
839
887
|
}
|
|
840
888
|
const { opId, replayed } = tryReplayClientOp(req, res, store, sessionId);
|
|
@@ -847,12 +895,12 @@ export function createRequestHandler(deps) {
|
|
|
847
895
|
catch (err) {
|
|
848
896
|
logPromptRejectBeforeSave({
|
|
849
897
|
sessionId,
|
|
850
|
-
status:
|
|
898
|
+
status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
851
899
|
reason: "resume_failed",
|
|
852
900
|
opId,
|
|
853
901
|
error: errorMessage(err),
|
|
854
902
|
});
|
|
855
|
-
json(res,
|
|
903
|
+
json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, {
|
|
856
904
|
error: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`,
|
|
857
905
|
});
|
|
858
906
|
return;
|
|
@@ -862,12 +910,15 @@ export function createRequestHandler(deps) {
|
|
|
862
910
|
if (busyKind) {
|
|
863
911
|
logPromptRejectBeforeSave({
|
|
864
912
|
sessionId,
|
|
865
|
-
status:
|
|
913
|
+
status: HTTP_STATUS.CONFLICT,
|
|
866
914
|
reason: "session_busy",
|
|
867
915
|
opId,
|
|
868
916
|
busyKind,
|
|
869
917
|
});
|
|
870
|
-
json(res,
|
|
918
|
+
json(res, HTTP_STATUS.CONFLICT, {
|
|
919
|
+
error: "Session is busy",
|
|
920
|
+
busyKind,
|
|
921
|
+
});
|
|
871
922
|
return;
|
|
872
923
|
}
|
|
873
924
|
let body;
|
|
@@ -877,17 +928,17 @@ export function createRequestHandler(deps) {
|
|
|
877
928
|
catch {
|
|
878
929
|
logPromptRejectBeforeSave({
|
|
879
930
|
sessionId,
|
|
880
|
-
status:
|
|
931
|
+
status: HTTP_STATUS.BAD_REQUEST,
|
|
881
932
|
reason: "invalid_json",
|
|
882
933
|
opId,
|
|
883
934
|
});
|
|
884
|
-
json(res,
|
|
935
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
885
936
|
return;
|
|
886
937
|
}
|
|
887
938
|
if (!body.text) {
|
|
888
939
|
logPromptRejectBeforeSave({
|
|
889
940
|
sessionId,
|
|
890
|
-
status:
|
|
941
|
+
status: HTTP_STATUS.BAD_REQUEST,
|
|
891
942
|
reason: "missing_text",
|
|
892
943
|
opId,
|
|
893
944
|
textLength: 0,
|
|
@@ -895,7 +946,9 @@ export function createRequestHandler(deps) {
|
|
|
895
946
|
? body.attachments.length
|
|
896
947
|
: undefined,
|
|
897
948
|
});
|
|
898
|
-
json(res,
|
|
949
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
950
|
+
error: "Missing required field: text",
|
|
951
|
+
});
|
|
899
952
|
return;
|
|
900
953
|
}
|
|
901
954
|
// Validate attachment shape: client must NEVER supply uri/data/path,
|
|
@@ -906,12 +959,14 @@ export function createRequestHandler(deps) {
|
|
|
906
959
|
if (!Array.isArray(attachments)) {
|
|
907
960
|
logPromptRejectBeforeSave({
|
|
908
961
|
sessionId,
|
|
909
|
-
status:
|
|
962
|
+
status: HTTP_STATUS.BAD_REQUEST,
|
|
910
963
|
reason: "attachments_not_array",
|
|
911
964
|
opId,
|
|
912
965
|
textLength: body.text.length,
|
|
913
966
|
});
|
|
914
|
-
json(res,
|
|
967
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
968
|
+
error: "attachments must be an array",
|
|
969
|
+
});
|
|
915
970
|
return;
|
|
916
971
|
}
|
|
917
972
|
for (const raw of attachments) {
|
|
@@ -924,13 +979,15 @@ export function createRequestHandler(deps) {
|
|
|
924
979
|
typeof att.mimeType !== "string") {
|
|
925
980
|
logPromptRejectBeforeSave({
|
|
926
981
|
sessionId,
|
|
927
|
-
status:
|
|
982
|
+
status: HTTP_STATUS.BAD_REQUEST,
|
|
928
983
|
reason: "invalid_attachment_entry",
|
|
929
984
|
opId,
|
|
930
985
|
textLength: body.text.length,
|
|
931
986
|
attachmentCount: attachments.length,
|
|
932
987
|
});
|
|
933
|
-
json(res,
|
|
988
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
989
|
+
error: "Invalid attachment entry",
|
|
990
|
+
});
|
|
934
991
|
return;
|
|
935
992
|
}
|
|
936
993
|
if (typeof att.uri === "string" ||
|
|
@@ -940,19 +997,41 @@ export function createRequestHandler(deps) {
|
|
|
940
997
|
typeof att.height === "number") {
|
|
941
998
|
logPromptRejectBeforeSave({
|
|
942
999
|
sessionId,
|
|
943
|
-
status:
|
|
1000
|
+
status: HTTP_STATUS.BAD_REQUEST,
|
|
944
1001
|
reason: "client_supplied_attachment_data",
|
|
945
1002
|
opId,
|
|
946
1003
|
textLength: body.text.length,
|
|
947
1004
|
attachmentCount: attachments.length,
|
|
948
1005
|
});
|
|
949
|
-
json(res,
|
|
1006
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
950
1007
|
error: "Client must not supply uri/data/path/width/height",
|
|
951
1008
|
});
|
|
952
1009
|
return;
|
|
953
1010
|
}
|
|
954
1011
|
}
|
|
955
1012
|
}
|
|
1013
|
+
let agentText = body.text;
|
|
1014
|
+
if (body.text.startsWith("//")) {
|
|
1015
|
+
const resolved = resolveAgentCommand(body.text, sessions.getAgentCommands(sessionId).commands);
|
|
1016
|
+
if (!resolved) {
|
|
1017
|
+
const command = agentCommandToken(body.text);
|
|
1018
|
+
logPromptRejectBeforeSave({
|
|
1019
|
+
sessionId,
|
|
1020
|
+
status: HTTP_STATUS.UNPROCESSABLE_CONTENT,
|
|
1021
|
+
reason: "unknown_command",
|
|
1022
|
+
opId,
|
|
1023
|
+
textLength: body.text.length,
|
|
1024
|
+
attachmentCount: attachments?.length,
|
|
1025
|
+
});
|
|
1026
|
+
json(res, HTTP_STATUS.UNPROCESSABLE_CONTENT, {
|
|
1027
|
+
error: "Unknown command",
|
|
1028
|
+
command,
|
|
1029
|
+
prefix: "//",
|
|
1030
|
+
});
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
agentText = resolved.agentText;
|
|
1034
|
+
}
|
|
956
1035
|
// Stored shape mirrors the wire shape PLUS a server-derived `path`
|
|
957
1036
|
// for renderers. The path is the unsigned base URL
|
|
958
1037
|
// (`/api/v1/sessions/<sid>/attachments/<filename>`); reSign on
|
|
@@ -1010,7 +1089,7 @@ export function createRequestHandler(deps) {
|
|
|
1010
1089
|
sessions.activePrompts.add(sessionId);
|
|
1011
1090
|
sessions.syncBusy(sessionId);
|
|
1012
1091
|
bridge
|
|
1013
|
-
.prompt(sessionId,
|
|
1092
|
+
.prompt(sessionId, agentText, attachments)
|
|
1014
1093
|
.catch((err) => {
|
|
1015
1094
|
plog.error("error", { sessionId, error: err });
|
|
1016
1095
|
})
|
|
@@ -1019,8 +1098,8 @@ export function createRequestHandler(deps) {
|
|
|
1019
1098
|
sessions.syncBusy(sessionId);
|
|
1020
1099
|
});
|
|
1021
1100
|
const acceptedBody = { status: "accepted" };
|
|
1022
|
-
saveClientOpResult(store, opId, sessionId,
|
|
1023
|
-
json(res,
|
|
1101
|
+
saveClientOpResult(store, opId, sessionId, HTTP_STATUS.ACCEPTED, acceptedBody);
|
|
1102
|
+
json(res, HTTP_STATUS.ACCEPTED, acceptedBody);
|
|
1024
1103
|
return;
|
|
1025
1104
|
}
|
|
1026
1105
|
// --- POST /api/v1/sessions/:id/bash ---
|
|
@@ -1029,15 +1108,17 @@ export function createRequestHandler(deps) {
|
|
|
1029
1108
|
const sessionId = decodeURIComponent(bashMatch[1]);
|
|
1030
1109
|
const session = store.getSession(sessionId);
|
|
1031
1110
|
if (!session) {
|
|
1032
|
-
json(res,
|
|
1111
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1033
1112
|
return;
|
|
1034
1113
|
}
|
|
1035
1114
|
if (!sessions) {
|
|
1036
|
-
json(res,
|
|
1115
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
1116
|
+
error: "Session manager not available",
|
|
1117
|
+
});
|
|
1037
1118
|
return;
|
|
1038
1119
|
}
|
|
1039
1120
|
if (sessions.runningBashProcs.has(sessionId)) {
|
|
1040
|
-
json(res,
|
|
1121
|
+
json(res, HTTP_STATUS.CONFLICT, {
|
|
1041
1122
|
error: "A bash command is already running in this session",
|
|
1042
1123
|
});
|
|
1043
1124
|
return;
|
|
@@ -1047,11 +1128,13 @@ export function createRequestHandler(deps) {
|
|
|
1047
1128
|
body = JSON.parse(await readBody(req));
|
|
1048
1129
|
}
|
|
1049
1130
|
catch {
|
|
1050
|
-
json(res,
|
|
1131
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1051
1132
|
return;
|
|
1052
1133
|
}
|
|
1053
1134
|
if (!body.command) {
|
|
1054
|
-
json(res,
|
|
1135
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1136
|
+
error: "Missing required field: command",
|
|
1137
|
+
});
|
|
1055
1138
|
return;
|
|
1056
1139
|
}
|
|
1057
1140
|
const cwd = sessions.getSessionCwd(sessionId);
|
|
@@ -1128,7 +1211,7 @@ export function createRequestHandler(deps) {
|
|
|
1128
1211
|
};
|
|
1129
1212
|
sseManager.broadcast(bashErrEvent);
|
|
1130
1213
|
});
|
|
1131
|
-
json(res,
|
|
1214
|
+
json(res, HTTP_STATUS.ACCEPTED, { status: "accepted" });
|
|
1132
1215
|
return;
|
|
1133
1216
|
}
|
|
1134
1217
|
// --- POST /api/v1/sessions/:id/bash/cancel ---
|
|
@@ -1137,11 +1220,11 @@ export function createRequestHandler(deps) {
|
|
|
1137
1220
|
const sessionId = decodeURIComponent(bashCancelMatch[1]);
|
|
1138
1221
|
const session = store.getSession(sessionId);
|
|
1139
1222
|
if (!session) {
|
|
1140
|
-
json(res,
|
|
1223
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1141
1224
|
return;
|
|
1142
1225
|
}
|
|
1143
1226
|
interruptBashProc(sessions?.runningBashProcs.get(sessionId));
|
|
1144
|
-
json(res,
|
|
1227
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
1145
1228
|
return;
|
|
1146
1229
|
}
|
|
1147
1230
|
// --- PUT /api/v1/sessions/:id/{model,mode,reasoning-effort} ---
|
|
@@ -1155,12 +1238,14 @@ export function createRequestHandler(deps) {
|
|
|
1155
1238
|
const configId = configPath.replace(/-/g, "_");
|
|
1156
1239
|
const session = store.getSession(sessionId);
|
|
1157
1240
|
if (!session) {
|
|
1158
|
-
json(res,
|
|
1241
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1159
1242
|
return;
|
|
1160
1243
|
}
|
|
1161
1244
|
const bridge = getBridge?.();
|
|
1162
1245
|
if (!bridge) {
|
|
1163
|
-
json(res,
|
|
1246
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
1247
|
+
error: "Agent not ready yet",
|
|
1248
|
+
});
|
|
1164
1249
|
return;
|
|
1165
1250
|
}
|
|
1166
1251
|
let body;
|
|
@@ -1168,11 +1253,13 @@ export function createRequestHandler(deps) {
|
|
|
1168
1253
|
body = JSON.parse(await readBody(req));
|
|
1169
1254
|
}
|
|
1170
1255
|
catch {
|
|
1171
|
-
json(res,
|
|
1256
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1172
1257
|
return;
|
|
1173
1258
|
}
|
|
1174
1259
|
if (body.value === undefined) {
|
|
1175
|
-
json(res,
|
|
1260
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1261
|
+
error: "Missing required field: value",
|
|
1262
|
+
});
|
|
1176
1263
|
return;
|
|
1177
1264
|
}
|
|
1178
1265
|
try {
|
|
@@ -1193,10 +1280,10 @@ export function createRequestHandler(deps) {
|
|
|
1193
1280
|
configId,
|
|
1194
1281
|
value: body.value,
|
|
1195
1282
|
});
|
|
1196
|
-
json(res,
|
|
1283
|
+
json(res, HTTP_STATUS.OK, { configOptions });
|
|
1197
1284
|
}
|
|
1198
1285
|
catch (err) {
|
|
1199
|
-
json(res,
|
|
1286
|
+
json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, {
|
|
1200
1287
|
error: `Failed to set ${configId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1201
1288
|
});
|
|
1202
1289
|
}
|
|
@@ -1208,7 +1295,7 @@ export function createRequestHandler(deps) {
|
|
|
1208
1295
|
const sessionId = decodeURIComponent(titlePutMatch[1]);
|
|
1209
1296
|
const session = store.getSession(sessionId);
|
|
1210
1297
|
if (!session) {
|
|
1211
|
-
json(res,
|
|
1298
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1212
1299
|
return;
|
|
1213
1300
|
}
|
|
1214
1301
|
let body;
|
|
@@ -1216,11 +1303,13 @@ export function createRequestHandler(deps) {
|
|
|
1216
1303
|
body = JSON.parse(await readBody(req));
|
|
1217
1304
|
}
|
|
1218
1305
|
catch {
|
|
1219
|
-
json(res,
|
|
1306
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1220
1307
|
return;
|
|
1221
1308
|
}
|
|
1222
1309
|
if (!body.value) {
|
|
1223
|
-
json(res,
|
|
1310
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1311
|
+
error: "Missing required field: value",
|
|
1312
|
+
});
|
|
1224
1313
|
return;
|
|
1225
1314
|
}
|
|
1226
1315
|
store.updateSessionTitle(sessionId, body.value);
|
|
@@ -1235,7 +1324,7 @@ export function createRequestHandler(deps) {
|
|
|
1235
1324
|
title: body.value,
|
|
1236
1325
|
};
|
|
1237
1326
|
sseManager.broadcast(titleEvent);
|
|
1238
|
-
json(res,
|
|
1327
|
+
json(res, HTTP_STATUS.OK, { title: body.value });
|
|
1239
1328
|
return;
|
|
1240
1329
|
}
|
|
1241
1330
|
// --- Session CRUD: /api/v1/sessions/:id ---
|
|
@@ -1248,7 +1337,7 @@ export function createRequestHandler(deps) {
|
|
|
1248
1337
|
if (req.method === "GET") {
|
|
1249
1338
|
const session = store.getSession(sessionId);
|
|
1250
1339
|
if (!session) {
|
|
1251
|
-
json(res,
|
|
1340
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1252
1341
|
return;
|
|
1253
1342
|
}
|
|
1254
1343
|
// Start resume in background (non-blocking) so the client gets metadata fast.
|
|
@@ -1353,7 +1442,7 @@ export function createRequestHandler(deps) {
|
|
|
1353
1442
|
return opts;
|
|
1354
1443
|
})()
|
|
1355
1444
|
: [];
|
|
1356
|
-
json(res,
|
|
1445
|
+
json(res, HTTP_STATUS.OK, {
|
|
1357
1446
|
id: freshSession.id,
|
|
1358
1447
|
cwd: freshSession.cwd,
|
|
1359
1448
|
title: freshSession.title,
|
|
@@ -1368,7 +1457,7 @@ export function createRequestHandler(deps) {
|
|
|
1368
1457
|
if (req.method === "DELETE") {
|
|
1369
1458
|
const session = store.getSession(sessionId);
|
|
1370
1459
|
if (!session) {
|
|
1371
|
-
json(res,
|
|
1460
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1372
1461
|
return;
|
|
1373
1462
|
}
|
|
1374
1463
|
if (sessions) {
|
|
@@ -1378,7 +1467,7 @@ export function createRequestHandler(deps) {
|
|
|
1378
1467
|
store.deleteSession(sessionId);
|
|
1379
1468
|
}
|
|
1380
1469
|
sseManager.broadcast({ type: "session_deleted", sessionId });
|
|
1381
|
-
res.writeHead(
|
|
1470
|
+
res.writeHead(HTTP_STATUS.NO_CONTENT);
|
|
1382
1471
|
res.end();
|
|
1383
1472
|
return;
|
|
1384
1473
|
}
|
|
@@ -1387,11 +1476,15 @@ export function createRequestHandler(deps) {
|
|
|
1387
1476
|
if (url === "/api/v1/sessions" && req.method === "POST") {
|
|
1388
1477
|
const bridge = getBridge?.();
|
|
1389
1478
|
if (!bridge) {
|
|
1390
|
-
json(res,
|
|
1479
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
1480
|
+
error: "Agent not ready yet",
|
|
1481
|
+
});
|
|
1391
1482
|
return;
|
|
1392
1483
|
}
|
|
1393
1484
|
if (!sessions) {
|
|
1394
|
-
json(res,
|
|
1485
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
1486
|
+
error: "Session manager not available",
|
|
1487
|
+
});
|
|
1395
1488
|
return;
|
|
1396
1489
|
}
|
|
1397
1490
|
let body;
|
|
@@ -1399,7 +1492,7 @@ export function createRequestHandler(deps) {
|
|
|
1399
1492
|
body = JSON.parse(await readBody(req));
|
|
1400
1493
|
}
|
|
1401
1494
|
catch {
|
|
1402
|
-
json(res,
|
|
1495
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1403
1496
|
return;
|
|
1404
1497
|
}
|
|
1405
1498
|
const source = body.source ?? "auto";
|
|
@@ -1412,6 +1505,7 @@ export function createRequestHandler(deps) {
|
|
|
1412
1505
|
cwd: session?.cwd,
|
|
1413
1506
|
title: session?.title,
|
|
1414
1507
|
configOptions,
|
|
1508
|
+
agentCommands: sessions.getAgentCommands(sessionId),
|
|
1415
1509
|
};
|
|
1416
1510
|
sseManager.broadcast(sessionCreatedEvent);
|
|
1417
1511
|
// ACP's session_created event fires before inheritance runs, so
|
|
@@ -1423,21 +1517,22 @@ export function createRequestHandler(deps) {
|
|
|
1423
1517
|
configOptions,
|
|
1424
1518
|
});
|
|
1425
1519
|
}
|
|
1426
|
-
json(res,
|
|
1520
|
+
json(res, HTTP_STATUS.CREATED, {
|
|
1427
1521
|
id: sessionId,
|
|
1428
1522
|
cwd: session?.cwd ?? body.cwd,
|
|
1429
1523
|
title: session?.title ?? null,
|
|
1430
1524
|
source: session?.source ?? source,
|
|
1431
1525
|
configOptions,
|
|
1526
|
+
agentCommands: sessions.getAgentCommands(sessionId),
|
|
1432
1527
|
});
|
|
1433
1528
|
}
|
|
1434
1529
|
catch (err) {
|
|
1435
1530
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1436
1531
|
if (msg.includes("does not exist")) {
|
|
1437
|
-
json(res,
|
|
1532
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: msg });
|
|
1438
1533
|
}
|
|
1439
1534
|
else {
|
|
1440
|
-
json(res,
|
|
1535
|
+
json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, { error: msg });
|
|
1441
1536
|
}
|
|
1442
1537
|
}
|
|
1443
1538
|
return;
|
|
@@ -1459,7 +1554,7 @@ export function createRequestHandler(deps) {
|
|
|
1459
1554
|
: undefined;
|
|
1460
1555
|
const session = store.getSession(sessionId);
|
|
1461
1556
|
if (!session) {
|
|
1462
|
-
json(res,
|
|
1557
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1463
1558
|
return;
|
|
1464
1559
|
}
|
|
1465
1560
|
// Flush pending buffers so their content becomes part of the event list.
|
|
@@ -1520,7 +1615,7 @@ export function createRequestHandler(deps) {
|
|
|
1520
1615
|
envelope.total = total;
|
|
1521
1616
|
envelope.hasMore = hasMore;
|
|
1522
1617
|
}
|
|
1523
|
-
json(res,
|
|
1618
|
+
json(res, HTTP_STATUS.OK, envelope, req);
|
|
1524
1619
|
return;
|
|
1525
1620
|
}
|
|
1526
1621
|
// --- SSE stream endpoints ---
|
|
@@ -1534,13 +1629,15 @@ export function createRequestHandler(deps) {
|
|
|
1534
1629
|
const ticket = new URLSearchParams(url.split("?")[1] ?? "").get("ticket") ?? "";
|
|
1535
1630
|
const principal = deps.ticketStore?.consume(ticket);
|
|
1536
1631
|
if (!principal) {
|
|
1537
|
-
json(res,
|
|
1632
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, {
|
|
1633
|
+
error: "Invalid or expired ticket",
|
|
1634
|
+
});
|
|
1538
1635
|
return;
|
|
1539
1636
|
}
|
|
1540
1637
|
tokenName = principal.tokenName;
|
|
1541
1638
|
}
|
|
1542
1639
|
const clientId = sseManager.generateClientId();
|
|
1543
|
-
res.writeHead(
|
|
1640
|
+
res.writeHead(HTTP_STATUS.OK, {
|
|
1544
1641
|
"Content-Type": "text/event-stream",
|
|
1545
1642
|
"Cache-Control": "no-cache",
|
|
1546
1643
|
Connection: "keep-alive",
|
|
@@ -1569,18 +1666,20 @@ export function createRequestHandler(deps) {
|
|
|
1569
1666
|
const ticket = new URLSearchParams(url.split("?")[1] ?? "").get("ticket") ?? "";
|
|
1570
1667
|
const principal = deps.ticketStore?.consume(ticket);
|
|
1571
1668
|
if (!principal) {
|
|
1572
|
-
json(res,
|
|
1669
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, {
|
|
1670
|
+
error: "Invalid or expired ticket",
|
|
1671
|
+
});
|
|
1573
1672
|
return;
|
|
1574
1673
|
}
|
|
1575
1674
|
tokenName = principal.tokenName;
|
|
1576
1675
|
}
|
|
1577
1676
|
const session = store.getSession(sessionId);
|
|
1578
1677
|
if (!session) {
|
|
1579
|
-
json(res,
|
|
1678
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1580
1679
|
return;
|
|
1581
1680
|
}
|
|
1582
1681
|
const clientId = sseManager.generateClientId();
|
|
1583
|
-
res.writeHead(
|
|
1682
|
+
res.writeHead(HTTP_STATUS.OK, {
|
|
1584
1683
|
"Content-Type": "text/event-stream",
|
|
1585
1684
|
"Cache-Control": "no-cache",
|
|
1586
1685
|
Connection: "keep-alive",
|
|
@@ -1632,12 +1731,14 @@ export function createRequestHandler(deps) {
|
|
|
1632
1731
|
if (imgUploadMatch && req.method === "POST") {
|
|
1633
1732
|
const sessionId = decodeURIComponent(imgUploadMatch[1]);
|
|
1634
1733
|
if (!SAFE_ID.test(sessionId)) {
|
|
1635
|
-
json(res,
|
|
1734
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid session ID" });
|
|
1636
1735
|
return;
|
|
1637
1736
|
}
|
|
1638
1737
|
const ctype = req.headers["content-type"] ?? "";
|
|
1639
1738
|
if (!ctype.toLowerCase().startsWith("multipart/form-data")) {
|
|
1640
|
-
json(res,
|
|
1739
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1740
|
+
error: "Expected multipart/form-data",
|
|
1741
|
+
});
|
|
1641
1742
|
return;
|
|
1642
1743
|
}
|
|
1643
1744
|
await handleAttachmentUpload(req, res, sessionId, deps);
|
|
@@ -1660,14 +1761,16 @@ export function createRequestHandler(deps) {
|
|
|
1660
1761
|
const exp = params.get("exp") ?? "";
|
|
1661
1762
|
const basePath = `/api/v1/sessions/${sessionId}/attachments/${file}`;
|
|
1662
1763
|
if (!verifyAttachmentSig(basePath, exp, sig, deps.attachmentSecret)) {
|
|
1663
|
-
res.writeHead(
|
|
1764
|
+
res.writeHead(HTTP_STATUS.UNAUTHORIZED, {
|
|
1765
|
+
"Content-Type": "application/json",
|
|
1766
|
+
});
|
|
1664
1767
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1665
1768
|
return;
|
|
1666
1769
|
}
|
|
1667
1770
|
}
|
|
1668
1771
|
const filePath = join(deps.dataDir, "sessions", sessionId, "attachments", file);
|
|
1669
1772
|
if (!filePath.startsWith(join(deps.dataDir, "sessions"))) {
|
|
1670
|
-
res.writeHead(
|
|
1773
|
+
res.writeHead(HTTP_STATUS.FORBIDDEN);
|
|
1671
1774
|
res.end("Forbidden");
|
|
1672
1775
|
return;
|
|
1673
1776
|
}
|
|
@@ -1688,11 +1791,11 @@ export function createRequestHandler(deps) {
|
|
|
1688
1791
|
const disposition = isInlineMime(mime) ? "inline" : "attachment";
|
|
1689
1792
|
headers["Content-Disposition"] = buildContentDisposition(disposition, row.name);
|
|
1690
1793
|
}
|
|
1691
|
-
res.writeHead(
|
|
1794
|
+
res.writeHead(HTTP_STATUS.OK, headers);
|
|
1692
1795
|
res.end(fileData);
|
|
1693
1796
|
}
|
|
1694
1797
|
catch {
|
|
1695
|
-
res.writeHead(
|
|
1798
|
+
res.writeHead(HTTP_STATUS.NOT_FOUND);
|
|
1696
1799
|
res.end("Not found");
|
|
1697
1800
|
}
|
|
1698
1801
|
return;
|
|
@@ -1711,7 +1814,7 @@ export function createRequestHandler(deps) {
|
|
|
1711
1814
|
raw = await readBody(req);
|
|
1712
1815
|
}
|
|
1713
1816
|
catch {
|
|
1714
|
-
json(res,
|
|
1817
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Failed to read body" });
|
|
1715
1818
|
return;
|
|
1716
1819
|
}
|
|
1717
1820
|
let parsed;
|
|
@@ -1719,12 +1822,12 @@ export function createRequestHandler(deps) {
|
|
|
1719
1822
|
parsed = JSON.parse(raw);
|
|
1720
1823
|
}
|
|
1721
1824
|
catch {
|
|
1722
|
-
json(res,
|
|
1825
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1723
1826
|
return;
|
|
1724
1827
|
}
|
|
1725
1828
|
const validation = MessageIngressSchema.safeParse(parsed);
|
|
1726
1829
|
if (!validation.success) {
|
|
1727
|
-
json(res,
|
|
1830
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1728
1831
|
error: "Invalid body",
|
|
1729
1832
|
issues: validation.error.issues,
|
|
1730
1833
|
});
|
|
@@ -1736,7 +1839,7 @@ export function createRequestHandler(deps) {
|
|
|
1736
1839
|
const targetSid = input.to.slice("session:".length);
|
|
1737
1840
|
const session = store.getSession(targetSid);
|
|
1738
1841
|
if (!session) {
|
|
1739
|
-
json(res,
|
|
1842
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "session_not_found" });
|
|
1740
1843
|
return;
|
|
1741
1844
|
}
|
|
1742
1845
|
sessions?.flushBuffers(targetSid);
|
|
@@ -1772,8 +1875,8 @@ export function createRequestHandler(deps) {
|
|
|
1772
1875
|
sess_id: targetSid.slice(0, 8),
|
|
1773
1876
|
});
|
|
1774
1877
|
const boundBody = { id, delivered: "session" };
|
|
1775
|
-
saveClientOpResult(store, opId, "__ingress__",
|
|
1776
|
-
json(res,
|
|
1878
|
+
saveClientOpResult(store, opId, "__ingress__", HTTP_STATUS.OK, boundBody);
|
|
1879
|
+
json(res, HTTP_STATUS.OK, boundBody);
|
|
1777
1880
|
return;
|
|
1778
1881
|
}
|
|
1779
1882
|
// Unbound: to=user → rows in `messages` table
|
|
@@ -1819,13 +1922,13 @@ export function createRequestHandler(deps) {
|
|
|
1819
1922
|
from_ref: input.from_ref,
|
|
1820
1923
|
});
|
|
1821
1924
|
const unboundBody = { id, delivered: "pending" };
|
|
1822
|
-
saveClientOpResult(store, opId, "__ingress__",
|
|
1823
|
-
json(res,
|
|
1925
|
+
saveClientOpResult(store, opId, "__ingress__", HTTP_STATUS.OK, unboundBody);
|
|
1926
|
+
json(res, HTTP_STATUS.OK, unboundBody);
|
|
1824
1927
|
return;
|
|
1825
1928
|
}
|
|
1826
1929
|
// GET /api/v1/messages — list unprocessed
|
|
1827
1930
|
if (url === "/api/v1/messages" && req.method === "GET") {
|
|
1828
|
-
json(res,
|
|
1931
|
+
json(res, HTTP_STATUS.OK, { messages: store.listUnprocessed() });
|
|
1829
1932
|
return;
|
|
1830
1933
|
}
|
|
1831
1934
|
// /api/v1/messages/:id... — GET single, POST :id/consume, POST :id/ack, DELETE :id
|
|
@@ -1841,7 +1944,7 @@ export function createRequestHandler(deps) {
|
|
|
1841
1944
|
}
|
|
1842
1945
|
catch (err) {
|
|
1843
1946
|
if (/message not found/.test(errorMessage(err))) {
|
|
1844
|
-
json(res,
|
|
1947
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
|
|
1845
1948
|
return;
|
|
1846
1949
|
}
|
|
1847
1950
|
throw err;
|
|
@@ -1859,7 +1962,7 @@ export function createRequestHandler(deps) {
|
|
|
1859
1962
|
sess_id: out.sessionId.slice(0, 8),
|
|
1860
1963
|
already_consumed: out.alreadyConsumed,
|
|
1861
1964
|
});
|
|
1862
|
-
json(res,
|
|
1965
|
+
json(res, HTTP_STATUS.OK, {
|
|
1863
1966
|
sessionId: out.sessionId,
|
|
1864
1967
|
alreadyConsumed: out.alreadyConsumed,
|
|
1865
1968
|
});
|
|
@@ -1873,28 +1976,28 @@ export function createRequestHandler(deps) {
|
|
|
1873
1976
|
const id = decodeURIComponent((ackPost ?? idOnly)[1]);
|
|
1874
1977
|
const changes = store.deleteMessage(id);
|
|
1875
1978
|
if (changes === 0) {
|
|
1876
|
-
json(res,
|
|
1979
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
|
|
1877
1980
|
return;
|
|
1878
1981
|
}
|
|
1879
1982
|
sseManager.broadcast({ type: "message_acked", messageId: id });
|
|
1880
1983
|
if (deps.pushService)
|
|
1881
1984
|
void deps.pushService.sendClose(id);
|
|
1882
1985
|
mlog.info("ack", { msg_id: id });
|
|
1883
|
-
json(res,
|
|
1986
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
1884
1987
|
return;
|
|
1885
1988
|
}
|
|
1886
1989
|
if (idOnly && req.method === "GET") {
|
|
1887
1990
|
const id = decodeURIComponent(idOnly[1]);
|
|
1888
1991
|
const row = store.getMessage(id);
|
|
1889
1992
|
if (!row) {
|
|
1890
|
-
json(res,
|
|
1993
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
|
|
1891
1994
|
return;
|
|
1892
1995
|
}
|
|
1893
|
-
json(res,
|
|
1996
|
+
json(res, HTTP_STATUS.OK, row);
|
|
1894
1997
|
return;
|
|
1895
1998
|
}
|
|
1896
1999
|
}
|
|
1897
|
-
json(res,
|
|
2000
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Not found" });
|
|
1898
2001
|
return;
|
|
1899
2002
|
}
|
|
1900
2003
|
// --- Beta API routes ---
|
|
@@ -1903,12 +2006,16 @@ export function createRequestHandler(deps) {
|
|
|
1903
2006
|
// POST /api/beta/prompt — quick one-shot prompt (create temp session + send)
|
|
1904
2007
|
if (url === "/api/beta/prompt" && req.method === "POST") {
|
|
1905
2008
|
if (!sessions || !getBridge) {
|
|
1906
|
-
json(res,
|
|
2009
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
2010
|
+
error: "Agent not available",
|
|
2011
|
+
});
|
|
1907
2012
|
return;
|
|
1908
2013
|
}
|
|
1909
2014
|
const bridge = getBridge();
|
|
1910
2015
|
if (!bridge) {
|
|
1911
|
-
json(res,
|
|
2016
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
2017
|
+
error: "Agent not available",
|
|
2018
|
+
});
|
|
1912
2019
|
return;
|
|
1913
2020
|
}
|
|
1914
2021
|
let body;
|
|
@@ -1916,18 +2023,20 @@ export function createRequestHandler(deps) {
|
|
|
1916
2023
|
body = JSON.parse(await readBody(req));
|
|
1917
2024
|
}
|
|
1918
2025
|
catch {
|
|
1919
|
-
json(res,
|
|
2026
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1920
2027
|
return;
|
|
1921
2028
|
}
|
|
1922
2029
|
const text = body.text;
|
|
1923
2030
|
if (!text || typeof text !== "string") {
|
|
1924
|
-
json(res,
|
|
2031
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2032
|
+
error: "Missing required field: text",
|
|
2033
|
+
});
|
|
1925
2034
|
return;
|
|
1926
2035
|
}
|
|
1927
2036
|
const cwd = typeof body.cwd === "string" ? body.cwd : undefined;
|
|
1928
2037
|
const { sessionId } = await sessions.createSession(bridge, cwd, undefined, "auto");
|
|
1929
2038
|
const streamUrl = `/api/v1/sessions/${sessionId}/events/stream`;
|
|
1930
|
-
json(res,
|
|
2039
|
+
json(res, HTTP_STATUS.ACCEPTED, { sessionId, streamUrl });
|
|
1931
2040
|
// Fire-and-forget: send the prompt asynchronously, tracking busy state
|
|
1932
2041
|
sessions.activePrompts.add(sessionId);
|
|
1933
2042
|
sessions.syncBusy(sessionId);
|
|
@@ -1963,7 +2072,7 @@ export function createRequestHandler(deps) {
|
|
|
1963
2072
|
const clientKnown = sseManager.clients.has(clientId) ||
|
|
1964
2073
|
deps.clientRegistry?.get(clientId) !== undefined;
|
|
1965
2074
|
if (!clientKnown) {
|
|
1966
|
-
json(res,
|
|
2075
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Client not found" });
|
|
1967
2076
|
return;
|
|
1968
2077
|
}
|
|
1969
2078
|
let body;
|
|
@@ -1971,11 +2080,13 @@ export function createRequestHandler(deps) {
|
|
|
1971
2080
|
body = JSON.parse(await readBody(req));
|
|
1972
2081
|
}
|
|
1973
2082
|
catch {
|
|
1974
|
-
json(res,
|
|
2083
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1975
2084
|
return;
|
|
1976
2085
|
}
|
|
1977
2086
|
if (typeof body.visible !== "boolean") {
|
|
1978
|
-
json(res,
|
|
2087
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2088
|
+
error: "Missing or invalid 'visible' field",
|
|
2089
|
+
});
|
|
1979
2090
|
return;
|
|
1980
2091
|
}
|
|
1981
2092
|
// sessionId patch semantics: absent = preserve, null = clear,
|
|
@@ -2022,23 +2133,25 @@ export function createRequestHandler(deps) {
|
|
|
2022
2133
|
}
|
|
2023
2134
|
}
|
|
2024
2135
|
}
|
|
2025
|
-
json(res,
|
|
2136
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
2026
2137
|
return;
|
|
2027
2138
|
}
|
|
2028
2139
|
// --- Push notification routes ---
|
|
2029
2140
|
// GET /api/beta/push/vapid-key
|
|
2030
2141
|
if (url === "/api/beta/push/vapid-key" && req.method === "GET") {
|
|
2031
2142
|
if (!deps.pushService) {
|
|
2032
|
-
json(res,
|
|
2143
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2033
2144
|
return;
|
|
2034
2145
|
}
|
|
2035
|
-
json(res,
|
|
2146
|
+
json(res, HTTP_STATUS.OK, {
|
|
2147
|
+
publicKey: deps.pushService.getPublicKey(),
|
|
2148
|
+
});
|
|
2036
2149
|
return;
|
|
2037
2150
|
}
|
|
2038
2151
|
// POST /api/beta/push/subscribe
|
|
2039
2152
|
if (url === "/api/beta/push/subscribe" && req.method === "POST") {
|
|
2040
2153
|
if (!deps.pushService) {
|
|
2041
|
-
json(res,
|
|
2154
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2042
2155
|
return;
|
|
2043
2156
|
}
|
|
2044
2157
|
const chunks = [];
|
|
@@ -2049,11 +2162,13 @@ export function createRequestHandler(deps) {
|
|
|
2049
2162
|
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2050
2163
|
}
|
|
2051
2164
|
catch {
|
|
2052
|
-
json(res,
|
|
2165
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
2053
2166
|
return;
|
|
2054
2167
|
}
|
|
2055
2168
|
if (!body.endpoint || !body.keys?.auth || !body.keys.p256dh) {
|
|
2056
|
-
json(res,
|
|
2169
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2170
|
+
error: "Missing endpoint or keys (auth, p256dh)",
|
|
2171
|
+
});
|
|
2057
2172
|
return;
|
|
2058
2173
|
}
|
|
2059
2174
|
store.saveSubscription(body.endpoint, body.keys.auth, body.keys.p256dh);
|
|
@@ -2061,13 +2176,13 @@ export function createRequestHandler(deps) {
|
|
|
2061
2176
|
if (body.clientId && deps.pushService) {
|
|
2062
2177
|
deps.pushService.registerClient(body.clientId, body.endpoint);
|
|
2063
2178
|
}
|
|
2064
|
-
json(res,
|
|
2179
|
+
json(res, HTTP_STATUS.CREATED, { ok: true });
|
|
2065
2180
|
return;
|
|
2066
2181
|
}
|
|
2067
2182
|
// POST /api/beta/push/register-client — associate clientId with push endpoint
|
|
2068
2183
|
if (url === "/api/beta/push/register-client" && req.method === "POST") {
|
|
2069
2184
|
if (!deps.pushService) {
|
|
2070
|
-
json(res,
|
|
2185
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2071
2186
|
return;
|
|
2072
2187
|
}
|
|
2073
2188
|
const chunks = [];
|
|
@@ -2078,21 +2193,23 @@ export function createRequestHandler(deps) {
|
|
|
2078
2193
|
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2079
2194
|
}
|
|
2080
2195
|
catch {
|
|
2081
|
-
json(res,
|
|
2196
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
2082
2197
|
return;
|
|
2083
2198
|
}
|
|
2084
2199
|
if (!body.clientId || !body.endpoint) {
|
|
2085
|
-
json(res,
|
|
2200
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2201
|
+
error: "Missing clientId or endpoint",
|
|
2202
|
+
});
|
|
2086
2203
|
return;
|
|
2087
2204
|
}
|
|
2088
2205
|
deps.pushService.registerClient(body.clientId, body.endpoint);
|
|
2089
|
-
json(res,
|
|
2206
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
2090
2207
|
return;
|
|
2091
2208
|
}
|
|
2092
2209
|
// POST /api/beta/push/unsubscribe
|
|
2093
2210
|
if (url === "/api/beta/push/unsubscribe" && req.method === "POST") {
|
|
2094
2211
|
if (!deps.pushService) {
|
|
2095
|
-
json(res,
|
|
2212
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2096
2213
|
return;
|
|
2097
2214
|
}
|
|
2098
2215
|
const chunks = [];
|
|
@@ -2103,16 +2220,16 @@ export function createRequestHandler(deps) {
|
|
|
2103
2220
|
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2104
2221
|
}
|
|
2105
2222
|
catch {
|
|
2106
|
-
json(res,
|
|
2223
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
2107
2224
|
return;
|
|
2108
2225
|
}
|
|
2109
2226
|
if (body.endpoint) {
|
|
2110
2227
|
store.removeSubscription(body.endpoint);
|
|
2111
2228
|
}
|
|
2112
|
-
json(res,
|
|
2229
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
2113
2230
|
return;
|
|
2114
2231
|
}
|
|
2115
|
-
json(res,
|
|
2232
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Not found" });
|
|
2116
2233
|
return;
|
|
2117
2234
|
}
|
|
2118
2235
|
// --- Static files ---
|
|
@@ -2122,7 +2239,7 @@ export function createRequestHandler(deps) {
|
|
|
2122
2239
|
staticPath = "/" + htmlEntry.file;
|
|
2123
2240
|
const filePath = join(deps.publicDir, staticPath);
|
|
2124
2241
|
if (!filePath.startsWith(deps.publicDir)) {
|
|
2125
|
-
res.writeHead(
|
|
2242
|
+
res.writeHead(HTTP_STATUS.FORBIDDEN);
|
|
2126
2243
|
res.end("Forbidden");
|
|
2127
2244
|
return;
|
|
2128
2245
|
}
|
|
@@ -2145,11 +2262,11 @@ export function createRequestHandler(deps) {
|
|
|
2145
2262
|
// CSP applies to HTML entrypoints (where script/style execute).
|
|
2146
2263
|
if (htmlEntry)
|
|
2147
2264
|
headers["Content-Security-Policy"] = CSP_POLICY;
|
|
2148
|
-
res.writeHead(
|
|
2265
|
+
res.writeHead(HTTP_STATUS.OK, headers);
|
|
2149
2266
|
res.end(data);
|
|
2150
2267
|
}
|
|
2151
2268
|
catch {
|
|
2152
|
-
res.writeHead(
|
|
2269
|
+
res.writeHead(HTTP_STATUS.NOT_FOUND);
|
|
2153
2270
|
res.end("Not found");
|
|
2154
2271
|
}
|
|
2155
2272
|
};
|