openmausbot 0.1.63 → 0.1.65
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/assets/index-BOrNJcfG.js +273 -0
- package/dist/assets/{index-C-SoIGdn.js → index-BZeH50da.js} +1 -1
- package/dist/assets/index-Dwzk199I.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +3 -1
- package/dist-server/index.js +1044 -427
- package/dist-server/openmausbot.js +404 -168
- package/dist-server/pair-cli.js +404 -168
- package/dist-server/prepare-cloudflared.js +10 -2
- package/dist-server/server/attachments.js +15 -3
- package/dist-server/server/config.js +5 -1
- package/dist-server/server/custom-domain.js +182 -0
- package/dist-server/server/drivers/codex-device-auth.js +234 -0
- package/dist-server/server/drivers/codex.js +10 -0
- package/dist-server/server/harness/registry.js +7 -0
- package/dist-server/server/index.js +103 -21
- package/dist-server/server/provider-auth-sessions.js +111 -0
- package/dist-server/server/sse-fanout.js +53 -0
- package/dist-server/src/lib/composer-attachments.js +20 -3
- package/dist-server/vps-container-mcp.js +3 -1
- package/package.json +1 -1
- package/dist/assets/index-BuntoOSX.js +0 -264
- package/dist/assets/index-oPMv6a_k.css +0 -1
|
@@ -117,9 +117,12 @@ import { createGracefulShutdown } from "./graceful-shutdown.js";
|
|
|
117
117
|
import { acquireDataDirLeaseForProcess } from "./data-dir-lease.js";
|
|
118
118
|
import { describeEdition, editionStatus, loadEnterpriseLayer } from "./enterprise.js";
|
|
119
119
|
import { environmentDescriptor, loadEnvironmentId } from "./environment.js";
|
|
120
|
+
import { createCustomDomainVerifier, normalizeCustomDomain } from "./custom-domain.js";
|
|
121
|
+
import { ProviderAuthSessions } from "./provider-auth-sessions.js";
|
|
120
122
|
import { clearSessionCookie, clientBotPatchViolation, clientGroupPatchViolation, labelFromUserAgent, requestOrigin, requestSource, resolveRequestAuth, parseCookies, serializeSessionCookie, sessionCookieName, } from "./request-auth.js";
|
|
121
123
|
import { cookieMaxAgeSeconds, formatPairingCode, SessionRegistry } from "./sessions.js";
|
|
122
124
|
import { describeBrand, loadBrand } from "./brand.js";
|
|
125
|
+
import { deliverSseFrame } from "./sse-fanout.js";
|
|
123
126
|
import { PHONE_SECRET_PROTOCOL_VERSION, PhoneSecretBridge, PhoneSecretError, PhoneSecretSubmissionRegistry, assertPhoneSecretRequestMatches, phoneSecretOperationId, } from "./phone-secret.js";
|
|
124
127
|
const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
|
|
125
128
|
const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
|
|
@@ -169,9 +172,29 @@ const DESKTOP_MANAGED = process.env.OMB_DESKTOP_PARENT === "1";
|
|
|
169
172
|
let desktopMutationToken = DESKTOP_MANAGED ? "" : undefined;
|
|
170
173
|
let companionMutationToken = DESKTOP_MANAGED ? "" : undefined;
|
|
171
174
|
// Where remote clients reach this server (a proxy's public address); pairing URLs use it.
|
|
172
|
-
const
|
|
175
|
+
const FALLBACK_PUBLIC_URL = process.env.OMB_PUBLIC_URL?.trim().replace(/\/+$/, "") || null;
|
|
173
176
|
const cfg = loadConfig();
|
|
177
|
+
const customDomainVerifier = createCustomDomainVerifier({ environmentId: ENVIRONMENT_ID });
|
|
178
|
+
let customDomainRevision = 0;
|
|
179
|
+
function savedCustomDomain() {
|
|
180
|
+
if (DESKTOP_MANAGED || !cfg.customDomain)
|
|
181
|
+
return null;
|
|
182
|
+
try {
|
|
183
|
+
return normalizeCustomDomain(cfg.customDomain);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function publicUrl() { return savedCustomDomain() ?? FALLBACK_PUBLIC_URL; }
|
|
190
|
+
function customDomainStatus() {
|
|
191
|
+
return {
|
|
192
|
+
customDomain: savedCustomDomain(), publicUrl: publicUrl(), fallbackUrl: FALLBACK_PUBLIC_URL,
|
|
193
|
+
supported: !DESKTOP_MANAGED, appPort: PORT, webhookPort: WEBHOOK_PORT,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
174
196
|
const registry = new ProviderRegistry(BUILT_IN_DRIVERS);
|
|
197
|
+
const providerAuthSessions = new ProviderAuthSessions();
|
|
175
198
|
await registry.load(instanceConfigs(cfg));
|
|
176
199
|
const bundledSkills = loadBundledSkills();
|
|
177
200
|
const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
|
|
@@ -521,6 +544,7 @@ function browserIntegration(botId, profile) {
|
|
|
521
544
|
session,
|
|
522
545
|
encryptionKey: browserEngineEncryptionKey(),
|
|
523
546
|
persistent: profile !== "guest",
|
|
547
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
524
548
|
}),
|
|
525
549
|
};
|
|
526
550
|
}
|
|
@@ -1658,6 +1682,7 @@ function messageWindow(threadId, messageId, limit) {
|
|
|
1658
1682
|
}
|
|
1659
1683
|
const sseClients = new Set();
|
|
1660
1684
|
sessions.onSessionRevoked((sessionId) => {
|
|
1685
|
+
providerAuthSessions.revokeOwner(sessionId);
|
|
1661
1686
|
for (const client of sseClients) {
|
|
1662
1687
|
if (client.sessionId !== sessionId)
|
|
1663
1688
|
continue;
|
|
@@ -1720,10 +1745,9 @@ function broadcast(payload) {
|
|
|
1720
1745
|
for (const client of [...sseClients]) {
|
|
1721
1746
|
if (!wants(client, kind))
|
|
1722
1747
|
continue;
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
catch {
|
|
1748
|
+
// Screen frames are replaceable and durable events are not: see
|
|
1749
|
+
// ./sse-fanout.ts for the backpressure/bound decision this makes.
|
|
1750
|
+
if (deliverSseFrame(client, kind, client.admin ? frame : clientFrame) === "disconnected") {
|
|
1727
1751
|
sseClients.delete(client);
|
|
1728
1752
|
}
|
|
1729
1753
|
}
|
|
@@ -6573,6 +6597,7 @@ function persistMcpServers(next) {
|
|
|
6573
6597
|
/** Rebuild the provider fleet after a config change so new keys take
|
|
6574
6598
|
* effect without a server restart (kills any in-flight turns). */
|
|
6575
6599
|
async function reloadProviders() {
|
|
6600
|
+
providerAuthSessions.clear();
|
|
6576
6601
|
// Every provider process is about to die. Revoke all turn capabilities in
|
|
6577
6602
|
// one synchronous step before the first teardown await, including room/task
|
|
6578
6603
|
// threads that are not a bot's default DM.
|
|
@@ -6716,6 +6741,12 @@ const handleRequest = async (req, res) => {
|
|
|
6716
6741
|
if (method === "GET" && path === "/.well-known/openmausbot/environment") {
|
|
6717
6742
|
return json(res, 200, environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED }));
|
|
6718
6743
|
}
|
|
6744
|
+
const domainCheck = /^\/\.well-known\/openmausbot\/domain-check\/([a-f0-9]{64})$/.exec(path);
|
|
6745
|
+
if (method === "GET" && domainCheck) {
|
|
6746
|
+
res.setHeader("cache-control", "no-store");
|
|
6747
|
+
const challenge = customDomainVerifier.challenge(domainCheck[1]);
|
|
6748
|
+
return json(res, challenge ? 200 : 404, challenge ?? { error: "No active domain check." });
|
|
6749
|
+
}
|
|
6719
6750
|
if (method === "POST" && path === "/api/auth/pair") {
|
|
6720
6751
|
// JSON only: a cross-site HTML form cannot send this content type
|
|
6721
6752
|
// without a preflight, so a stray unused code cannot be planted as a
|
|
@@ -6803,7 +6834,7 @@ const handleRequest = async (req, res) => {
|
|
|
6803
6834
|
const scopes = Array.isArray(requested) ? requested.filter((v) => v === "admin" || v === "client") : undefined;
|
|
6804
6835
|
const opened = sessions.openPairing({ label: typeof body?.label === "string" ? body.label : undefined, scopes });
|
|
6805
6836
|
const origin = requestOrigin(req);
|
|
6806
|
-
const base =
|
|
6837
|
+
const base = publicUrl() ?? (auth.kind === "session" && origin ? origin : null);
|
|
6807
6838
|
const code = formatPairingCode(opened.code);
|
|
6808
6839
|
return json(res, 200, {
|
|
6809
6840
|
id: opened.id,
|
|
@@ -6816,7 +6847,42 @@ const handleRequest = async (req, res) => {
|
|
|
6816
6847
|
});
|
|
6817
6848
|
}
|
|
6818
6849
|
if (method === "GET" && path === "/api/auth/pairing")
|
|
6819
|
-
return json(res, 200, { pairings: sessions.openPairings(), publicUrl:
|
|
6850
|
+
return json(res, 200, { pairings: sessions.openPairings(), publicUrl: publicUrl() });
|
|
6851
|
+
// Admin-only via request-auth's default deny. Connecting a domain only
|
|
6852
|
+
// changes future pairing links; DNS, proxy setup, webhooks and all existing
|
|
6853
|
+
// sessions remain untouched. The tunnel/deployment URL is kept as fallback.
|
|
6854
|
+
if (path === "/api/settings/custom-domain") {
|
|
6855
|
+
res.setHeader("cache-control", "no-store");
|
|
6856
|
+
if (method === "GET")
|
|
6857
|
+
return json(res, 200, customDomainStatus());
|
|
6858
|
+
if (method === "POST" || method === "DELETE") {
|
|
6859
|
+
if (DESKTOP_MANAGED)
|
|
6860
|
+
return json(res, 409, { error: "Custom domains are configured on a self-hosted OpenMausBot server, not the desktop companion." });
|
|
6861
|
+
if (!/^application\/json\b/i.test(String(req.headers["content-type"] ?? ""))) {
|
|
6862
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
6863
|
+
}
|
|
6864
|
+
if (method === "DELETE") {
|
|
6865
|
+
saveConfig({ customDomain: "" });
|
|
6866
|
+
cfg.customDomain = "";
|
|
6867
|
+
customDomainRevision++;
|
|
6868
|
+
return json(res, 200, customDomainStatus());
|
|
6869
|
+
}
|
|
6870
|
+
const body = await readBody(req, 4096);
|
|
6871
|
+
if (typeof body?.domain !== "string")
|
|
6872
|
+
return json(res, 400, { error: "Enter your domain name." });
|
|
6873
|
+
const revision = customDomainRevision;
|
|
6874
|
+
const verified = await customDomainVerifier.verify(body.domain);
|
|
6875
|
+
if (auth.kind === "session" && !sessions.isLive(auth.session.id)) {
|
|
6876
|
+
return json(res, 401, { error: "Your session ended. Sign in again before connecting a domain." });
|
|
6877
|
+
}
|
|
6878
|
+
if (revision !== customDomainRevision)
|
|
6879
|
+
return json(res, 409, { error: "Domain settings changed during verification. Try again." });
|
|
6880
|
+
saveConfig({ customDomain: verified.origin });
|
|
6881
|
+
cfg.customDomain = verified.origin;
|
|
6882
|
+
customDomainRevision++;
|
|
6883
|
+
return json(res, 200, customDomainStatus());
|
|
6884
|
+
}
|
|
6885
|
+
}
|
|
6820
6886
|
m = path.match(/^\/api\/auth\/pairing\/([\w-]+)$/);
|
|
6821
6887
|
if (m && method === "DELETE") {
|
|
6822
6888
|
const cancelled = sessions.cancelPairing(m[1]);
|
|
@@ -8019,7 +8085,12 @@ const handleRequest = async (req, res) => {
|
|
|
8019
8085
|
}
|
|
8020
8086
|
// ── events stream ──
|
|
8021
8087
|
if (method === "GET" && path === "/api/events") {
|
|
8022
|
-
const client = {
|
|
8088
|
+
const client = {
|
|
8089
|
+
res,
|
|
8090
|
+
admin: auth.scopes.includes("admin"),
|
|
8091
|
+
screens: url.searchParams.get("screens") !== "off",
|
|
8092
|
+
backpressured: false,
|
|
8093
|
+
};
|
|
8023
8094
|
if (auth.kind === "session")
|
|
8024
8095
|
client.sessionId = auth.session.id;
|
|
8025
8096
|
res.writeHead(200, {
|
|
@@ -11170,6 +11241,12 @@ const handleRequest = async (req, res) => {
|
|
|
11170
11241
|
resetPathCache();
|
|
11171
11242
|
return json(res, 200, { instances: await registry.describe() });
|
|
11172
11243
|
}
|
|
11244
|
+
const authStatus = /^\/api\/instances\/([\w.-]+)\/auth\/status$/.exec(path);
|
|
11245
|
+
if (method === "GET" && authStatus) {
|
|
11246
|
+
res.setHeader("cache-control", "no-store");
|
|
11247
|
+
const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
|
|
11248
|
+
return json(res, 200, { auth: state });
|
|
11249
|
+
}
|
|
11173
11250
|
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel)$/.exec(path);
|
|
11174
11251
|
if (method === "POST" && instanceAction) {
|
|
11175
11252
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
@@ -11177,6 +11254,9 @@ const handleRequest = async (req, res) => {
|
|
|
11177
11254
|
}
|
|
11178
11255
|
const instanceId = instanceAction[1];
|
|
11179
11256
|
const action = instanceAction[2];
|
|
11257
|
+
const owner = auth.kind === "session" ? auth.session.id : "loopback";
|
|
11258
|
+
if (action.startsWith("auth/"))
|
|
11259
|
+
res.setHeader("cache-control", "no-store");
|
|
11180
11260
|
try {
|
|
11181
11261
|
if (action === "refresh-models") {
|
|
11182
11262
|
if (!(await registry.refreshModels(instanceId)))
|
|
@@ -11189,10 +11269,16 @@ const handleRequest = async (req, res) => {
|
|
|
11189
11269
|
return json(res, 200, { instances: await registry.describe() });
|
|
11190
11270
|
}
|
|
11191
11271
|
if (action === "auth/start") {
|
|
11192
|
-
const
|
|
11193
|
-
if (!
|
|
11194
|
-
return json(res, 404, { error: "
|
|
11195
|
-
|
|
11272
|
+
const instance = registry.get(instanceId);
|
|
11273
|
+
if (!instance)
|
|
11274
|
+
return json(res, 404, { error: "unknown instance" });
|
|
11275
|
+
const started = await providerAuthSessions.start(instance, owner);
|
|
11276
|
+
// Revocation can arrive while the CLI is obtaining a device code.
|
|
11277
|
+
if (auth.kind === "session" && !sessions.isLive(auth.session.id)) {
|
|
11278
|
+
providerAuthSessions.revokeOwner(owner);
|
|
11279
|
+
return json(res, 401, { error: "Your session ended. Start a new sign-in." });
|
|
11280
|
+
}
|
|
11281
|
+
return json(res, 200, { auth: started });
|
|
11196
11282
|
}
|
|
11197
11283
|
if (action === "auth/complete") {
|
|
11198
11284
|
const body = await readBody(req);
|
|
@@ -11200,20 +11286,16 @@ const handleRequest = async (req, res) => {
|
|
|
11200
11286
|
const callbackUrl = typeof body?.callbackUrl === "string" ? body.callbackUrl : "";
|
|
11201
11287
|
if (!flowId || !callbackUrl)
|
|
11202
11288
|
return json(res, 400, { error: "flowId and callbackUrl are required" });
|
|
11203
|
-
|
|
11204
|
-
return json(res, 404, { error: "account setup is unavailable" });
|
|
11205
|
-
}
|
|
11289
|
+
await providerAuthSessions.complete(instanceId, owner, flowId, callbackUrl);
|
|
11206
11290
|
return json(res, 200, { ok: true });
|
|
11207
11291
|
}
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
}
|
|
11292
|
+
const body = await readBody(req, 4096);
|
|
11293
|
+
await providerAuthSessions.cancel(instanceId, owner, typeof body?.flowId === "string" ? body.flowId : "");
|
|
11211
11294
|
return json(res, 200, { ok: true });
|
|
11212
11295
|
}
|
|
11213
11296
|
catch (error) {
|
|
11214
|
-
const
|
|
11215
|
-
|
|
11216
|
-
: 500;
|
|
11297
|
+
const requestedStatus = error && typeof error === "object" ? error.status : undefined;
|
|
11298
|
+
const status = typeof requestedStatus === "number" && [400, 401, 404, 409, 413, 415].includes(requestedStatus) ? requestedStatus : 500;
|
|
11217
11299
|
return json(res, status, { error: error instanceof Error ? error.message : String(error) });
|
|
11218
11300
|
}
|
|
11219
11301
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const failure = (message, status) => Object.assign(new Error(message), { status });
|
|
2
|
+
export class ProviderAuthSessions {
|
|
3
|
+
flows = new Map();
|
|
4
|
+
async start(instance, owner) {
|
|
5
|
+
if (!instance.startAuthentication)
|
|
6
|
+
throw failure("Account setup is unavailable for this provider.", 404);
|
|
7
|
+
const existing = this.flows.get(instance.instanceId);
|
|
8
|
+
if (existing && (existing.busy || (existing.owner !== owner && existing.expiresAt > Date.now()))) {
|
|
9
|
+
throw failure("A sign-in is already in progress. Finish or cancel it in the browser that started it, or wait for it to expire.", 409);
|
|
10
|
+
}
|
|
11
|
+
// Reopening Settings may resume the same owner's flow. Keep its ownership
|
|
12
|
+
// if the driver's retry fails, rather than orphaning a still-running CLI.
|
|
13
|
+
const resuming = existing && existing.owner === owner && existing.expiresAt > Date.now() && !existing.revoked;
|
|
14
|
+
const flow = resuming ? existing : { instance, owner, flowId: null, expiresAt: Date.now() + 15 * 60_000, busy: false, starting: false, revoked: false };
|
|
15
|
+
flow.busy = true;
|
|
16
|
+
flow.starting = true;
|
|
17
|
+
this.flows.set(instance.instanceId, flow);
|
|
18
|
+
try {
|
|
19
|
+
const result = await instance.startAuthentication();
|
|
20
|
+
if (flow.revoked || this.flows.get(instance.instanceId) !== flow) {
|
|
21
|
+
await instance.cancelAuthentication?.();
|
|
22
|
+
throw failure("Sign-in cancelled because the session or provider changed.", 409);
|
|
23
|
+
}
|
|
24
|
+
flow.flowId = result.flowId;
|
|
25
|
+
const expiry = Date.parse(result.expiresAt ?? "");
|
|
26
|
+
if (Number.isFinite(expiry))
|
|
27
|
+
flow.expiresAt = expiry;
|
|
28
|
+
if (result.phase === "succeeded")
|
|
29
|
+
this.flows.delete(instance.instanceId);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if ((!resuming || flow.revoked) && this.flows.get(instance.instanceId) === flow)
|
|
34
|
+
this.flows.delete(instance.instanceId);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
flow.busy = false;
|
|
39
|
+
flow.starting = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
owned(instanceId, owner, flowId) {
|
|
43
|
+
const flow = this.flows.get(instanceId);
|
|
44
|
+
if (!flowId || !flow || flow.revoked || flow.owner !== owner || flow.flowId !== flowId) {
|
|
45
|
+
throw failure("This sign-in is no longer available in this browser. Start a new sign-in.", 404);
|
|
46
|
+
}
|
|
47
|
+
if (flow.busy)
|
|
48
|
+
throw failure("The sign-in is still being updated. Try again shortly.", 409);
|
|
49
|
+
return flow;
|
|
50
|
+
}
|
|
51
|
+
async status(instanceId, owner, flowId) {
|
|
52
|
+
const flow = this.owned(instanceId, owner, flowId);
|
|
53
|
+
if (!flow.instance.getAuthentication)
|
|
54
|
+
throw failure("Sign-in status is unavailable for this provider.", 404);
|
|
55
|
+
const status = await flow.instance.getAuthentication(flowId);
|
|
56
|
+
if (flow.revoked)
|
|
57
|
+
throw failure("This sign-in session ended.", 404);
|
|
58
|
+
// Keep the terminal result available to its owner, but release the slot so
|
|
59
|
+
// another admin need not wait fifteen minutes after success or cancellation.
|
|
60
|
+
if (status.phase !== "waiting")
|
|
61
|
+
flow.expiresAt = 0;
|
|
62
|
+
return status;
|
|
63
|
+
}
|
|
64
|
+
async complete(instanceId, owner, flowId, callbackUrl) {
|
|
65
|
+
const flow = this.owned(instanceId, owner, flowId);
|
|
66
|
+
if (!flow.instance.completeAuthentication)
|
|
67
|
+
throw failure("This provider does not use a callback URL.", 400);
|
|
68
|
+
flow.busy = true;
|
|
69
|
+
try {
|
|
70
|
+
await flow.instance.completeAuthentication(flowId, callbackUrl);
|
|
71
|
+
if (this.flows.get(instanceId) === flow)
|
|
72
|
+
this.flows.delete(instanceId);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
flow.busy = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async cancel(instanceId, owner, flowId) {
|
|
79
|
+
const flow = this.owned(instanceId, owner, flowId);
|
|
80
|
+
flow.busy = true;
|
|
81
|
+
try {
|
|
82
|
+
await flow.instance.cancelAuthentication?.();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
flow.busy = false;
|
|
86
|
+
flow.expiresAt = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
revokeOwner(owner) {
|
|
90
|
+
for (const [id, flow] of this.flows) {
|
|
91
|
+
if (flow.owner !== owner)
|
|
92
|
+
continue;
|
|
93
|
+
flow.revoked = true;
|
|
94
|
+
// A pending start owns its eventual cancellation. Keep the reservation
|
|
95
|
+
// until it finishes so another browser cannot race into this auth home.
|
|
96
|
+
if (flow.starting)
|
|
97
|
+
continue;
|
|
98
|
+
flow.busy = true;
|
|
99
|
+
void Promise.resolve().then(() => flow.instance.cancelAuthentication?.()).catch(() => { }).finally(() => {
|
|
100
|
+
if (this.flows.get(id) === flow)
|
|
101
|
+
this.flows.delete(id);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
clear() {
|
|
106
|
+
for (const flow of this.flows.values())
|
|
107
|
+
flow.revoked = true;
|
|
108
|
+
// Called when disposing the provider fleet, which owns child teardown.
|
|
109
|
+
this.flows.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Per-client delivery decision for the SSE broadcast fan-out in index.ts.
|
|
2
|
+
// Pulled into its own file — not just its own function — because index.ts
|
|
3
|
+
// starts the real HTTP server as an import-time side effect (see the bottom
|
|
4
|
+
// of index.ts: `server.listen(...)` runs unconditionally on import), so it
|
|
5
|
+
// can never be imported from a plain unit test. This one function has no
|
|
6
|
+
// dependency on that module and can be driven directly with a fake response.
|
|
7
|
+
/** Past this many buffered bytes, the client isn't draining on any useful
|
|
8
|
+
* timeframe: cut it loose rather than let Node's per-connection write buffer
|
|
9
|
+
* grow without bound. Its own EventSource reconnects and replays via
|
|
10
|
+
* Last-Event-ID (server/index.ts's existing replayBuffer/cursorSeq). */
|
|
11
|
+
// ponytail: fixed byte bound, not adaptive. Retune from real writableLength
|
|
12
|
+
// telemetry (see the audit's validation step) if it trips too eager or late.
|
|
13
|
+
export const SSE_MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
14
|
+
/** One client's fate for one frame.
|
|
15
|
+
*
|
|
16
|
+
* Screen captures are replaceable: once a client is backpressured, skip them
|
|
17
|
+
* until it drains, since the next capture supersedes whatever was dropped.
|
|
18
|
+
* Durable events (messages, bot/group state, config, ...) are never
|
|
19
|
+
* silently dropped here — they're written regardless of the backpressure
|
|
20
|
+
* flag. The only way a durable event goes missing is the bound below, which
|
|
21
|
+
* disconnects the client outright so its next reconnect replays what it
|
|
22
|
+
* missed instead of silently losing it. */
|
|
23
|
+
export function deliverSseFrame(client, kind, frame) {
|
|
24
|
+
if (client.res.writableLength > SSE_MAX_BUFFERED_BYTES) {
|
|
25
|
+
try {
|
|
26
|
+
client.res.end();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* already gone */
|
|
30
|
+
}
|
|
31
|
+
return "disconnected";
|
|
32
|
+
}
|
|
33
|
+
if (client.backpressured && kind === "screen")
|
|
34
|
+
return "dropped";
|
|
35
|
+
try {
|
|
36
|
+
if (!client.res.write(frame) && !client.backpressured) {
|
|
37
|
+
// write() returns false as soon as buffered bytes pass the socket's
|
|
38
|
+
// 16 KB highWaterMark, which a single screen frame clears on its own —
|
|
39
|
+
// so this says "hasn't drained yet", not "is a slow client". Without
|
|
40
|
+
// the drain listener the flag latches on for the life of the
|
|
41
|
+
// connection and screen streaming never resumes. One listener per
|
|
42
|
+
// transition into backpressure, never one per frame.
|
|
43
|
+
client.backpressured = true;
|
|
44
|
+
client.res.once("drain", () => {
|
|
45
|
+
client.backpressured = false;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return "sent";
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return "disconnected";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -88,6 +88,20 @@ const DOCUMENT_MIMES = {
|
|
|
88
88
|
odp: "application/vnd.oasis.opendocument.presentation",
|
|
89
89
|
};
|
|
90
90
|
const ACCEPTED_DOCUMENT_MIMES = new Set(Object.values(DOCUMENT_MIMES));
|
|
91
|
+
const AUDIO_MIMES_BY_EXTENSION = {
|
|
92
|
+
opus: "audio/opus",
|
|
93
|
+
ogg: "audio/ogg",
|
|
94
|
+
mp3: "audio/mpeg",
|
|
95
|
+
m4a: "audio/mp4",
|
|
96
|
+
aac: "audio/aac",
|
|
97
|
+
wav: "audio/wav",
|
|
98
|
+
flac: "audio/flac",
|
|
99
|
+
webm: "audio/webm",
|
|
100
|
+
};
|
|
101
|
+
const ACCEPTED_AUDIO_MIMES = new Set([
|
|
102
|
+
...Object.values(AUDIO_MIMES_BY_EXTENSION),
|
|
103
|
+
"audio/x-m4a", "audio/x-wav", "audio/wave", "audio/x-flac",
|
|
104
|
+
]);
|
|
91
105
|
export function documentMime(file) {
|
|
92
106
|
const declared = file.type.split(";", 1)[0].trim().toLowerCase();
|
|
93
107
|
if (ACCEPTED_DOCUMENT_MIMES.has(declared))
|
|
@@ -257,11 +271,14 @@ export async function imageAttachmentFromFile(file, optimistic) {
|
|
|
257
271
|
...(optimistic?.previewUrl ? { previewUrl: optimistic.previewUrl } : {}),
|
|
258
272
|
};
|
|
259
273
|
}
|
|
260
|
-
/** Copy a supported document into the private attachment store. The prompt
|
|
274
|
+
/** Copy a supported document or audio file into the private attachment store. The prompt
|
|
261
275
|
* then carries the same durable path for the local agent and paired phones,
|
|
262
276
|
* instead of exposing an arbitrary Finder path to the companion route. */
|
|
263
277
|
export async function fileAttachmentFromFile(file) {
|
|
264
|
-
const
|
|
278
|
+
const declared = file.type.split(";", 1)[0].trim().toLowerCase();
|
|
279
|
+
const extension = file.name.split(".").at(-1)?.toLowerCase() ?? "";
|
|
280
|
+
const mime = documentMime(file) ??
|
|
281
|
+
(ACCEPTED_AUDIO_MIMES.has(declared) ? declared : AUDIO_MIMES_BY_EXTENSION[extension]);
|
|
265
282
|
if (!mime)
|
|
266
283
|
return null;
|
|
267
284
|
if (file.size > FILE_MAX_BYTES) {
|
|
@@ -666,7 +683,7 @@ export async function intakeFiles(_files, _opts) {
|
|
|
666
683
|
const rejectedNames = results.flatMap((result) => result.rejectedNames);
|
|
667
684
|
const uploadErrors = results.flatMap((result) => result.uploadError ? [result.uploadError] : []);
|
|
668
685
|
const pathless = rejectedNames.length
|
|
669
|
-
?
|
|
686
|
+
? `Unable to attach ${rejectedNames.join(", ")}. Choose a supported image, document, or audio file.`
|
|
670
687
|
: null;
|
|
671
688
|
const failed = uploadErrors.length ? uploadErrors.join("; ") : null;
|
|
672
689
|
return {
|
|
@@ -15086,6 +15086,8 @@ var defaultModelSelectionSchema = external_exports.object({
|
|
|
15086
15086
|
effort: external_exports.enum(EFFORT_LEVELS).optional()
|
|
15087
15087
|
});
|
|
15088
15088
|
var appConfigSchema = external_exports.object({
|
|
15089
|
+
/** Verified by the dedicated domain endpoint, never a generic config patch. */
|
|
15090
|
+
customDomain: external_exports.string().optional(),
|
|
15089
15091
|
defaultModelSelection: defaultModelSelectionSchema.optional(),
|
|
15090
15092
|
/** CLI-only launch preferences. Never enable remote access implicitly. */
|
|
15091
15093
|
cliStartup: external_exports.object({
|
|
@@ -15129,7 +15131,7 @@ var appConfigSchema = external_exports.object({
|
|
|
15129
15131
|
var storedAppConfigSchema = appConfigSchema.extend({
|
|
15130
15132
|
browserProfiles: storedBrowserProfilesSchema.optional()
|
|
15131
15133
|
});
|
|
15132
|
-
var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true });
|
|
15134
|
+
var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true });
|
|
15133
15135
|
var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.json());
|
|
15134
15136
|
var DATA_DIR = process.env.OMB_DATA_DIR ?? join2(homedir2(), ".openmausbot");
|
|
15135
15137
|
var LEGACY_DATA_DIR = join2(homedir2(), ".opengrokbot");
|