carouselbot 0.2.0 → 0.3.1
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/README.md +23 -3
- package/guidance/design.md +1 -0
- package/package.json +2 -1
- package/skill/carouselbot/SKILL.md +17 -4
- package/src/companion.mjs +159 -44
- package/src/config.mjs +20 -0
- package/src/daemon.mjs +200 -12
- package/src/local-fonts.mjs +574 -0
- package/src/mcp-server.mjs +31 -11
- package/src/setup.mjs +5 -4
package/src/daemon.mjs
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { appendFile, mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import { basename, extname } from "node:path";
|
|
5
|
+
import { basename, delimiter, extname } from "node:path";
|
|
6
6
|
import {
|
|
7
7
|
ALLOWED_ORIGINS, AUDIT_LOG_PATH, BRIDGE_HOST, BRIDGE_PORT, BRIDGE_URL, DAEMON_LOCK_PATH,
|
|
8
|
-
|
|
8
|
+
DAEMON_API_VERSION, DAEMON_INTERNAL_ACTIONS, DAEMON_STATE_PATH, PACKAGE_NAME, PACKAGE_VERSION,
|
|
9
|
+
PROTOCOL_VERSION, STATE_DIRECTORY,
|
|
9
10
|
} from "./config.mjs";
|
|
11
|
+
import { createLocalFontService } from "./local-fonts.mjs";
|
|
10
12
|
|
|
11
13
|
const MAX_JSON_BYTES = 40 * 1024 * 1024;
|
|
12
14
|
const MAX_MEDIA_BYTES = 25 * 1024 * 1024;
|
|
13
15
|
const EDITOR_TTL_MS = Number(process.env.CAROUSELBOT_EDITOR_TTL_MS || process.env.SLIDE_STUDIO_EDITOR_TTL_MS) || 60_000;
|
|
14
16
|
const CLIENT_TTL_MS = 45_000;
|
|
15
17
|
const MEDIA_TTL_MS = 5 * 60_000;
|
|
18
|
+
const FONT_MEDIA_TTL_MS = 5 * 60_000;
|
|
19
|
+
const MAX_FONT_MEDIA_ITEMS = 32;
|
|
20
|
+
const MAX_FONT_MEDIA_BYTES = 256 * 1024 * 1024;
|
|
16
21
|
const COMMAND_TIMEOUT_MS = 90_000;
|
|
17
22
|
const EDIT_SESSION_TTL_MS = Number(process.env.CAROUSELBOT_EDIT_SESSION_TTL_MS || process.env.SLIDE_STUDIO_EDIT_SESSION_TTL_MS) || 5 * 60_000;
|
|
18
23
|
const MAX_AUDIT_EVENTS = 500;
|
|
@@ -24,8 +29,17 @@ const editors = new Map();
|
|
|
24
29
|
const clients = new Map();
|
|
25
30
|
const inflight = new Map();
|
|
26
31
|
const media = new Map();
|
|
32
|
+
const fontMedia = new Map();
|
|
27
33
|
const editSessions = new Map();
|
|
28
34
|
const auditEvents = [];
|
|
35
|
+
const configuredFontDirectories = String(process.env.CAROUSELBOT_FONT_DIRS || process.env.SLIDE_STUDIO_FONT_DIRS || "")
|
|
36
|
+
.split(delimiter)
|
|
37
|
+
.map((value) => value.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
const localFonts = createLocalFontService({
|
|
40
|
+
cacheDirectory: STATE_DIRECTORY,
|
|
41
|
+
...(configuredFontDirectories.length ? { directories: configuredFontDirectories } : {}),
|
|
42
|
+
});
|
|
29
43
|
let focusedEditorId = null;
|
|
30
44
|
let lockHandle = null;
|
|
31
45
|
let idleSince = null;
|
|
@@ -65,6 +79,19 @@ function codedError(code, message, details = {}) {
|
|
|
65
79
|
return error;
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
function daemonHealth(details = {}) {
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
service: PACKAGE_NAME,
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
version: PACKAGE_VERSION,
|
|
88
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
89
|
+
daemonApiVersion: DAEMON_API_VERSION,
|
|
90
|
+
capabilities: { internalActions: [...DAEMON_INTERNAL_ACTIONS] },
|
|
91
|
+
...details,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
function publicSession(session) {
|
|
69
96
|
return {
|
|
70
97
|
id: session.id,
|
|
@@ -189,7 +216,7 @@ function browserCors(origin) {
|
|
|
189
216
|
"Access-Control-Allow-Origin": origin,
|
|
190
217
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
191
218
|
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
192
|
-
"Access-Control-Expose-Headers": "X-CarouselBot-Filename, X-Slide-Studio-Filename",
|
|
219
|
+
"Access-Control-Expose-Headers": "X-CarouselBot-Filename, X-Slide-Studio-Filename, X-CarouselBot-Local-Font-Id",
|
|
193
220
|
"Access-Control-Allow-Private-Network": "true",
|
|
194
221
|
"Access-Control-Max-Age": "600",
|
|
195
222
|
"Cache-Control": "no-store",
|
|
@@ -203,6 +230,18 @@ function sendJson(response, statusCode, value, headers = {}) {
|
|
|
203
230
|
response.end(body);
|
|
204
231
|
}
|
|
205
232
|
|
|
233
|
+
function sendLocalFont(response, item, headers = {}) {
|
|
234
|
+
response.writeHead(200, {
|
|
235
|
+
...headers,
|
|
236
|
+
"Content-Type": item.mimeType,
|
|
237
|
+
"Content-Length": item.buffer.length,
|
|
238
|
+
"X-CarouselBot-Filename": encodeURIComponent(item.filename),
|
|
239
|
+
"X-Slide-Studio-Filename": encodeURIComponent(item.filename),
|
|
240
|
+
"X-CarouselBot-Local-Font-Id": encodeURIComponent(item.font.localFontId),
|
|
241
|
+
});
|
|
242
|
+
response.end(item.buffer);
|
|
243
|
+
}
|
|
244
|
+
|
|
206
245
|
function readJson(request) {
|
|
207
246
|
return new Promise((resolve, reject) => {
|
|
208
247
|
const chunks = [];
|
|
@@ -269,6 +308,7 @@ function disconnectEditor(editorId, message = "Browser editor disconnected.") {
|
|
|
269
308
|
endEditorPoll(editor);
|
|
270
309
|
for (const session of editSessions.values()) if (session.editorId === editorId) releaseEditSession(session.id, "editor disconnected");
|
|
271
310
|
editors.delete(editorId);
|
|
311
|
+
for (const [id, item] of fontMedia) if (item.editorId === editorId) fontMedia.delete(id);
|
|
272
312
|
if (focusedEditorId === editorId) focusedEditorId = null;
|
|
273
313
|
for (const [requestId, pending] of inflight) {
|
|
274
314
|
if (pending.editorId !== editorId) continue;
|
|
@@ -305,6 +345,22 @@ function selectEditor(clientId) {
|
|
|
305
345
|
throw new Error("Multiple editors are connected and none is selected. Call list_editors, then select_editor.");
|
|
306
346
|
}
|
|
307
347
|
|
|
348
|
+
function requireLocalFontPermission(editor) {
|
|
349
|
+
if (editor?.localFontsEnabled) return editor;
|
|
350
|
+
throw codedError("FONT_PERMISSION_REQUIRED", "Open CarouselBot and enable local fonts.");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function selectLocalFontEditor(clientId) {
|
|
354
|
+
return requireLocalFontPermission(selectEditor(clientId));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function localFontEditorForCall(clientId, editSessionId) {
|
|
358
|
+
if (!editSessionId) return selectLocalFontEditor(clientId);
|
|
359
|
+
const session = requireEditSession(editSessionId);
|
|
360
|
+
session.lastClientId = clientId;
|
|
361
|
+
return requireLocalFontPermission(editors.get(session.editorId));
|
|
362
|
+
}
|
|
363
|
+
|
|
308
364
|
function resolveBrowserTarget(clientId, { editSessionId, mutating, projectId }) {
|
|
309
365
|
const client = clients.get(clientId) || { id: clientId, name: "MCP agent" };
|
|
310
366
|
if (editSessionId) {
|
|
@@ -348,10 +404,21 @@ function callBrowser(clientId, toolName, operation, label, { editSessionId = nul
|
|
|
348
404
|
return new Promise((resolve, reject) => {
|
|
349
405
|
const timer = setTimeout(() => {
|
|
350
406
|
inflight.delete(requestId);
|
|
407
|
+
if (operation?.fontMediaId) fontMedia.delete(operation.fontMediaId);
|
|
351
408
|
recordAudit({ action: "tool.result", client, session, editorId: editor.id, projectId, toolName, status: "error", message: "Browser timeout" });
|
|
352
409
|
reject(codedError("BROWSER_TIMEOUT", "The browser did not answer within 90 seconds."));
|
|
353
410
|
}, COMMAND_TIMEOUT_MS);
|
|
354
|
-
inflight.set(requestId, {
|
|
411
|
+
inflight.set(requestId, {
|
|
412
|
+
resolve,
|
|
413
|
+
reject,
|
|
414
|
+
timer,
|
|
415
|
+
editorId: editor.id,
|
|
416
|
+
client,
|
|
417
|
+
session,
|
|
418
|
+
projectId,
|
|
419
|
+
toolName,
|
|
420
|
+
fontMediaId: operation?.fontMediaId || null,
|
|
421
|
+
});
|
|
355
422
|
queueEditorEvent(editor, { kind: "command", requestId, toolName, operation, label, editSessionId: session?.id || null, agent: publicClient(client) });
|
|
356
423
|
});
|
|
357
424
|
}
|
|
@@ -382,6 +449,51 @@ async function prepareMedia(filePath) {
|
|
|
382
449
|
return { mediaId: id, filename: basename(filePath), mimeType, size: buffer.length };
|
|
383
450
|
}
|
|
384
451
|
|
|
452
|
+
function localFontFilename(font, mimeType) {
|
|
453
|
+
const extension = ({
|
|
454
|
+
"font/ttf": "ttf",
|
|
455
|
+
"font/otf": "otf",
|
|
456
|
+
"font/woff": "woff",
|
|
457
|
+
"font/woff2": "woff2",
|
|
458
|
+
})[mimeType] || "font";
|
|
459
|
+
const stem = String(font?.postscriptName || font?.localFontId || "local-font")
|
|
460
|
+
.replace(/[^a-z0-9._-]+/gi, "-")
|
|
461
|
+
.replace(/^-+|-+$/g, "") || "local-font";
|
|
462
|
+
return `${stem}.${extension}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function readLocalFontFace(localFontId) {
|
|
466
|
+
if (!localFontId || typeof localFontId !== "string") throw codedError("FONT_NOT_FOUND", "A valid localFontId is required.");
|
|
467
|
+
const prepared = await localFonts.readFace(localFontId);
|
|
468
|
+
if (!prepared?.font || !prepared?.buffer) throw codedError("FONT_NOT_FOUND", `Local font is unavailable: ${localFontId}`);
|
|
469
|
+
const buffer = Buffer.isBuffer(prepared.buffer) ? prepared.buffer : Buffer.from(prepared.buffer);
|
|
470
|
+
const mimeType = prepared.mimeType || "application/octet-stream";
|
|
471
|
+
return { font: prepared.font, buffer, mimeType, filename: localFontFilename(prepared.font, mimeType) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function prepareFont(clientId, localFontId, editSessionId) {
|
|
475
|
+
const editor = localFontEditorForCall(clientId, editSessionId);
|
|
476
|
+
const prepared = await readLocalFontFace(localFontId);
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
let retainedBytes = 0;
|
|
479
|
+
for (const [id, item] of fontMedia) {
|
|
480
|
+
if (item.expiresAt < now) fontMedia.delete(id);
|
|
481
|
+
else retainedBytes += item.buffer.length;
|
|
482
|
+
}
|
|
483
|
+
if (fontMedia.size >= MAX_FONT_MEDIA_ITEMS || retainedBytes + prepared.buffer.length > MAX_FONT_MEDIA_BYTES) {
|
|
484
|
+
throw codedError("FONT_TRANSFER_LIMIT", "Too many local fonts are waiting to be transferred. Finish the pending imports and retry.");
|
|
485
|
+
}
|
|
486
|
+
const fontMediaId = randomUUID();
|
|
487
|
+
fontMedia.set(fontMediaId, {
|
|
488
|
+
...prepared,
|
|
489
|
+
id: fontMediaId,
|
|
490
|
+
editorId: editor.id,
|
|
491
|
+
localFontId: prepared.font.localFontId,
|
|
492
|
+
expiresAt: now + FONT_MEDIA_TTL_MS,
|
|
493
|
+
});
|
|
494
|
+
return { font: prepared.font, fontMediaId };
|
|
495
|
+
}
|
|
496
|
+
|
|
385
497
|
async function writeExport(filePath, data, overwrite) {
|
|
386
498
|
const buffer = Buffer.from(data, "base64");
|
|
387
499
|
const handle = await open(filePath, overwrite ? "w" : "wx", 0o600).catch((error) => {
|
|
@@ -430,6 +542,11 @@ async function handleInternalCall(body) {
|
|
|
430
542
|
const events = auditEvents.filter((event) => (!body.projectId || event.projectId === body.projectId) && (!body.status || event.status === body.status));
|
|
431
543
|
return { events: events.slice(-limit).reverse(), localLogPath: AUDIT_LOG_PATH };
|
|
432
544
|
}
|
|
545
|
+
if (body.action === "list_local_fonts") {
|
|
546
|
+
localFontEditorForCall(body.clientId, body.editSessionId);
|
|
547
|
+
return localFonts.list({ query: body.query, limit: body.limit, cursor: body.cursor, sort: body.sort });
|
|
548
|
+
}
|
|
549
|
+
if (body.action === "prepare_font") return prepareFont(body.clientId, body.localFontId, body.editSessionId);
|
|
433
550
|
if (body.action === "prepare_media") return prepareMedia(body.path);
|
|
434
551
|
if (body.action === "write_export") return writeExport(body.path, body.data, Boolean(body.overwrite));
|
|
435
552
|
if (body.action === "notify") {
|
|
@@ -443,7 +560,10 @@ async function handleInternalCall(body) {
|
|
|
443
560
|
for (const item of body.items) results.push(await callBrowser(body.clientId, item.toolName || "apply_operations", item.operation, item.label, { editSessionId: body.editSessionId, mutating: true }));
|
|
444
561
|
return { applied: results.length, results };
|
|
445
562
|
}
|
|
446
|
-
throw
|
|
563
|
+
throw codedError("UNSUPPORTED_INTERNAL_ACTION", `Unknown internal action: ${body.action}`, {
|
|
564
|
+
action: body.action,
|
|
565
|
+
supportedActions: [...DAEMON_INTERNAL_ACTIONS],
|
|
566
|
+
});
|
|
447
567
|
}
|
|
448
568
|
|
|
449
569
|
const server = createServer(async (request, response) => {
|
|
@@ -461,13 +581,13 @@ const server = createServer(async (request, response) => {
|
|
|
461
581
|
}
|
|
462
582
|
if (url.pathname === "/health" && request.method === "GET") {
|
|
463
583
|
if (origin && !cors) return sendJson(response, 403, { error: "Origin not allowed." });
|
|
464
|
-
return sendJson(response, 200, {
|
|
584
|
+
return sendJson(response, 200, daemonHealth({ editors: activeEditors().length, agents: activeClients().length }), cors || {});
|
|
465
585
|
}
|
|
466
586
|
|
|
467
587
|
try {
|
|
468
588
|
if (url.pathname.startsWith("/internal/")) {
|
|
469
589
|
if (!requireInternal(request, response)) return;
|
|
470
|
-
if (url.pathname === "/internal/health" && request.method === "GET") return sendJson(response, 200,
|
|
590
|
+
if (url.pathname === "/internal/health" && request.method === "GET") return sendJson(response, 200, daemonHealth());
|
|
471
591
|
if (url.pathname === "/internal/shutdown" && request.method === "POST") {
|
|
472
592
|
sendJson(response, 202, { ok: true, pid: process.pid });
|
|
473
593
|
setImmediate(() => void shutdown());
|
|
@@ -518,12 +638,14 @@ const server = createServer(async (request, response) => {
|
|
|
518
638
|
}
|
|
519
639
|
const editor = {
|
|
520
640
|
id: body.editorId, queue: [], poll: null, pageUrl: body.pageUrl,
|
|
521
|
-
pollTimer: null, state: body.state, lastSeen: Date.now(), cors,
|
|
641
|
+
pollTimer: null, state: body.state, lastSeen: Date.now(), cors,
|
|
642
|
+
localFontsEnabled: body.localFontsEnabled === true,
|
|
643
|
+
sessionToken: randomBytes(32).toString("base64url"),
|
|
522
644
|
};
|
|
523
645
|
editors.set(editor.id, editor);
|
|
524
646
|
if (body.hasFocus && body.visibilityState === "visible") focusedEditorId = editor.id;
|
|
525
647
|
log(`Editor connected (${editor.id.slice(0, 8)})`);
|
|
526
|
-
return sendJson(response, 200, { ok: true, editorId: editor.id, sessionToken: editor.sessionToken, protocolVersion: PROTOCOL_VERSION, version: PACKAGE_VERSION, agents: activeClients().map(publicClient), editSessions: activeEditSessions().map(publicSession) }, cors);
|
|
648
|
+
return sendJson(response, 200, { ok: true, editorId: editor.id, sessionToken: editor.sessionToken, protocolVersion: PROTOCOL_VERSION, version: PACKAGE_VERSION, localFontsEnabled: editor.localFontsEnabled, agents: activeClients().map(publicClient), editSessions: activeEditSessions().map(publicSession) }, cors);
|
|
527
649
|
}
|
|
528
650
|
if (url.pathname === "/activate" && request.method === "POST") {
|
|
529
651
|
const body = await readJson(request);
|
|
@@ -545,6 +667,55 @@ const server = createServer(async (request, response) => {
|
|
|
545
667
|
disconnectEditor(editor.id);
|
|
546
668
|
return sendJson(response, 200, { ok: true, editorId: editor.id }, cors);
|
|
547
669
|
}
|
|
670
|
+
if (url.pathname === "/fonts/enable" && request.method === "POST") {
|
|
671
|
+
const body = await readJson(request);
|
|
672
|
+
const editor = requireEditor(request, response, body.editorId, cors);
|
|
673
|
+
if (!editor) return;
|
|
674
|
+
editor.localFontsEnabled = true;
|
|
675
|
+
return sendJson(response, 200, { enabled: true }, cors);
|
|
676
|
+
}
|
|
677
|
+
if (url.pathname === "/fonts" && request.method === "GET") {
|
|
678
|
+
const editor = requireEditor(request, response, url.searchParams.get("editorId"), cors);
|
|
679
|
+
if (!editor) return;
|
|
680
|
+
requireLocalFontPermission(editor);
|
|
681
|
+
const result = await localFonts.list({
|
|
682
|
+
query: url.searchParams.get("query") || "",
|
|
683
|
+
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
|
|
684
|
+
cursor: url.searchParams.get("cursor") || null,
|
|
685
|
+
sort: url.searchParams.get("sort") || undefined,
|
|
686
|
+
});
|
|
687
|
+
return sendJson(response, 200, result, cors);
|
|
688
|
+
}
|
|
689
|
+
if (url.pathname === "/fonts/use" && request.method === "POST") {
|
|
690
|
+
const body = await readJson(request);
|
|
691
|
+
const editor = requireEditor(request, response, body.editorId, cors);
|
|
692
|
+
if (!editor) return;
|
|
693
|
+
requireLocalFontPermission(editor);
|
|
694
|
+
const font = await localFonts.markUsed(body.localFontId);
|
|
695
|
+
if (!font) throw codedError("FONT_NOT_FOUND", `Local font is unavailable: ${body.localFontId}`);
|
|
696
|
+
return sendJson(response, 200, { font }, cors);
|
|
697
|
+
}
|
|
698
|
+
if (url.pathname.startsWith("/fonts/") && request.method === "GET") {
|
|
699
|
+
const editor = requireEditor(request, response, url.searchParams.get("editorId"), cors);
|
|
700
|
+
if (!editor) return;
|
|
701
|
+
requireLocalFontPermission(editor);
|
|
702
|
+
const localFontId = decodeURIComponent(url.pathname.slice("/fonts/".length));
|
|
703
|
+
return sendLocalFont(response, await readLocalFontFace(localFontId), cors);
|
|
704
|
+
}
|
|
705
|
+
if (url.pathname.startsWith("/font-media/") && request.method === "GET") {
|
|
706
|
+
const editor = requireEditor(request, response, url.searchParams.get("editorId"), cors);
|
|
707
|
+
if (!editor) return;
|
|
708
|
+
requireLocalFontPermission(editor);
|
|
709
|
+
const id = decodeURIComponent(url.pathname.slice("/font-media/".length));
|
|
710
|
+
const item = fontMedia.get(id);
|
|
711
|
+
if (!item || item.expiresAt < Date.now()) {
|
|
712
|
+
fontMedia.delete(id);
|
|
713
|
+
throw codedError("FONT_MEDIA_UNAVAILABLE", "Local font transfer is missing or expired.");
|
|
714
|
+
}
|
|
715
|
+
if (item.editorId !== editor.id) throw codedError("FONT_MEDIA_UNAVAILABLE", "Local font transfer is missing or expired.");
|
|
716
|
+
fontMedia.delete(id);
|
|
717
|
+
return sendLocalFont(response, item, cors);
|
|
718
|
+
}
|
|
548
719
|
if (url.pathname === "/events" && request.method === "GET") {
|
|
549
720
|
const editor = requireEditor(request, response, url.searchParams.get("editorId"), cors);
|
|
550
721
|
if (!editor) return;
|
|
@@ -580,6 +751,7 @@ const server = createServer(async (request, response) => {
|
|
|
580
751
|
if (!pending || pending.editorId !== editor.id) return sendJson(response, 404, { error: "Unknown request." }, cors);
|
|
581
752
|
inflight.delete(body.requestId);
|
|
582
753
|
clearTimeout(pending.timer);
|
|
754
|
+
if (pending.fontMediaId) fontMedia.delete(pending.fontMediaId);
|
|
583
755
|
if (body.state) editor.state = body.state;
|
|
584
756
|
if (body.ok) {
|
|
585
757
|
try {
|
|
@@ -612,8 +784,16 @@ const server = createServer(async (request, response) => {
|
|
|
612
784
|
return sendJson(response, 404, { error: "Not found." }, cors);
|
|
613
785
|
} catch (error) {
|
|
614
786
|
const headers = cors || {};
|
|
615
|
-
const statusCode =
|
|
616
|
-
|
|
787
|
+
const statusCode = ["ENOENT", "FONT_NOT_FOUND", "FONT_MEDIA_UNAVAILABLE"].includes(error.code)
|
|
788
|
+
? 404
|
|
789
|
+
: ["EACCES", "FONT_PERMISSION_REQUIRED"].includes(error.code)
|
|
790
|
+
? 403
|
|
791
|
+
: error.code === "FONT_TRANSFER_LIMIT" ? 429 : 400;
|
|
792
|
+
return sendJson(response, statusCode, {
|
|
793
|
+
error: error.message,
|
|
794
|
+
...(error.code ? { code: error.code } : {}),
|
|
795
|
+
...(error.details && Object.keys(error.details).length ? { details: error.details } : {}),
|
|
796
|
+
}, headers);
|
|
617
797
|
}
|
|
618
798
|
});
|
|
619
799
|
|
|
@@ -643,7 +823,14 @@ async function acquireDaemonLock() {
|
|
|
643
823
|
|
|
644
824
|
async function writeDaemonState() {
|
|
645
825
|
const temporary = `${DAEMON_STATE_PATH}.${process.pid}.tmp`;
|
|
646
|
-
await writeFile(temporary, JSON.stringify({
|
|
826
|
+
await writeFile(temporary, JSON.stringify({
|
|
827
|
+
pid: process.pid,
|
|
828
|
+
port: BRIDGE_PORT,
|
|
829
|
+
secret: daemonSecret,
|
|
830
|
+
version: PACKAGE_VERSION,
|
|
831
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
832
|
+
daemonApiVersion: DAEMON_API_VERSION,
|
|
833
|
+
}), { mode: 0o600 });
|
|
647
834
|
await rename(temporary, DAEMON_STATE_PATH);
|
|
648
835
|
}
|
|
649
836
|
|
|
@@ -661,6 +848,7 @@ async function cleanup() {
|
|
|
661
848
|
setInterval(() => {
|
|
662
849
|
const now = Date.now();
|
|
663
850
|
for (const [id, item] of media) if (item.expiresAt < now) media.delete(id);
|
|
851
|
+
for (const [id, item] of fontMedia) if (item.expiresAt < now) fontMedia.delete(id);
|
|
664
852
|
for (const session of editSessions.values()) if (session.lastSeen < now - EDIT_SESSION_TTL_MS) releaseEditSession(session.id, "lease expired");
|
|
665
853
|
let clientsChanged = false;
|
|
666
854
|
for (const [id, client] of clients) if (client.lastSeen < now - CLIENT_TTL_MS) {
|