@lelouchhe/webagent 0.5.1 → 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 +4 -4
- package/dist/js/app.KFQAFHA3.js +4 -0
- package/dist/js/{chunk.IJM5DBCO.js → chunk.3CCHZCG7.js} +10 -10
- 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 +2 -2
- package/dist/share-viewer.html +4 -4
- package/dist/{styles.00xfh3e6.css → styles.01ky3cex.css} +8 -0
- package/lib/agent-commands.js +27 -0
- package/lib/bridge.js +20 -7
- package/lib/event-handler.js +18 -1
- package/lib/http-status.js +17 -0
- package/lib/model-picker.js +1 -1
- package/lib/push-service.js +2 -1
- package/lib/routes.js +294 -168
- package/lib/session-manager.js +44 -2
- package/lib/share/routes.js +106 -72
- package/lib/share/sanitize.js +2 -1
- package/package.json +2 -2
- package/dist/js/app.3OEVQXHK.js +0 -4
- package/dist/js/chunk.D4ZYHJAM.js +0 -1
- package/dist/js/viewer.FCSSTUVY.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,27 +1220,32 @@ 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} ---
|
|
1148
|
-
|
|
1231
|
+
// --- PUT /api/v1/sessions/:id/config/:configId ---
|
|
1232
|
+
const legacyConfigPutMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/(model|mode|reasoning-effort)\/?$/);
|
|
1233
|
+
const genericConfigPutMatch = url.match(/^\/api\/v1\/sessions\/([^/]+)\/config\/([^/]+)\/?$/);
|
|
1234
|
+
const configPutMatch = legacyConfigPutMatch ?? genericConfigPutMatch;
|
|
1149
1235
|
if (configPutMatch && req.method === "PUT") {
|
|
1150
1236
|
const sessionId = decodeURIComponent(configPutMatch[1]);
|
|
1151
|
-
const configPath = configPutMatch[2];
|
|
1152
|
-
const configId = configPath
|
|
1237
|
+
const configPath = decodeURIComponent(configPutMatch[2]);
|
|
1238
|
+
const configId = configPath.replace(/-/g, "_");
|
|
1153
1239
|
const session = store.getSession(sessionId);
|
|
1154
1240
|
if (!session) {
|
|
1155
|
-
json(res,
|
|
1241
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1156
1242
|
return;
|
|
1157
1243
|
}
|
|
1158
1244
|
const bridge = getBridge?.();
|
|
1159
1245
|
if (!bridge) {
|
|
1160
|
-
json(res,
|
|
1246
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
1247
|
+
error: "Agent not ready yet",
|
|
1248
|
+
});
|
|
1161
1249
|
return;
|
|
1162
1250
|
}
|
|
1163
1251
|
let body;
|
|
@@ -1165,17 +1253,21 @@ export function createRequestHandler(deps) {
|
|
|
1165
1253
|
body = JSON.parse(await readBody(req));
|
|
1166
1254
|
}
|
|
1167
1255
|
catch {
|
|
1168
|
-
json(res,
|
|
1256
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1169
1257
|
return;
|
|
1170
1258
|
}
|
|
1171
|
-
if (
|
|
1172
|
-
json(res,
|
|
1259
|
+
if (body.value === undefined) {
|
|
1260
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1261
|
+
error: "Missing required field: value",
|
|
1262
|
+
});
|
|
1173
1263
|
return;
|
|
1174
1264
|
}
|
|
1175
1265
|
try {
|
|
1176
1266
|
const configOptions = await bridge.setConfigOption(sessionId, configId, body.value);
|
|
1177
1267
|
for (const opt of configOptions) {
|
|
1178
|
-
|
|
1268
|
+
if (typeof opt.currentValue === "string") {
|
|
1269
|
+
store.updateSessionConfig(sessionId, opt.id, opt.currentValue);
|
|
1270
|
+
}
|
|
1179
1271
|
}
|
|
1180
1272
|
sseManager.broadcast({
|
|
1181
1273
|
type: "config_option_update",
|
|
@@ -1188,10 +1280,10 @@ export function createRequestHandler(deps) {
|
|
|
1188
1280
|
configId,
|
|
1189
1281
|
value: body.value,
|
|
1190
1282
|
});
|
|
1191
|
-
json(res,
|
|
1283
|
+
json(res, HTTP_STATUS.OK, { configOptions });
|
|
1192
1284
|
}
|
|
1193
1285
|
catch (err) {
|
|
1194
|
-
json(res,
|
|
1286
|
+
json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, {
|
|
1195
1287
|
error: `Failed to set ${configId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1196
1288
|
});
|
|
1197
1289
|
}
|
|
@@ -1203,7 +1295,7 @@ export function createRequestHandler(deps) {
|
|
|
1203
1295
|
const sessionId = decodeURIComponent(titlePutMatch[1]);
|
|
1204
1296
|
const session = store.getSession(sessionId);
|
|
1205
1297
|
if (!session) {
|
|
1206
|
-
json(res,
|
|
1298
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1207
1299
|
return;
|
|
1208
1300
|
}
|
|
1209
1301
|
let body;
|
|
@@ -1211,11 +1303,13 @@ export function createRequestHandler(deps) {
|
|
|
1211
1303
|
body = JSON.parse(await readBody(req));
|
|
1212
1304
|
}
|
|
1213
1305
|
catch {
|
|
1214
|
-
json(res,
|
|
1306
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1215
1307
|
return;
|
|
1216
1308
|
}
|
|
1217
1309
|
if (!body.value) {
|
|
1218
|
-
json(res,
|
|
1310
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1311
|
+
error: "Missing required field: value",
|
|
1312
|
+
});
|
|
1219
1313
|
return;
|
|
1220
1314
|
}
|
|
1221
1315
|
store.updateSessionTitle(sessionId, body.value);
|
|
@@ -1230,7 +1324,7 @@ export function createRequestHandler(deps) {
|
|
|
1230
1324
|
title: body.value,
|
|
1231
1325
|
};
|
|
1232
1326
|
sseManager.broadcast(titleEvent);
|
|
1233
|
-
json(res,
|
|
1327
|
+
json(res, HTTP_STATUS.OK, { title: body.value });
|
|
1234
1328
|
return;
|
|
1235
1329
|
}
|
|
1236
1330
|
// --- Session CRUD: /api/v1/sessions/:id ---
|
|
@@ -1243,7 +1337,7 @@ export function createRequestHandler(deps) {
|
|
|
1243
1337
|
if (req.method === "GET") {
|
|
1244
1338
|
const session = store.getSession(sessionId);
|
|
1245
1339
|
if (!session) {
|
|
1246
|
-
json(res,
|
|
1340
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1247
1341
|
return;
|
|
1248
1342
|
}
|
|
1249
1343
|
// Start resume in background (non-blocking) so the client gets metadata fast.
|
|
@@ -1272,7 +1366,9 @@ export function createRequestHandler(deps) {
|
|
|
1272
1366
|
reasoning_effort: cur.reasoning_effort,
|
|
1273
1367
|
};
|
|
1274
1368
|
const override = stored[opt.id];
|
|
1275
|
-
return override
|
|
1369
|
+
return override && "options" in opt
|
|
1370
|
+
? { ...opt, currentValue: override }
|
|
1371
|
+
: opt;
|
|
1276
1372
|
});
|
|
1277
1373
|
sseManager.broadcast({
|
|
1278
1374
|
type: "config_option_update",
|
|
@@ -1339,12 +1435,14 @@ export function createRequestHandler(deps) {
|
|
|
1339
1435
|
reasoning_effort: freshSession.reasoning_effort,
|
|
1340
1436
|
};
|
|
1341
1437
|
const override = stored[opt.id];
|
|
1342
|
-
return override
|
|
1438
|
+
return override && "options" in opt
|
|
1439
|
+
? { ...opt, currentValue: override }
|
|
1440
|
+
: opt;
|
|
1343
1441
|
});
|
|
1344
1442
|
return opts;
|
|
1345
1443
|
})()
|
|
1346
1444
|
: [];
|
|
1347
|
-
json(res,
|
|
1445
|
+
json(res, HTTP_STATUS.OK, {
|
|
1348
1446
|
id: freshSession.id,
|
|
1349
1447
|
cwd: freshSession.cwd,
|
|
1350
1448
|
title: freshSession.title,
|
|
@@ -1359,7 +1457,7 @@ export function createRequestHandler(deps) {
|
|
|
1359
1457
|
if (req.method === "DELETE") {
|
|
1360
1458
|
const session = store.getSession(sessionId);
|
|
1361
1459
|
if (!session) {
|
|
1362
|
-
json(res,
|
|
1460
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1363
1461
|
return;
|
|
1364
1462
|
}
|
|
1365
1463
|
if (sessions) {
|
|
@@ -1369,7 +1467,7 @@ export function createRequestHandler(deps) {
|
|
|
1369
1467
|
store.deleteSession(sessionId);
|
|
1370
1468
|
}
|
|
1371
1469
|
sseManager.broadcast({ type: "session_deleted", sessionId });
|
|
1372
|
-
res.writeHead(
|
|
1470
|
+
res.writeHead(HTTP_STATUS.NO_CONTENT);
|
|
1373
1471
|
res.end();
|
|
1374
1472
|
return;
|
|
1375
1473
|
}
|
|
@@ -1378,11 +1476,15 @@ export function createRequestHandler(deps) {
|
|
|
1378
1476
|
if (url === "/api/v1/sessions" && req.method === "POST") {
|
|
1379
1477
|
const bridge = getBridge?.();
|
|
1380
1478
|
if (!bridge) {
|
|
1381
|
-
json(res,
|
|
1479
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
1480
|
+
error: "Agent not ready yet",
|
|
1481
|
+
});
|
|
1382
1482
|
return;
|
|
1383
1483
|
}
|
|
1384
1484
|
if (!sessions) {
|
|
1385
|
-
json(res,
|
|
1485
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
1486
|
+
error: "Session manager not available",
|
|
1487
|
+
});
|
|
1386
1488
|
return;
|
|
1387
1489
|
}
|
|
1388
1490
|
let body;
|
|
@@ -1390,7 +1492,7 @@ export function createRequestHandler(deps) {
|
|
|
1390
1492
|
body = JSON.parse(await readBody(req));
|
|
1391
1493
|
}
|
|
1392
1494
|
catch {
|
|
1393
|
-
json(res,
|
|
1495
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1394
1496
|
return;
|
|
1395
1497
|
}
|
|
1396
1498
|
const source = body.source ?? "auto";
|
|
@@ -1403,6 +1505,7 @@ export function createRequestHandler(deps) {
|
|
|
1403
1505
|
cwd: session?.cwd,
|
|
1404
1506
|
title: session?.title,
|
|
1405
1507
|
configOptions,
|
|
1508
|
+
agentCommands: sessions.getAgentCommands(sessionId),
|
|
1406
1509
|
};
|
|
1407
1510
|
sseManager.broadcast(sessionCreatedEvent);
|
|
1408
1511
|
// ACP's session_created event fires before inheritance runs, so
|
|
@@ -1414,21 +1517,22 @@ export function createRequestHandler(deps) {
|
|
|
1414
1517
|
configOptions,
|
|
1415
1518
|
});
|
|
1416
1519
|
}
|
|
1417
|
-
json(res,
|
|
1520
|
+
json(res, HTTP_STATUS.CREATED, {
|
|
1418
1521
|
id: sessionId,
|
|
1419
1522
|
cwd: session?.cwd ?? body.cwd,
|
|
1420
1523
|
title: session?.title ?? null,
|
|
1421
1524
|
source: session?.source ?? source,
|
|
1422
1525
|
configOptions,
|
|
1526
|
+
agentCommands: sessions.getAgentCommands(sessionId),
|
|
1423
1527
|
});
|
|
1424
1528
|
}
|
|
1425
1529
|
catch (err) {
|
|
1426
1530
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1427
1531
|
if (msg.includes("does not exist")) {
|
|
1428
|
-
json(res,
|
|
1532
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: msg });
|
|
1429
1533
|
}
|
|
1430
1534
|
else {
|
|
1431
|
-
json(res,
|
|
1535
|
+
json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, { error: msg });
|
|
1432
1536
|
}
|
|
1433
1537
|
}
|
|
1434
1538
|
return;
|
|
@@ -1450,7 +1554,7 @@ export function createRequestHandler(deps) {
|
|
|
1450
1554
|
: undefined;
|
|
1451
1555
|
const session = store.getSession(sessionId);
|
|
1452
1556
|
if (!session) {
|
|
1453
|
-
json(res,
|
|
1557
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1454
1558
|
return;
|
|
1455
1559
|
}
|
|
1456
1560
|
// Flush pending buffers so their content becomes part of the event list.
|
|
@@ -1511,7 +1615,7 @@ export function createRequestHandler(deps) {
|
|
|
1511
1615
|
envelope.total = total;
|
|
1512
1616
|
envelope.hasMore = hasMore;
|
|
1513
1617
|
}
|
|
1514
|
-
json(res,
|
|
1618
|
+
json(res, HTTP_STATUS.OK, envelope, req);
|
|
1515
1619
|
return;
|
|
1516
1620
|
}
|
|
1517
1621
|
// --- SSE stream endpoints ---
|
|
@@ -1525,13 +1629,15 @@ export function createRequestHandler(deps) {
|
|
|
1525
1629
|
const ticket = new URLSearchParams(url.split("?")[1] ?? "").get("ticket") ?? "";
|
|
1526
1630
|
const principal = deps.ticketStore?.consume(ticket);
|
|
1527
1631
|
if (!principal) {
|
|
1528
|
-
json(res,
|
|
1632
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, {
|
|
1633
|
+
error: "Invalid or expired ticket",
|
|
1634
|
+
});
|
|
1529
1635
|
return;
|
|
1530
1636
|
}
|
|
1531
1637
|
tokenName = principal.tokenName;
|
|
1532
1638
|
}
|
|
1533
1639
|
const clientId = sseManager.generateClientId();
|
|
1534
|
-
res.writeHead(
|
|
1640
|
+
res.writeHead(HTTP_STATUS.OK, {
|
|
1535
1641
|
"Content-Type": "text/event-stream",
|
|
1536
1642
|
"Cache-Control": "no-cache",
|
|
1537
1643
|
Connection: "keep-alive",
|
|
@@ -1560,18 +1666,20 @@ export function createRequestHandler(deps) {
|
|
|
1560
1666
|
const ticket = new URLSearchParams(url.split("?")[1] ?? "").get("ticket") ?? "";
|
|
1561
1667
|
const principal = deps.ticketStore?.consume(ticket);
|
|
1562
1668
|
if (!principal) {
|
|
1563
|
-
json(res,
|
|
1669
|
+
json(res, HTTP_STATUS.UNAUTHORIZED, {
|
|
1670
|
+
error: "Invalid or expired ticket",
|
|
1671
|
+
});
|
|
1564
1672
|
return;
|
|
1565
1673
|
}
|
|
1566
1674
|
tokenName = principal.tokenName;
|
|
1567
1675
|
}
|
|
1568
1676
|
const session = store.getSession(sessionId);
|
|
1569
1677
|
if (!session) {
|
|
1570
|
-
json(res,
|
|
1678
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
|
|
1571
1679
|
return;
|
|
1572
1680
|
}
|
|
1573
1681
|
const clientId = sseManager.generateClientId();
|
|
1574
|
-
res.writeHead(
|
|
1682
|
+
res.writeHead(HTTP_STATUS.OK, {
|
|
1575
1683
|
"Content-Type": "text/event-stream",
|
|
1576
1684
|
"Cache-Control": "no-cache",
|
|
1577
1685
|
Connection: "keep-alive",
|
|
@@ -1623,12 +1731,14 @@ export function createRequestHandler(deps) {
|
|
|
1623
1731
|
if (imgUploadMatch && req.method === "POST") {
|
|
1624
1732
|
const sessionId = decodeURIComponent(imgUploadMatch[1]);
|
|
1625
1733
|
if (!SAFE_ID.test(sessionId)) {
|
|
1626
|
-
json(res,
|
|
1734
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid session ID" });
|
|
1627
1735
|
return;
|
|
1628
1736
|
}
|
|
1629
1737
|
const ctype = req.headers["content-type"] ?? "";
|
|
1630
1738
|
if (!ctype.toLowerCase().startsWith("multipart/form-data")) {
|
|
1631
|
-
json(res,
|
|
1739
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1740
|
+
error: "Expected multipart/form-data",
|
|
1741
|
+
});
|
|
1632
1742
|
return;
|
|
1633
1743
|
}
|
|
1634
1744
|
await handleAttachmentUpload(req, res, sessionId, deps);
|
|
@@ -1651,14 +1761,16 @@ export function createRequestHandler(deps) {
|
|
|
1651
1761
|
const exp = params.get("exp") ?? "";
|
|
1652
1762
|
const basePath = `/api/v1/sessions/${sessionId}/attachments/${file}`;
|
|
1653
1763
|
if (!verifyAttachmentSig(basePath, exp, sig, deps.attachmentSecret)) {
|
|
1654
|
-
res.writeHead(
|
|
1764
|
+
res.writeHead(HTTP_STATUS.UNAUTHORIZED, {
|
|
1765
|
+
"Content-Type": "application/json",
|
|
1766
|
+
});
|
|
1655
1767
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1656
1768
|
return;
|
|
1657
1769
|
}
|
|
1658
1770
|
}
|
|
1659
1771
|
const filePath = join(deps.dataDir, "sessions", sessionId, "attachments", file);
|
|
1660
1772
|
if (!filePath.startsWith(join(deps.dataDir, "sessions"))) {
|
|
1661
|
-
res.writeHead(
|
|
1773
|
+
res.writeHead(HTTP_STATUS.FORBIDDEN);
|
|
1662
1774
|
res.end("Forbidden");
|
|
1663
1775
|
return;
|
|
1664
1776
|
}
|
|
@@ -1679,11 +1791,11 @@ export function createRequestHandler(deps) {
|
|
|
1679
1791
|
const disposition = isInlineMime(mime) ? "inline" : "attachment";
|
|
1680
1792
|
headers["Content-Disposition"] = buildContentDisposition(disposition, row.name);
|
|
1681
1793
|
}
|
|
1682
|
-
res.writeHead(
|
|
1794
|
+
res.writeHead(HTTP_STATUS.OK, headers);
|
|
1683
1795
|
res.end(fileData);
|
|
1684
1796
|
}
|
|
1685
1797
|
catch {
|
|
1686
|
-
res.writeHead(
|
|
1798
|
+
res.writeHead(HTTP_STATUS.NOT_FOUND);
|
|
1687
1799
|
res.end("Not found");
|
|
1688
1800
|
}
|
|
1689
1801
|
return;
|
|
@@ -1702,7 +1814,7 @@ export function createRequestHandler(deps) {
|
|
|
1702
1814
|
raw = await readBody(req);
|
|
1703
1815
|
}
|
|
1704
1816
|
catch {
|
|
1705
|
-
json(res,
|
|
1817
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Failed to read body" });
|
|
1706
1818
|
return;
|
|
1707
1819
|
}
|
|
1708
1820
|
let parsed;
|
|
@@ -1710,12 +1822,12 @@ export function createRequestHandler(deps) {
|
|
|
1710
1822
|
parsed = JSON.parse(raw);
|
|
1711
1823
|
}
|
|
1712
1824
|
catch {
|
|
1713
|
-
json(res,
|
|
1825
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1714
1826
|
return;
|
|
1715
1827
|
}
|
|
1716
1828
|
const validation = MessageIngressSchema.safeParse(parsed);
|
|
1717
1829
|
if (!validation.success) {
|
|
1718
|
-
json(res,
|
|
1830
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
1719
1831
|
error: "Invalid body",
|
|
1720
1832
|
issues: validation.error.issues,
|
|
1721
1833
|
});
|
|
@@ -1727,7 +1839,7 @@ export function createRequestHandler(deps) {
|
|
|
1727
1839
|
const targetSid = input.to.slice("session:".length);
|
|
1728
1840
|
const session = store.getSession(targetSid);
|
|
1729
1841
|
if (!session) {
|
|
1730
|
-
json(res,
|
|
1842
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "session_not_found" });
|
|
1731
1843
|
return;
|
|
1732
1844
|
}
|
|
1733
1845
|
sessions?.flushBuffers(targetSid);
|
|
@@ -1763,8 +1875,8 @@ export function createRequestHandler(deps) {
|
|
|
1763
1875
|
sess_id: targetSid.slice(0, 8),
|
|
1764
1876
|
});
|
|
1765
1877
|
const boundBody = { id, delivered: "session" };
|
|
1766
|
-
saveClientOpResult(store, opId, "__ingress__",
|
|
1767
|
-
json(res,
|
|
1878
|
+
saveClientOpResult(store, opId, "__ingress__", HTTP_STATUS.OK, boundBody);
|
|
1879
|
+
json(res, HTTP_STATUS.OK, boundBody);
|
|
1768
1880
|
return;
|
|
1769
1881
|
}
|
|
1770
1882
|
// Unbound: to=user → rows in `messages` table
|
|
@@ -1810,13 +1922,13 @@ export function createRequestHandler(deps) {
|
|
|
1810
1922
|
from_ref: input.from_ref,
|
|
1811
1923
|
});
|
|
1812
1924
|
const unboundBody = { id, delivered: "pending" };
|
|
1813
|
-
saveClientOpResult(store, opId, "__ingress__",
|
|
1814
|
-
json(res,
|
|
1925
|
+
saveClientOpResult(store, opId, "__ingress__", HTTP_STATUS.OK, unboundBody);
|
|
1926
|
+
json(res, HTTP_STATUS.OK, unboundBody);
|
|
1815
1927
|
return;
|
|
1816
1928
|
}
|
|
1817
1929
|
// GET /api/v1/messages — list unprocessed
|
|
1818
1930
|
if (url === "/api/v1/messages" && req.method === "GET") {
|
|
1819
|
-
json(res,
|
|
1931
|
+
json(res, HTTP_STATUS.OK, { messages: store.listUnprocessed() });
|
|
1820
1932
|
return;
|
|
1821
1933
|
}
|
|
1822
1934
|
// /api/v1/messages/:id... — GET single, POST :id/consume, POST :id/ack, DELETE :id
|
|
@@ -1832,7 +1944,7 @@ export function createRequestHandler(deps) {
|
|
|
1832
1944
|
}
|
|
1833
1945
|
catch (err) {
|
|
1834
1946
|
if (/message not found/.test(errorMessage(err))) {
|
|
1835
|
-
json(res,
|
|
1947
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
|
|
1836
1948
|
return;
|
|
1837
1949
|
}
|
|
1838
1950
|
throw err;
|
|
@@ -1850,7 +1962,7 @@ export function createRequestHandler(deps) {
|
|
|
1850
1962
|
sess_id: out.sessionId.slice(0, 8),
|
|
1851
1963
|
already_consumed: out.alreadyConsumed,
|
|
1852
1964
|
});
|
|
1853
|
-
json(res,
|
|
1965
|
+
json(res, HTTP_STATUS.OK, {
|
|
1854
1966
|
sessionId: out.sessionId,
|
|
1855
1967
|
alreadyConsumed: out.alreadyConsumed,
|
|
1856
1968
|
});
|
|
@@ -1864,28 +1976,28 @@ export function createRequestHandler(deps) {
|
|
|
1864
1976
|
const id = decodeURIComponent((ackPost ?? idOnly)[1]);
|
|
1865
1977
|
const changes = store.deleteMessage(id);
|
|
1866
1978
|
if (changes === 0) {
|
|
1867
|
-
json(res,
|
|
1979
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
|
|
1868
1980
|
return;
|
|
1869
1981
|
}
|
|
1870
1982
|
sseManager.broadcast({ type: "message_acked", messageId: id });
|
|
1871
1983
|
if (deps.pushService)
|
|
1872
1984
|
void deps.pushService.sendClose(id);
|
|
1873
1985
|
mlog.info("ack", { msg_id: id });
|
|
1874
|
-
json(res,
|
|
1986
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
1875
1987
|
return;
|
|
1876
1988
|
}
|
|
1877
1989
|
if (idOnly && req.method === "GET") {
|
|
1878
1990
|
const id = decodeURIComponent(idOnly[1]);
|
|
1879
1991
|
const row = store.getMessage(id);
|
|
1880
1992
|
if (!row) {
|
|
1881
|
-
json(res,
|
|
1993
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
|
|
1882
1994
|
return;
|
|
1883
1995
|
}
|
|
1884
|
-
json(res,
|
|
1996
|
+
json(res, HTTP_STATUS.OK, row);
|
|
1885
1997
|
return;
|
|
1886
1998
|
}
|
|
1887
1999
|
}
|
|
1888
|
-
json(res,
|
|
2000
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Not found" });
|
|
1889
2001
|
return;
|
|
1890
2002
|
}
|
|
1891
2003
|
// --- Beta API routes ---
|
|
@@ -1894,12 +2006,16 @@ export function createRequestHandler(deps) {
|
|
|
1894
2006
|
// POST /api/beta/prompt — quick one-shot prompt (create temp session + send)
|
|
1895
2007
|
if (url === "/api/beta/prompt" && req.method === "POST") {
|
|
1896
2008
|
if (!sessions || !getBridge) {
|
|
1897
|
-
json(res,
|
|
2009
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
2010
|
+
error: "Agent not available",
|
|
2011
|
+
});
|
|
1898
2012
|
return;
|
|
1899
2013
|
}
|
|
1900
2014
|
const bridge = getBridge();
|
|
1901
2015
|
if (!bridge) {
|
|
1902
|
-
json(res,
|
|
2016
|
+
json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
|
|
2017
|
+
error: "Agent not available",
|
|
2018
|
+
});
|
|
1903
2019
|
return;
|
|
1904
2020
|
}
|
|
1905
2021
|
let body;
|
|
@@ -1907,18 +2023,20 @@ export function createRequestHandler(deps) {
|
|
|
1907
2023
|
body = JSON.parse(await readBody(req));
|
|
1908
2024
|
}
|
|
1909
2025
|
catch {
|
|
1910
|
-
json(res,
|
|
2026
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1911
2027
|
return;
|
|
1912
2028
|
}
|
|
1913
2029
|
const text = body.text;
|
|
1914
2030
|
if (!text || typeof text !== "string") {
|
|
1915
|
-
json(res,
|
|
2031
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2032
|
+
error: "Missing required field: text",
|
|
2033
|
+
});
|
|
1916
2034
|
return;
|
|
1917
2035
|
}
|
|
1918
2036
|
const cwd = typeof body.cwd === "string" ? body.cwd : undefined;
|
|
1919
2037
|
const { sessionId } = await sessions.createSession(bridge, cwd, undefined, "auto");
|
|
1920
2038
|
const streamUrl = `/api/v1/sessions/${sessionId}/events/stream`;
|
|
1921
|
-
json(res,
|
|
2039
|
+
json(res, HTTP_STATUS.ACCEPTED, { sessionId, streamUrl });
|
|
1922
2040
|
// Fire-and-forget: send the prompt asynchronously, tracking busy state
|
|
1923
2041
|
sessions.activePrompts.add(sessionId);
|
|
1924
2042
|
sessions.syncBusy(sessionId);
|
|
@@ -1954,7 +2072,7 @@ export function createRequestHandler(deps) {
|
|
|
1954
2072
|
const clientKnown = sseManager.clients.has(clientId) ||
|
|
1955
2073
|
deps.clientRegistry?.get(clientId) !== undefined;
|
|
1956
2074
|
if (!clientKnown) {
|
|
1957
|
-
json(res,
|
|
2075
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Client not found" });
|
|
1958
2076
|
return;
|
|
1959
2077
|
}
|
|
1960
2078
|
let body;
|
|
@@ -1962,11 +2080,13 @@ export function createRequestHandler(deps) {
|
|
|
1962
2080
|
body = JSON.parse(await readBody(req));
|
|
1963
2081
|
}
|
|
1964
2082
|
catch {
|
|
1965
|
-
json(res,
|
|
2083
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
1966
2084
|
return;
|
|
1967
2085
|
}
|
|
1968
2086
|
if (typeof body.visible !== "boolean") {
|
|
1969
|
-
json(res,
|
|
2087
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2088
|
+
error: "Missing or invalid 'visible' field",
|
|
2089
|
+
});
|
|
1970
2090
|
return;
|
|
1971
2091
|
}
|
|
1972
2092
|
// sessionId patch semantics: absent = preserve, null = clear,
|
|
@@ -2013,23 +2133,25 @@ export function createRequestHandler(deps) {
|
|
|
2013
2133
|
}
|
|
2014
2134
|
}
|
|
2015
2135
|
}
|
|
2016
|
-
json(res,
|
|
2136
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
2017
2137
|
return;
|
|
2018
2138
|
}
|
|
2019
2139
|
// --- Push notification routes ---
|
|
2020
2140
|
// GET /api/beta/push/vapid-key
|
|
2021
2141
|
if (url === "/api/beta/push/vapid-key" && req.method === "GET") {
|
|
2022
2142
|
if (!deps.pushService) {
|
|
2023
|
-
json(res,
|
|
2143
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2024
2144
|
return;
|
|
2025
2145
|
}
|
|
2026
|
-
json(res,
|
|
2146
|
+
json(res, HTTP_STATUS.OK, {
|
|
2147
|
+
publicKey: deps.pushService.getPublicKey(),
|
|
2148
|
+
});
|
|
2027
2149
|
return;
|
|
2028
2150
|
}
|
|
2029
2151
|
// POST /api/beta/push/subscribe
|
|
2030
2152
|
if (url === "/api/beta/push/subscribe" && req.method === "POST") {
|
|
2031
2153
|
if (!deps.pushService) {
|
|
2032
|
-
json(res,
|
|
2154
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2033
2155
|
return;
|
|
2034
2156
|
}
|
|
2035
2157
|
const chunks = [];
|
|
@@ -2040,11 +2162,13 @@ export function createRequestHandler(deps) {
|
|
|
2040
2162
|
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2041
2163
|
}
|
|
2042
2164
|
catch {
|
|
2043
|
-
json(res,
|
|
2165
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
2044
2166
|
return;
|
|
2045
2167
|
}
|
|
2046
2168
|
if (!body.endpoint || !body.keys?.auth || !body.keys.p256dh) {
|
|
2047
|
-
json(res,
|
|
2169
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2170
|
+
error: "Missing endpoint or keys (auth, p256dh)",
|
|
2171
|
+
});
|
|
2048
2172
|
return;
|
|
2049
2173
|
}
|
|
2050
2174
|
store.saveSubscription(body.endpoint, body.keys.auth, body.keys.p256dh);
|
|
@@ -2052,13 +2176,13 @@ export function createRequestHandler(deps) {
|
|
|
2052
2176
|
if (body.clientId && deps.pushService) {
|
|
2053
2177
|
deps.pushService.registerClient(body.clientId, body.endpoint);
|
|
2054
2178
|
}
|
|
2055
|
-
json(res,
|
|
2179
|
+
json(res, HTTP_STATUS.CREATED, { ok: true });
|
|
2056
2180
|
return;
|
|
2057
2181
|
}
|
|
2058
2182
|
// POST /api/beta/push/register-client — associate clientId with push endpoint
|
|
2059
2183
|
if (url === "/api/beta/push/register-client" && req.method === "POST") {
|
|
2060
2184
|
if (!deps.pushService) {
|
|
2061
|
-
json(res,
|
|
2185
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2062
2186
|
return;
|
|
2063
2187
|
}
|
|
2064
2188
|
const chunks = [];
|
|
@@ -2069,21 +2193,23 @@ export function createRequestHandler(deps) {
|
|
|
2069
2193
|
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2070
2194
|
}
|
|
2071
2195
|
catch {
|
|
2072
|
-
json(res,
|
|
2196
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
2073
2197
|
return;
|
|
2074
2198
|
}
|
|
2075
2199
|
if (!body.clientId || !body.endpoint) {
|
|
2076
|
-
json(res,
|
|
2200
|
+
json(res, HTTP_STATUS.BAD_REQUEST, {
|
|
2201
|
+
error: "Missing clientId or endpoint",
|
|
2202
|
+
});
|
|
2077
2203
|
return;
|
|
2078
2204
|
}
|
|
2079
2205
|
deps.pushService.registerClient(body.clientId, body.endpoint);
|
|
2080
|
-
json(res,
|
|
2206
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
2081
2207
|
return;
|
|
2082
2208
|
}
|
|
2083
2209
|
// POST /api/beta/push/unsubscribe
|
|
2084
2210
|
if (url === "/api/beta/push/unsubscribe" && req.method === "POST") {
|
|
2085
2211
|
if (!deps.pushService) {
|
|
2086
|
-
json(res,
|
|
2212
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
|
|
2087
2213
|
return;
|
|
2088
2214
|
}
|
|
2089
2215
|
const chunks = [];
|
|
@@ -2094,16 +2220,16 @@ export function createRequestHandler(deps) {
|
|
|
2094
2220
|
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
2095
2221
|
}
|
|
2096
2222
|
catch {
|
|
2097
|
-
json(res,
|
|
2223
|
+
json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
|
|
2098
2224
|
return;
|
|
2099
2225
|
}
|
|
2100
2226
|
if (body.endpoint) {
|
|
2101
2227
|
store.removeSubscription(body.endpoint);
|
|
2102
2228
|
}
|
|
2103
|
-
json(res,
|
|
2229
|
+
json(res, HTTP_STATUS.OK, { ok: true });
|
|
2104
2230
|
return;
|
|
2105
2231
|
}
|
|
2106
|
-
json(res,
|
|
2232
|
+
json(res, HTTP_STATUS.NOT_FOUND, { error: "Not found" });
|
|
2107
2233
|
return;
|
|
2108
2234
|
}
|
|
2109
2235
|
// --- Static files ---
|
|
@@ -2113,7 +2239,7 @@ export function createRequestHandler(deps) {
|
|
|
2113
2239
|
staticPath = "/" + htmlEntry.file;
|
|
2114
2240
|
const filePath = join(deps.publicDir, staticPath);
|
|
2115
2241
|
if (!filePath.startsWith(deps.publicDir)) {
|
|
2116
|
-
res.writeHead(
|
|
2242
|
+
res.writeHead(HTTP_STATUS.FORBIDDEN);
|
|
2117
2243
|
res.end("Forbidden");
|
|
2118
2244
|
return;
|
|
2119
2245
|
}
|
|
@@ -2136,11 +2262,11 @@ export function createRequestHandler(deps) {
|
|
|
2136
2262
|
// CSP applies to HTML entrypoints (where script/style execute).
|
|
2137
2263
|
if (htmlEntry)
|
|
2138
2264
|
headers["Content-Security-Policy"] = CSP_POLICY;
|
|
2139
|
-
res.writeHead(
|
|
2265
|
+
res.writeHead(HTTP_STATUS.OK, headers);
|
|
2140
2266
|
res.end(data);
|
|
2141
2267
|
}
|
|
2142
2268
|
catch {
|
|
2143
|
-
res.writeHead(
|
|
2269
|
+
res.writeHead(HTTP_STATUS.NOT_FOUND);
|
|
2144
2270
|
res.end("Not found");
|
|
2145
2271
|
}
|
|
2146
2272
|
};
|