@workerdeck/server 0.7.0 → 0.9.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/build/index.d.mts +95 -5
- package/build/index.mjs +486 -71
- package/build/index.mjs.map +1 -1
- package/package.json +6 -6
package/build/index.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { closeSync, constants, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
|
-
import {
|
|
8
|
-
import { BrowserBridgeExecutor, SessionRunner, checkClaudeAuth } from "@workerdeck/core";
|
|
7
|
+
import { BrowserBridgeExecutor, SessionRunner, attachmentKind, checkClaudeAuth, getEngineAdapter, normalizeMediaType } from "@workerdeck/core";
|
|
9
8
|
import { JobQueue } from "@workerdeck/queue";
|
|
10
|
-
import {
|
|
9
|
+
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, supportsPermissionMode } from "@workerdeck/protocol";
|
|
11
10
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
12
11
|
//#region src/host-files.ts
|
|
13
12
|
/**
|
|
@@ -364,6 +363,132 @@ function subsequenceScore(haystack, needle) {
|
|
|
364
363
|
return score - haystack.length / 100;
|
|
365
364
|
}
|
|
366
365
|
//#endregion
|
|
366
|
+
//#region src/attachments.ts
|
|
367
|
+
const DEFAULT_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
368
|
+
const DEFAULT_MAX_SESSION_BYTES = 64 * 1024 * 1024;
|
|
369
|
+
/**
|
|
370
|
+
* Per-session hold for files the user attached to a message.
|
|
371
|
+
*
|
|
372
|
+
* In memory, and deliberately so. An attachment is only *needed* for the instant
|
|
373
|
+
* between the upload and the message that names it; everything after that is
|
|
374
|
+
* convenience (a client re-rendering a thumbnail after a reattach). That is the
|
|
375
|
+
* same bargain `GET /sessions/:id/files` makes — the session's lifetime, no
|
|
376
|
+
* durability tier — and it keeps the gateway from accumulating a photo library
|
|
377
|
+
* on disk that nobody asked it to look after.
|
|
378
|
+
*
|
|
379
|
+
* Both caps are enforced here rather than at the route, so a host embedding the
|
|
380
|
+
* server cannot forget one: a single file that is too big is a 413, and so is a
|
|
381
|
+
* session whose total would go over.
|
|
382
|
+
*/
|
|
383
|
+
var AttachmentStore = class {
|
|
384
|
+
#bySession = /* @__PURE__ */ new Map();
|
|
385
|
+
#maxFileBytes;
|
|
386
|
+
#maxSessionBytes;
|
|
387
|
+
constructor(options = {}) {
|
|
388
|
+
this.#maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
389
|
+
this.#maxSessionBytes = options.maxSessionBytes ?? DEFAULT_MAX_SESSION_BYTES;
|
|
390
|
+
}
|
|
391
|
+
get maxFileBytes() {
|
|
392
|
+
return this.#maxFileBytes;
|
|
393
|
+
}
|
|
394
|
+
put(sessionId, name, mediaType, body) {
|
|
395
|
+
if (body.length === 0) return {
|
|
396
|
+
ok: false,
|
|
397
|
+
error: {
|
|
398
|
+
code: "empty",
|
|
399
|
+
message: "attachment is empty"
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
if (body.length > this.#maxFileBytes) return {
|
|
403
|
+
ok: false,
|
|
404
|
+
error: {
|
|
405
|
+
code: "too_large",
|
|
406
|
+
message: `attachment is larger than the ${this.#maxFileBytes}-byte limit`
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const type = normalizeMediaType(mediaType);
|
|
410
|
+
if (!attachmentKind(type)) return {
|
|
411
|
+
ok: false,
|
|
412
|
+
error: {
|
|
413
|
+
code: "unsupported_type",
|
|
414
|
+
message: `unsupported media type: ${type}`
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const held = this.#bySession.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
418
|
+
const heldBytes = [...held.values()].reduce((sum, a) => sum + a.bytes, 0);
|
|
419
|
+
if (heldBytes + body.length > this.#maxSessionBytes) return {
|
|
420
|
+
ok: false,
|
|
421
|
+
error: {
|
|
422
|
+
code: "session_full",
|
|
423
|
+
message: `session is already holding ${heldBytes} bytes of attachments (limit ${this.#maxSessionBytes})`
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const attachment = {
|
|
427
|
+
id: randomUUID(),
|
|
428
|
+
name: safeName(name),
|
|
429
|
+
mediaType: type,
|
|
430
|
+
bytes: body.length,
|
|
431
|
+
data: body.toString("base64")
|
|
432
|
+
};
|
|
433
|
+
held.set(attachment.id, attachment);
|
|
434
|
+
this.#bySession.set(sessionId, held);
|
|
435
|
+
return {
|
|
436
|
+
ok: true,
|
|
437
|
+
attachment: ref(attachment)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/** The stored record, bytes included — for the download route and for the send
|
|
441
|
+
* path that turns ids into content blocks. */
|
|
442
|
+
get(sessionId, id) {
|
|
443
|
+
return this.#bySession.get(sessionId)?.get(id);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Resolve the ids a `user_message` named, in the order given.
|
|
447
|
+
*
|
|
448
|
+
* Missing ids are reported rather than skipped: a message that quietly lost its
|
|
449
|
+
* picture reads as the model ignoring it, which is a far worse failure than a
|
|
450
|
+
* command that errors.
|
|
451
|
+
*/
|
|
452
|
+
resolve(sessionId, ids) {
|
|
453
|
+
const held = this.#bySession.get(sessionId);
|
|
454
|
+
const attachments = [];
|
|
455
|
+
const missing = [];
|
|
456
|
+
for (const id of ids) {
|
|
457
|
+
const found = held?.get(id);
|
|
458
|
+
if (found) attachments.push(found);
|
|
459
|
+
else missing.push(id);
|
|
460
|
+
}
|
|
461
|
+
return missing.length ? {
|
|
462
|
+
ok: false,
|
|
463
|
+
missing
|
|
464
|
+
} : {
|
|
465
|
+
ok: true,
|
|
466
|
+
attachments
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
drop(sessionId) {
|
|
470
|
+
this.#bySession.delete(sessionId);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
function ref(attachment) {
|
|
474
|
+
return {
|
|
475
|
+
id: attachment.id,
|
|
476
|
+
name: attachment.name,
|
|
477
|
+
mediaType: attachment.mediaType,
|
|
478
|
+
bytes: attachment.bytes
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* A display name, not a path. The name is echoed back to clients and put in front
|
|
483
|
+
* of the model in the text-attachment envelope, so directory separators, control
|
|
484
|
+
* characters and unbounded length all come off here.
|
|
485
|
+
*/
|
|
486
|
+
function safeName(name) {
|
|
487
|
+
const cleaned = (name.split(/[/\\]/).pop() ?? "").replace(/[\u0000-\u001f\u007f"<>]/g, "").trim();
|
|
488
|
+
if (cleaned === "" || cleaned === "." || cleaned === "..") return "attachment";
|
|
489
|
+
return cleaned.length > 120 ? cleaned.slice(0, 120) : cleaned;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
367
492
|
//#region src/registry.ts
|
|
368
493
|
/** In-memory session table. Terminal sessions stay listed until removed or the process exits. */
|
|
369
494
|
var SessionRegistry = class {
|
|
@@ -1092,18 +1217,6 @@ function parseRecord(value) {
|
|
|
1092
1217
|
}
|
|
1093
1218
|
//#endregion
|
|
1094
1219
|
//#region src/server.ts
|
|
1095
|
-
const defaultSdkSessionLister = async (options) => {
|
|
1096
|
-
return (await listSessions(options)).map((s) => ({
|
|
1097
|
-
sessionId: s.sessionId,
|
|
1098
|
-
summary: s.summary,
|
|
1099
|
-
lastModified: s.lastModified,
|
|
1100
|
-
createdAt: s.createdAt,
|
|
1101
|
-
customTitle: s.customTitle,
|
|
1102
|
-
firstPrompt: s.firstPrompt,
|
|
1103
|
-
gitBranch: s.gitBranch,
|
|
1104
|
-
cwd: s.cwd
|
|
1105
|
-
}));
|
|
1106
|
-
};
|
|
1107
1220
|
function json(res, status, body) {
|
|
1108
1221
|
const payload = JSON.stringify(body);
|
|
1109
1222
|
res.writeHead(status, {
|
|
@@ -1123,6 +1236,18 @@ async function readJsonBody(req, maxBytes) {
|
|
|
1123
1236
|
if (size === 0) return {};
|
|
1124
1237
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1125
1238
|
}
|
|
1239
|
+
/** Body as bytes, refusing anything over `maxBytes`. Attachments are the one
|
|
1240
|
+
* thing this server takes that isn't JSON. */
|
|
1241
|
+
async function readRawBody(req, maxBytes) {
|
|
1242
|
+
const chunks = [];
|
|
1243
|
+
let size = 0;
|
|
1244
|
+
for await (const chunk of req) {
|
|
1245
|
+
size += chunk.length;
|
|
1246
|
+
if (size > maxBytes) throw new Error("request body too large");
|
|
1247
|
+
chunks.push(chunk);
|
|
1248
|
+
}
|
|
1249
|
+
return Buffer.concat(chunks);
|
|
1250
|
+
}
|
|
1126
1251
|
/**
|
|
1127
1252
|
* Curated, view-only snapshot of a profile's config dir for GET /profiles/:name.
|
|
1128
1253
|
* Best-effort: a missing or unparseable settings.json just omits the settings block.
|
|
@@ -1210,6 +1335,10 @@ function contentTypeFor(filename) {
|
|
|
1210
1335
|
function isProviderProfile(profile) {
|
|
1211
1336
|
return profile.engine === "provider";
|
|
1212
1337
|
}
|
|
1338
|
+
/** The engine a profile runs, absent meaning 'claude' (pre-provider profiles). */
|
|
1339
|
+
function engineOf(profile) {
|
|
1340
|
+
return profile?.engine ?? "claude";
|
|
1341
|
+
}
|
|
1213
1342
|
/** Where the CLI's own resolution lands for a given environment: an explicit
|
|
1214
1343
|
* CLAUDE_CONFIG_DIR, else ~/.claude. */
|
|
1215
1344
|
function cliConfigDir(env) {
|
|
@@ -1264,6 +1393,8 @@ function createWorkerServer(options = {}) {
|
|
|
1264
1393
|
const basePath = options.basePath ?? "/v1";
|
|
1265
1394
|
const fallback = options.fallback;
|
|
1266
1395
|
const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
|
|
1396
|
+
/** The engine's adapter, honoring the test-only `engines` override. */
|
|
1397
|
+
const adapterFor = (engine) => options.engines?.[engine ?? "claude"] ?? getEngineAdapter(engine);
|
|
1267
1398
|
const hostBuildRunnerConfig = options.buildRunnerConfig ?? ((req) => req);
|
|
1268
1399
|
const declared = options.profiles ?? detectDefaultProfiles();
|
|
1269
1400
|
const declaredByName = new Map(declared.map((p) => [p.name, p]));
|
|
@@ -1277,10 +1408,13 @@ function createWorkerServer(options = {}) {
|
|
|
1277
1408
|
if (isProviderProfile(p)) {
|
|
1278
1409
|
if (!p.provider?.id) return `provider profile '${p.name}' is missing provider.id`;
|
|
1279
1410
|
if (!options.createEngineRunner) return `profile '${p.name}' uses engine 'provider' but no \`createEngineRunner\` was provided to build one`;
|
|
1411
|
+
} else if (p.engine === "codex") {
|
|
1412
|
+
if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
|
|
1413
|
+
if (p.session?.instructions) return `profile '${p.name}' declares session.instructions, which the codex engine cannot deliver — put instructions in the target repo’s AGENTS.md instead`;
|
|
1280
1414
|
} else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
|
|
1281
1415
|
if (options.disableBypassPermissions && p.defaults?.permissionMode === "bypassPermissions") return `profile '${p.name}' defaults to bypassPermissions but disableBypassPermissions is set`;
|
|
1282
1416
|
const fallbackMode = p.defaults?.permissionMode;
|
|
1283
|
-
if (fallbackMode && !supportsPermissionMode(p.engine, fallbackMode)) return `profile '${p.name}' defaults to permission mode '${fallbackMode}', which engine '${p
|
|
1417
|
+
if (fallbackMode && !supportsPermissionMode(p.engine, fallbackMode)) return `profile '${p.name}' defaults to permission mode '${fallbackMode}', which engine '${engineOf(p)}' does not support (supported: ${adapterFor(engineOf(p)).capabilities.permissionModes.join(", ")})`;
|
|
1284
1418
|
return null;
|
|
1285
1419
|
};
|
|
1286
1420
|
for (const p of options.profiles ?? []) {
|
|
@@ -1304,6 +1438,31 @@ function createWorkerServer(options = {}) {
|
|
|
1304
1438
|
...p,
|
|
1305
1439
|
managed: true
|
|
1306
1440
|
};
|
|
1441
|
+
/**
|
|
1442
|
+
* Response shape for a profile: the managed marker, the engine's capability
|
|
1443
|
+
* record, its static model catalog (correct from the first request — no
|
|
1444
|
+
* warm-up session, no process spawned), the availability verdict when one
|
|
1445
|
+
* has been probed, and the learned default model (the one thing a static
|
|
1446
|
+
* catalog cannot know: a claude profile's default is the operator's CLI
|
|
1447
|
+
* config, so it stays absent until a session on the profile reports it).
|
|
1448
|
+
* Read-only decoration — never persisted.
|
|
1449
|
+
*/
|
|
1450
|
+
const forResponse = (p) => {
|
|
1451
|
+
const adapter = adapterFor(p.engine);
|
|
1452
|
+
const base = {
|
|
1453
|
+
...withManagedFlag(p),
|
|
1454
|
+
capabilities: adapter.capabilities
|
|
1455
|
+
};
|
|
1456
|
+
if (adapter.catalog.models.length > 0) base.models = adapter.catalog.models;
|
|
1457
|
+
const defaultModel = profileDefaultModels.get(p.name);
|
|
1458
|
+
if (defaultModel) base.defaultModel = defaultModel;
|
|
1459
|
+
const probed = availability.get(p.name)?.verdict;
|
|
1460
|
+
if (probed && probed.available !== "unknown") {
|
|
1461
|
+
base.available = probed.available;
|
|
1462
|
+
if (probed.available === false) base.unavailableReason = probed.reason;
|
|
1463
|
+
}
|
|
1464
|
+
return base;
|
|
1465
|
+
};
|
|
1307
1466
|
/** Declared profiles first: a name collision means the code wins, and the stored
|
|
1308
1467
|
* one is unreachable rather than silently overriding server options. */
|
|
1309
1468
|
const allProfiles = () => [...declared, ...[...stored.values()].filter((p) => !declaredByName.has(p.name))];
|
|
@@ -1377,28 +1536,42 @@ function createWorkerServer(options = {}) {
|
|
|
1377
1536
|
* into 'default' by whatever assembles its runner. Returns an error message. */
|
|
1378
1537
|
const checkPermissionMode = (mode, profile) => {
|
|
1379
1538
|
if (mode === void 0 || supportsPermissionMode(profile?.engine, mode)) return null;
|
|
1380
|
-
return `permission mode '${mode}' is not supported by profile '${profile.name}' (engine '${profile
|
|
1539
|
+
return `permission mode '${mode}' is not supported by profile '${profile.name}' (engine '${engineOf(profile)}') — supported: ` + adapterFor(profile?.engine).capabilities.permissionModes.join(", ");
|
|
1381
1540
|
};
|
|
1382
1541
|
/**
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1542
|
+
* Refuse the request fields the resolved profile's engine cannot honor —
|
|
1543
|
+
* read off its capability record, so the create form's filtering and the
|
|
1544
|
+
* API boundary can never disagree. Refusing beats coercing: a caller who
|
|
1545
|
+
* asked for something the engine has no meaning for should be told, not
|
|
1546
|
+
* left wondering where the option went. Also enforces the provider grant
|
|
1547
|
+
* rules (capabilities narrow, never widen; MCP servers are the profile's to
|
|
1548
|
+
* declare — MCP tools are authoritative, server-side, with server
|
|
1549
|
+
* credentials, so honoring a client-supplied server would let a caller
|
|
1550
|
+
* point an authoritative tool anywhere it liked).
|
|
1392
1551
|
*/
|
|
1393
1552
|
const checkEngineGrants = (req, profile) => {
|
|
1553
|
+
const engine = engineOf(profile);
|
|
1554
|
+
const caps = adapterFor(profile?.engine).capabilities;
|
|
1555
|
+
const name = profile?.name ?? "default";
|
|
1556
|
+
if (!caps.sessionMcpServers && req.mcpServers && Object.keys(req.mcpServers).length > 0) return `profile '${name}' runs the ${engine} engine, whose MCP servers are declared outside the session request — a request cannot add its own`;
|
|
1557
|
+
if (!caps.budgets && (req.maxTurns !== void 0 || req.maxBudgetUsd !== void 0)) return `the ${engine} engine does not honor maxTurns/maxBudgetUsd`;
|
|
1558
|
+
if (!caps.settingSources && req.settingSources !== void 0) return `the ${engine} engine does not load settingSources`;
|
|
1559
|
+
if (!caps.resume && req.resume !== void 0) return `the ${engine} engine cannot resume a session`;
|
|
1560
|
+
if (req.forkSession && engine !== "claude") return `the ${engine} engine cannot fork a resumed session`;
|
|
1561
|
+
if (req.reasoningEffort !== void 0 && (!caps.reasoningEfforts || caps.reasoningEfforts.length === 0)) return `the ${engine} engine does not take a reasoningEffort`;
|
|
1394
1562
|
if (!profile || !isProviderProfile(profile)) return null;
|
|
1395
|
-
if (req.mcpServers && Object.keys(req.mcpServers).length > 0) return `profile '${profile.name}' runs the provider engine, whose MCP servers are declared on the profile (session.mcpServers) — a session request cannot add its own`;
|
|
1396
1563
|
const granted = profile.session?.capabilities;
|
|
1397
1564
|
if (!req.capabilities || !granted) return null;
|
|
1398
1565
|
const ungranted = req.capabilities.filter((c) => !granted.includes(c));
|
|
1399
1566
|
if (ungranted.length === 0) return null;
|
|
1400
1567
|
return `profile '${profile.name}' does not grant: ${ungranted.join(", ")} (granted: ${granted.join(", ") || "none"}) — a request may narrow capabilities, not widen them`;
|
|
1401
1568
|
};
|
|
1569
|
+
/** Drop request fields that are meaningless (not wrong) for the engine —
|
|
1570
|
+
* today just `questionBehavior` where no approval channel exists, so job
|
|
1571
|
+
* webhooks never grow phantom permission_requested expectations. */
|
|
1572
|
+
const stripInertFields = (req, profile) => {
|
|
1573
|
+
if (!adapterFor(profile?.engine).capabilities.interactiveApprovals) delete req.questionBehavior;
|
|
1574
|
+
};
|
|
1402
1575
|
/** Profile-aware config hook: fill the profile's defaults into unset request fields,
|
|
1403
1576
|
* run the host hook, then pin CLAUDE_CONFIG_DIR — the profile wins even when the
|
|
1404
1577
|
* host hook set its own env (see `claudeSessionEnv` for the one case the pin is
|
|
@@ -1411,7 +1584,7 @@ function createWorkerServer(options = {}) {
|
|
|
1411
1584
|
model: req.model ?? profile.defaults?.model ?? profile.provider?.model,
|
|
1412
1585
|
permissionMode: req.permissionMode ?? profile.defaults?.permissionMode
|
|
1413
1586
|
});
|
|
1414
|
-
if (
|
|
1587
|
+
if (engineOf(profile) !== "claude") return config;
|
|
1415
1588
|
const base = config.env ?? process.env;
|
|
1416
1589
|
const env = claudeSessionEnv(profile, base);
|
|
1417
1590
|
return env === base ? config : {
|
|
@@ -1435,8 +1608,11 @@ function createWorkerServer(options = {}) {
|
|
|
1435
1608
|
bridge,
|
|
1436
1609
|
restore
|
|
1437
1610
|
});
|
|
1438
|
-
|
|
1439
|
-
|
|
1611
|
+
return adapterFor(profile?.engine).createRunner({
|
|
1612
|
+
config,
|
|
1613
|
+
profile,
|
|
1614
|
+
restore
|
|
1615
|
+
});
|
|
1440
1616
|
};
|
|
1441
1617
|
const createRunner = async (config) => {
|
|
1442
1618
|
const runner = registry.register(await buildRunner(config));
|
|
@@ -1483,7 +1659,24 @@ function createWorkerServer(options = {}) {
|
|
|
1483
1659
|
};
|
|
1484
1660
|
};
|
|
1485
1661
|
const notifier = new SessionNotifier(options.notifications ?? {});
|
|
1486
|
-
|
|
1662
|
+
/**
|
|
1663
|
+
* What each claude profile's *default* model resolves to, learned from the
|
|
1664
|
+
* `capabilities` events of sessions that ran on it. The model *list* is the
|
|
1665
|
+
* adapter's static catalog now; the default is the one thing a catalog
|
|
1666
|
+
* cannot know (it is the operator's CLI config), so it alone is still
|
|
1667
|
+
* learned — and still absent on a cold server, the accepted regression.
|
|
1668
|
+
*/
|
|
1669
|
+
const profileDefaultModels = /* @__PURE__ */ new Map();
|
|
1670
|
+
const registry = new SessionRegistry({ onRegister: (runner) => {
|
|
1671
|
+
notifier.watch(runner);
|
|
1672
|
+
const profile = runner.info().profile;
|
|
1673
|
+
if (!profile) return;
|
|
1674
|
+
runner.subscribe((event) => {
|
|
1675
|
+
if (event.type !== "capabilities" || !event.defaultModel) return;
|
|
1676
|
+
profileDefaultModels.set(profile, event.defaultModel);
|
|
1677
|
+
});
|
|
1678
|
+
} });
|
|
1679
|
+
const attachmentStore = new AttachmentStore(options.attachments);
|
|
1487
1680
|
const bridge = new BridgeHub({
|
|
1488
1681
|
...options.bridge,
|
|
1489
1682
|
onResult: (sessionId, executionId, result) => {
|
|
@@ -1556,31 +1749,70 @@ function createWorkerServer(options = {}) {
|
|
|
1556
1749
|
});
|
|
1557
1750
|
};
|
|
1558
1751
|
/**
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
1561
|
-
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1752
|
+
* Availability, per profile: the adapter's probe run over the env the real
|
|
1753
|
+
* assembly path produces (so anything the host hook injects — a
|
|
1754
|
+
* CLAUDE_CODE_OAUTH_TOKEN, say — counts as logged in). Cached, and served on
|
|
1755
|
+
* `GET /profiles` as `available`/`unavailableReason`.
|
|
1756
|
+
*
|
|
1757
|
+
* Gated on `checkCredentials` like the old claude-only preflight (this is a
|
|
1758
|
+
* library; `pnpm test` must spawn nothing unless a test injects fake
|
|
1759
|
+
* adapters or probes). 'unknown' stays out of the cache's answers: a probe
|
|
1760
|
+
* that couldn't run is not evidence of a missing login. **Display-only**
|
|
1761
|
+
* downstream — session create against an unavailable profile still proceeds
|
|
1762
|
+
* and fails with the engine's own error, because the probe can be stale in
|
|
1763
|
+
* both directions and refusing on it would turn a probe bug into an outage.
|
|
1564
1764
|
*/
|
|
1565
|
-
const
|
|
1765
|
+
const availability = /* @__PURE__ */ new Map();
|
|
1766
|
+
const AVAILABILITY_TTL_MS = 6e4;
|
|
1767
|
+
/** Profiles already warned about on the console, so re-probes don't spam. */
|
|
1768
|
+
const availabilityWarned = /* @__PURE__ */ new Set();
|
|
1769
|
+
const sessionEnvFor = (profile) => {
|
|
1770
|
+
try {
|
|
1771
|
+
return buildRunnerConfig({
|
|
1772
|
+
cwd: process.cwd(),
|
|
1773
|
+
profile: profile.name
|
|
1774
|
+
}).env ?? process.env;
|
|
1775
|
+
} catch {
|
|
1776
|
+
return engineOf(profile) === "claude" ? claudeSessionEnv(profile, process.env) : process.env;
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
const probeProfile = (profile) => {
|
|
1566
1780
|
if (!options.checkCredentials) return;
|
|
1567
1781
|
const conf = options.checkCredentials === true ? {} : options.checkCredentials;
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1782
|
+
availability.set(profile.name, {
|
|
1783
|
+
verdict: availability.get(profile.name)?.verdict ?? { available: "unknown" },
|
|
1784
|
+
at: Date.now()
|
|
1785
|
+
});
|
|
1786
|
+
const adapter = adapterFor(profile.engine);
|
|
1787
|
+
const claudeProbe = engineOf(profile) !== "claude" ? void 0 : conf.probe ?? (conf.timeoutMs !== void 0 ? (env) => checkClaudeAuth(env, { timeoutMs: conf.timeoutMs }) : void 0);
|
|
1788
|
+
(claudeProbe ? claudeProbe(sessionEnvFor(profile)).then((status) => status === "logged_in" ? { available: true } : status === "logged_out" ? {
|
|
1789
|
+
available: false,
|
|
1790
|
+
reason: "no usable Claude credentials for this profile"
|
|
1791
|
+
} : { available: "unknown" }) : adapter.checkAvailability(profile, sessionEnvFor(profile))).then((verdict) => {
|
|
1792
|
+
availability.set(profile.name, {
|
|
1793
|
+
verdict,
|
|
1794
|
+
at: Date.now()
|
|
1795
|
+
});
|
|
1796
|
+
if (verdict.available === false && !availabilityWarned.has(profile.name)) {
|
|
1797
|
+
availabilityWarned.add(profile.name);
|
|
1798
|
+
console.warn(`[workerdeck] Profile '${profile.name}' is unavailable: ${verdict.reason} (\`checkCredentials: false\` disables this check)`);
|
|
1579
1799
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1800
|
+
if (verdict.available === true) availabilityWarned.delete(profile.name);
|
|
1801
|
+
}).catch(() => {});
|
|
1802
|
+
};
|
|
1803
|
+
/** Launch-time sweep, concurrent and fire-and-forget. */
|
|
1804
|
+
const preflightCredentials = () => {
|
|
1805
|
+
for (const profile of allProfiles()) probeProfile(profile);
|
|
1806
|
+
};
|
|
1807
|
+
/** Lazy re-probe on reads, so an operator who just ran `codex login` (or
|
|
1808
|
+
* exported a key) sees the profile go green without a restart. Serves the
|
|
1809
|
+
* cached verdict now; the refreshed one lands on the next request. */
|
|
1810
|
+
const refreshAvailability = (profiles) => {
|
|
1811
|
+
if (!options.checkCredentials) return;
|
|
1812
|
+
const now = Date.now();
|
|
1813
|
+
for (const profile of profiles) {
|
|
1814
|
+
const cached = availability.get(profile.name);
|
|
1815
|
+
if (!cached || now - cached.at > AVAILABILITY_TTL_MS) probeProfile(profile);
|
|
1584
1816
|
}
|
|
1585
1817
|
};
|
|
1586
1818
|
const authenticate = async (req) => {
|
|
@@ -1609,6 +1841,16 @@ function createWorkerServer(options = {}) {
|
|
|
1609
1841
|
id: decodeURIComponent(parts[0]),
|
|
1610
1842
|
permissionId: decodeURIComponent(parts[2])
|
|
1611
1843
|
};
|
|
1844
|
+
if (parts.length <= 3 && parts[1] === "attachments") return {
|
|
1845
|
+
id: decodeURIComponent(parts[0]),
|
|
1846
|
+
attachments: true,
|
|
1847
|
+
attachmentId: parts[2] === void 0 ? void 0 : decodeURIComponent(parts[2])
|
|
1848
|
+
};
|
|
1849
|
+
if (parts.length <= 3 && parts[1] === "mcp") return {
|
|
1850
|
+
id: decodeURIComponent(parts[0]),
|
|
1851
|
+
mcp: true,
|
|
1852
|
+
mcpServer: parts[2] === void 0 ? void 0 : decodeURIComponent(parts[2])
|
|
1853
|
+
};
|
|
1612
1854
|
if (parts.length >= 2 && parts[1] === "files") {
|
|
1613
1855
|
const filePath = parts.slice(2).map(decodeURIComponent).join("/");
|
|
1614
1856
|
return {
|
|
@@ -1625,6 +1867,106 @@ function createWorkerServer(options = {}) {
|
|
|
1625
1867
|
const maxHostFileBytes = options.hostFiles?.maxFileBytes ?? 1024 * 1024;
|
|
1626
1868
|
const maxHostDirEntries = options.hostFiles?.maxEntries ?? 5e3;
|
|
1627
1869
|
/**
|
|
1870
|
+
* `{basePath}/sessions/:id/attachments` — the files a client sends with a message.
|
|
1871
|
+
*
|
|
1872
|
+
* `POST ?name=<name>` takes the raw bytes as the body and the media type from
|
|
1873
|
+
* the `content-type` header; there is no multipart parsing here on purpose, so
|
|
1874
|
+
* a phone and a browser both upload with one plain request and this file stays
|
|
1875
|
+
* dependency-free. `GET /:attachmentId` hands the bytes back for thumbnails.
|
|
1876
|
+
*
|
|
1877
|
+
* The download always answers `content-disposition: attachment` and `nosniff`,
|
|
1878
|
+
* the same as `/files`: an upload is client-supplied content served from the
|
|
1879
|
+
* gateway's own origin, and it must never render as a document there. (An
|
|
1880
|
+
* `<img src>` is unaffected — disposition does not apply to subresources.)
|
|
1881
|
+
*/
|
|
1882
|
+
const handleAttachments = async (req, res, sessionId, session, attachmentId) => {
|
|
1883
|
+
if (req.method === "POST" && attachmentId === void 0) {
|
|
1884
|
+
const url = new URL(req.url ?? "/", "http://internal");
|
|
1885
|
+
const mediaType = req.headers["content-type"];
|
|
1886
|
+
if (!mediaType) {
|
|
1887
|
+
json(res, 400, { error: "content-type header is required" });
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
const accepted = (session.capabilities ?? ENGINE_CAPABILITIES[session.engine ?? "claude"]).attachments;
|
|
1891
|
+
const kind = attachmentKind(mediaType);
|
|
1892
|
+
if (kind && !accepted.includes(kind === "document" ? "pdf" : kind)) {
|
|
1893
|
+
json(res, 415, { error: `the ${session.engine ?? "claude"} engine does not accept ${kind} attachments` });
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
let body;
|
|
1897
|
+
try {
|
|
1898
|
+
body = await readRawBody(req, attachmentStore.maxFileBytes);
|
|
1899
|
+
} catch {
|
|
1900
|
+
json(res, 413, { error: "attachment is larger than the limit" });
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
const result = attachmentStore.put(sessionId, url.searchParams.get("name") ?? "attachment", mediaType, body);
|
|
1904
|
+
if (!result.ok) {
|
|
1905
|
+
json(res, result.error.code === "unsupported_type" ? 415 : result.error.code === "empty" ? 400 : 413, { error: result.error.message });
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
json(res, 201, { attachment: result.attachment });
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (req.method === "GET" && attachmentId !== void 0) {
|
|
1912
|
+
const found = attachmentStore.get(sessionId, attachmentId);
|
|
1913
|
+
if (!found) {
|
|
1914
|
+
json(res, 404, { error: "attachment not found" });
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
const bytes = Buffer.from(found.data, "base64");
|
|
1918
|
+
res.writeHead(200, {
|
|
1919
|
+
"content-type": found.mediaType,
|
|
1920
|
+
"content-length": bytes.length,
|
|
1921
|
+
"content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(found.name)}`,
|
|
1922
|
+
"x-content-type-options": "nosniff"
|
|
1923
|
+
});
|
|
1924
|
+
res.end(bytes);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
json(res, 405, { error: "method not allowed" });
|
|
1928
|
+
};
|
|
1929
|
+
/**
|
|
1930
|
+
* `{basePath}/sessions/:id/mcp` — the session's MCP servers, and the three
|
|
1931
|
+
* things the CLI's own `/mcp` screen can do to one (reconnect, enable, disable).
|
|
1932
|
+
*
|
|
1933
|
+
* Every answer goes through `mcpStatusInfo`, which is where the servers' `env`
|
|
1934
|
+
* and `headers` are dropped: reading this route must not be a way to read the
|
|
1935
|
+
* operator's API tokens.
|
|
1936
|
+
*/
|
|
1937
|
+
const handleMcp = async (req, res, runner, serverName) => {
|
|
1938
|
+
const listServers = async () => {
|
|
1939
|
+
const servers = await runner.mcpServers?.();
|
|
1940
|
+
if (!servers) {
|
|
1941
|
+
json(res, 501, { error: "this session does not report MCP servers" });
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
json(res, 200, { servers });
|
|
1945
|
+
return true;
|
|
1946
|
+
};
|
|
1947
|
+
if (req.method === "GET" && serverName === void 0) {
|
|
1948
|
+
await listServers();
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (req.method === "POST" && serverName !== void 0) {
|
|
1952
|
+
const body = await readJsonBody(req, maxBodyBytes);
|
|
1953
|
+
if (body?.action !== "reconnect" && body?.action !== "enable" && body?.action !== "disable") {
|
|
1954
|
+
json(res, 400, { error: "action must be 'reconnect', 'enable' or 'disable'" });
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
try {
|
|
1958
|
+
if (body.action === "reconnect") await runner.reconnectMcpServer?.(serverName);
|
|
1959
|
+
else await runner.setMcpServerEnabled?.(serverName, body.action === "enable");
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
json(res, 400, { error: error instanceof Error ? error.message : "MCP action failed" });
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
await listServers();
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
json(res, 405, { error: "method not allowed" });
|
|
1968
|
+
};
|
|
1969
|
+
/**
|
|
1628
1970
|
* `{basePath}/fs/*` — the operator's real tree. Authorized by the auth key alone
|
|
1629
1971
|
* and deliberately outside the agent permission flow: the caller is the operator.
|
|
1630
1972
|
*
|
|
@@ -1847,8 +2189,19 @@ function createWorkerServer(options = {}) {
|
|
|
1847
2189
|
}
|
|
1848
2190
|
json(res, 404, { error: "not found" });
|
|
1849
2191
|
};
|
|
1850
|
-
|
|
1851
|
-
|
|
2192
|
+
/**
|
|
2193
|
+
* `GET /sdk-sessions`, engine-aware: `?profile=` names whose on-disk store to
|
|
2194
|
+
* list, and the profile's engine adapter answers (for codex, over a
|
|
2195
|
+
* short-lived `thread/list` child — no live session involved). Absent
|
|
2196
|
+
* `profile`, the choice is implicit when the server declares exactly one
|
|
2197
|
+
* profile (the resolveProfile rule); with several, the Claude engine's
|
|
2198
|
+
* global store is listed — the pre-engine-aware behavior every existing
|
|
2199
|
+
* caller already gets, kept because old clients cannot answer a new 400.
|
|
2200
|
+
* The injectable `listSdkSessions` option predates the adapter layer and is
|
|
2201
|
+
* honored for the claude engine only (existing tests and hosts wire it),
|
|
2202
|
+
* exactly like the injectable claude auth probe.
|
|
2203
|
+
*/
|
|
2204
|
+
const handleSdkSessions = async (req, res, auth) => {
|
|
1852
2205
|
if (req.method !== "GET") {
|
|
1853
2206
|
json(res, 405, { error: "method not allowed" });
|
|
1854
2207
|
return;
|
|
@@ -1856,21 +2209,58 @@ function createWorkerServer(options = {}) {
|
|
|
1856
2209
|
const url = new URL(req.url ?? "/", "http://internal");
|
|
1857
2210
|
const dir = url.searchParams.get("dir") ?? void 0;
|
|
1858
2211
|
const roots = options.allowedCwdRoots;
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2212
|
+
const limit = Number(url.searchParams.get("limit") ?? "") || void 0;
|
|
2213
|
+
const offset = Number(url.searchParams.get("offset") ?? "") || void 0;
|
|
2214
|
+
const requested = url.searchParams.get("profile") ?? void 0;
|
|
2215
|
+
let profile;
|
|
2216
|
+
if (requested !== void 0) {
|
|
2217
|
+
const resolved = resolveProfile(requested, auth.allowedProfiles);
|
|
2218
|
+
if (!resolved.ok) {
|
|
2219
|
+
json(res, resolved.status, { error: resolved.error });
|
|
1862
2220
|
return;
|
|
1863
2221
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
2222
|
+
profile = resolved.profile;
|
|
2223
|
+
} else {
|
|
2224
|
+
const all = allProfiles();
|
|
2225
|
+
if (all.length === 1 && (!auth.allowedProfiles || auth.allowedProfiles.includes(all[0].name))) profile = all[0];
|
|
2226
|
+
}
|
|
2227
|
+
const adapter = adapterFor(profile?.engine);
|
|
2228
|
+
if (!adapter.capabilities.listSessions) {
|
|
2229
|
+
json(res, 400, { error: `profile '${profile?.name ?? "default"}' runs the ${engineOf(profile)} engine, which has no browsable session store` });
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const lister = engineOf(profile) === "claude" && options.listSdkSessions ? options.listSdkSessions : (params) => {
|
|
2233
|
+
if (!adapter.listSessions) throw new Error(`the ${engineOf(profile)} engine does not implement session listing`);
|
|
2234
|
+
return adapter.listSessions({
|
|
2235
|
+
...params,
|
|
2236
|
+
profile,
|
|
2237
|
+
env: profile ? sessionEnvFor(profile) : process.env
|
|
2238
|
+
});
|
|
2239
|
+
};
|
|
2240
|
+
try {
|
|
2241
|
+
if (roots && roots.length > 0) if (dir) {
|
|
2242
|
+
if (!cwdAllowed(dir, roots)) {
|
|
2243
|
+
json(res, 403, { error: "dir is outside the allowed roots" });
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
} else {
|
|
2247
|
+
json(res, 200, { sdkSessions: withinRoots(await lister({}), roots, limit, offset) });
|
|
1866
2248
|
return;
|
|
1867
2249
|
}
|
|
2250
|
+
json(res, 200, { sdkSessions: await lister({
|
|
2251
|
+
dir,
|
|
2252
|
+
limit,
|
|
2253
|
+
offset
|
|
2254
|
+
}) });
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
json(res, 500, { error: error instanceof Error ? error.message : "failed to list sessions" });
|
|
1868
2257
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2258
|
+
};
|
|
2259
|
+
/** The sessions whose `cwd` is inside the roots, newest first, then paged. A
|
|
2260
|
+
* summary with no `cwd` cannot be shown to be inside them, so it is dropped. */
|
|
2261
|
+
const withinRoots = (sessions, roots, limit, offset = 0) => {
|
|
2262
|
+
const allowed = sessions.filter((s) => s.cwd !== void 0 && cwdAllowed(s.cwd, roots)).sort((a, b) => b.lastModified - a.lastModified);
|
|
2263
|
+
return limit === void 0 ? allowed.slice(offset) : allowed.slice(offset, offset + limit);
|
|
1874
2264
|
};
|
|
1875
2265
|
const handleJobs = async (req, res, pathname, auth) => {
|
|
1876
2266
|
if (!queue) {
|
|
@@ -1924,6 +2314,7 @@ function createWorkerServer(options = {}) {
|
|
|
1924
2314
|
json(res, 400, { error: badRequest });
|
|
1925
2315
|
return;
|
|
1926
2316
|
}
|
|
2317
|
+
stripInertFields(body.session, resolved.profile);
|
|
1927
2318
|
body.session.profile = resolved.profile?.name;
|
|
1928
2319
|
try {
|
|
1929
2320
|
json(res, 201, { job: await queue.submit(body) });
|
|
@@ -2041,8 +2432,10 @@ function createWorkerServer(options = {}) {
|
|
|
2041
2432
|
const rest = pathname.slice((basePath + "/profiles").length).replace(/^\//, "");
|
|
2042
2433
|
if (rest === "") {
|
|
2043
2434
|
if (req.method === "GET") {
|
|
2435
|
+
const visible = auth.allowedProfiles ? allProfiles().filter((p) => auth.allowedProfiles.includes(p.name)) : allProfiles();
|
|
2436
|
+
refreshAvailability(visible);
|
|
2044
2437
|
json(res, 200, {
|
|
2045
|
-
profiles:
|
|
2438
|
+
profiles: visible.map(forResponse),
|
|
2046
2439
|
canManage: manageGuard(auth) === null
|
|
2047
2440
|
});
|
|
2048
2441
|
return;
|
|
@@ -2118,11 +2511,12 @@ function createWorkerServer(options = {}) {
|
|
|
2118
2511
|
return;
|
|
2119
2512
|
}
|
|
2120
2513
|
if (pathname === basePath + "/sdk-sessions") {
|
|
2121
|
-
|
|
2514
|
+
const auth = await authenticate(req);
|
|
2515
|
+
if (!auth.ok) {
|
|
2122
2516
|
json(res, 401, { error: "unauthorized" });
|
|
2123
2517
|
return;
|
|
2124
2518
|
}
|
|
2125
|
-
await handleSdkSessions(req, res);
|
|
2519
|
+
await handleSdkSessions(req, res, auth);
|
|
2126
2520
|
return;
|
|
2127
2521
|
}
|
|
2128
2522
|
if (pathname.startsWith(basePath + "/fs/")) {
|
|
@@ -2173,6 +2567,7 @@ function createWorkerServer(options = {}) {
|
|
|
2173
2567
|
json(res, 400, { error: badRequest });
|
|
2174
2568
|
return;
|
|
2175
2569
|
}
|
|
2570
|
+
stripInertFields(body, resolved.profile);
|
|
2176
2571
|
body.profile = resolved.profile?.name;
|
|
2177
2572
|
const runner = await createRunner(buildRunnerConfig(body));
|
|
2178
2573
|
watchAuthSource(runner);
|
|
@@ -2188,6 +2583,18 @@ function createWorkerServer(options = {}) {
|
|
|
2188
2583
|
json(res, 404, { error: "session not found" });
|
|
2189
2584
|
return;
|
|
2190
2585
|
}
|
|
2586
|
+
if (route.attachments) {
|
|
2587
|
+
await handleAttachments(req, res, route.id, runner?.info() ?? parked.info, route.attachmentId);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
if (route.mcp) {
|
|
2591
|
+
if (!runner) {
|
|
2592
|
+
json(res, 409, { error: "session is parked (wake it before asking about MCP)" });
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
await handleMcp(req, res, runner, route.mcpServer);
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2191
2598
|
if (route.files) {
|
|
2192
2599
|
if (req.method !== "GET") {
|
|
2193
2600
|
json(res, 405, { error: "method not allowed" });
|
|
@@ -2253,6 +2660,7 @@ function createWorkerServer(options = {}) {
|
|
|
2253
2660
|
registry.remove(route.id);
|
|
2254
2661
|
bridge.remove(route.id);
|
|
2255
2662
|
await parking.discard(route.id);
|
|
2663
|
+
attachmentStore.drop(route.id);
|
|
2256
2664
|
json(res, 200, { session: runner?.info() ?? {
|
|
2257
2665
|
...parked.info,
|
|
2258
2666
|
status: "closed"
|
|
@@ -2356,9 +2764,16 @@ function createWorkerServer(options = {}) {
|
|
|
2356
2764
|
};
|
|
2357
2765
|
const handleCommand = async (frame, runner) => {
|
|
2358
2766
|
switch (frame.type) {
|
|
2359
|
-
case "user_message":
|
|
2360
|
-
|
|
2767
|
+
case "user_message": {
|
|
2768
|
+
if (!frame.attachmentIds?.length) {
|
|
2769
|
+
runner.sendMessage(frame.text);
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
const resolved = attachmentStore.resolve(runner.id, frame.attachmentIds);
|
|
2773
|
+
if (!resolved.ok) throw new Error(`unknown attachment(s): ${resolved.missing.join(", ")}`);
|
|
2774
|
+
runner.sendMessage(frame.text, resolved.attachments);
|
|
2361
2775
|
return;
|
|
2776
|
+
}
|
|
2362
2777
|
case "permission_decision":
|
|
2363
2778
|
if (frame.behavior === "allow") runner.resolvePermission(frame.requestId, {
|
|
2364
2779
|
behavior: "allow",
|
|
@@ -2478,6 +2893,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
|
|
|
2478
2893
|
};
|
|
2479
2894
|
}
|
|
2480
2895
|
//#endregion
|
|
2481
|
-
export { BridgeHub, MemorySessionStore, SessionNotifier, SessionParkManager, SessionRegistry, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createWorkerServer, toDurableRecord };
|
|
2896
|
+
export { AttachmentStore, BridgeHub, MemorySessionStore, SessionNotifier, SessionParkManager, SessionRegistry, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createWorkerServer, toDurableRecord };
|
|
2482
2897
|
|
|
2483
2898
|
//# sourceMappingURL=index.mjs.map
|