@sideboard-ai/core 0.1.273 → 0.1.275
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/{abletime-4ZSDI4YP.js → abletime-VCVPSLSU.js} +4 -4
- package/dist/{abletime-JNQAHLHH.js → abletime-VRATUMDO.js} +4 -4
- package/dist/{abletime-oauth-4RXZH2KU.js → abletime-oauth-4C4G2J3R.js} +3 -3
- package/dist/{abletime-oauth-DCEIINYT.js → abletime-oauth-4K7MICAD.js} +3 -3
- package/dist/{abletime-rest-KVBJFVFD.js → abletime-rest-6N63ZVQ3.js} +3 -3
- package/dist/{abletime-rest-VN6PZ6P2.js → abletime-rest-PIBCNHF3.js} +3 -3
- package/dist/{agents-LAFFXBBZ.js → agents-2FOWVJCN.js} +4 -4
- package/dist/{agents-ZCO37SKG.js → agents-JBVJRDH3.js} +4 -4
- package/dist/{app-settings-RAC4PSSP.js → app-settings-EQQZQNYD.js} +1 -1
- package/dist/{app-settings-L5HGYXWJ.js → app-settings-NI4X5HW3.js} +1 -1
- package/dist/{chunk-UXI52MLJ.js → chunk-2J34G3WJ.js} +3 -3
- package/dist/{chunk-PTNGMA5C.js → chunk-A2G3ZBJS.js} +2 -2
- package/dist/{chunk-T47ARZ5Q.js → chunk-FELFTNH2.js} +25 -24
- package/dist/{chunk-OUBQH5GR.js → chunk-HOMZKYHF.js} +31 -29
- package/dist/{chunk-GONPMH36.js → chunk-IJDQKC5A.js} +5 -5
- package/dist/{chunk-USBZ7PH4.js → chunk-IPPY3FG5.js} +1 -1
- package/dist/{chunk-NWE3NOWQ.js → chunk-K7MWJLR7.js} +1 -1
- package/dist/{chunk-HO2T6BC3.js → chunk-MGS5T2A3.js} +31 -29
- package/dist/{chunk-ADWNK2JH.js → chunk-NIB2IZ6J.js} +5 -5
- package/dist/{chunk-AGXZAMOF.js → chunk-NQ7JMVSU.js} +2 -2
- package/dist/{chunk-JLDJGGCL.js → chunk-NT7RIWRR.js} +5 -5
- package/dist/{chunk-L6CYOON4.js → chunk-PZURG3XN.js} +5 -5
- package/dist/{chunk-LAIBYKZS.js → chunk-QINJKNLR.js} +3 -3
- package/dist/{chunk-2QUILREI.js → chunk-QTZO4LPU.js} +2 -2
- package/dist/{chunk-KZ2CDZBZ.js → chunk-R7HLXD4I.js} +3 -3
- package/dist/{chunk-N3LASDJM.js → chunk-SQOAFR2U.js} +26 -25
- package/dist/{chunk-OIENQEGG.js → chunk-WRIMAEF2.js} +67 -122
- package/dist/{chunk-DBFAFI5A.js → chunk-XKZAVGTT.js} +2 -2
- package/dist/{chunk-PAH7KDL2.js → chunk-YNTXWFXT.js} +67 -122
- package/dist/{conductor-3AQM67VK.js → conductor-GBCE37K4.js} +4 -4
- package/dist/{conductor-FLWACOVC.js → conductor-YYQKZAND.js} +4 -4
- package/dist/{coordinator-prompt-4KVA6XUX.js → coordinator-prompt-B3GLVXPP.js} +3 -3
- package/dist/{coordinator-prompt-MVUHFNSC.js → coordinator-prompt-SFZCZ3HR.js} +3 -3
- package/dist/{global-workspace-FR7QTLPK.js → global-workspace-7XEXTIHL.js} +3 -3
- package/dist/{global-workspace-34Z2IWYQ.js → global-workspace-N2YYJKA5.js} +3 -3
- package/dist/index.cjs +1700 -186
- package/dist/index.d.cts +99 -20
- package/dist/index.d.ts +99 -20
- package/dist/index.js +1583 -43
- package/dist/mcp/run-stdio.cjs +95 -141
- package/dist/mcp/run-stdio.js +11 -11
- package/dist/{orchestrator-YKSDWR6C.js → orchestrator-6QJLM6Y4.js} +6 -6
- package/dist/{orchestrator-K42IBMBE.js → orchestrator-QMKVXVUR.js} +6 -6
- package/dist/{workspaces-5GERYHDJ.js → workspaces-BJCHMGVG.js} +3 -3
- package/dist/{workspaces-2JBDUMM6.js → workspaces-PBB6HY2D.js} +3 -3
- package/dist/{worktree-ACW23KJ5.js → worktree-L57T3KH7.js} +2 -2
- package/dist/{worktree-C7RPVHZ7.js → worktree-Q6UJHG25.js} +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -173,7 +173,7 @@ import {
|
|
|
173
173
|
worktreeBoardStatus,
|
|
174
174
|
worktreeCleanupSettings,
|
|
175
175
|
writeWorktreeFile
|
|
176
|
-
} from "./chunk-
|
|
176
|
+
} from "./chunk-SQOAFR2U.js";
|
|
177
177
|
import {
|
|
178
178
|
BASE_ALLOWED_TOOLS,
|
|
179
179
|
BRIGHTSY_MCP_ALLOWED_TOOLS,
|
|
@@ -263,7 +263,7 @@ import {
|
|
|
263
263
|
whichOnPath,
|
|
264
264
|
worktreeMcpToolNames,
|
|
265
265
|
writeInjectedMcpConfig
|
|
266
|
-
} from "./chunk-
|
|
266
|
+
} from "./chunk-NIB2IZ6J.js";
|
|
267
267
|
import {
|
|
268
268
|
brightsyMcpServerName,
|
|
269
269
|
connectBrightsyTeam,
|
|
@@ -380,7 +380,7 @@ import {
|
|
|
380
380
|
waitForTurnToolResult,
|
|
381
381
|
worktreeRunPortsPath,
|
|
382
382
|
wrapReviewSkillMarkdown
|
|
383
|
-
} from "./chunk-
|
|
383
|
+
} from "./chunk-MGS5T2A3.js";
|
|
384
384
|
import {
|
|
385
385
|
STDERR_EVENT_MAX_CHARS,
|
|
386
386
|
STDOUT_EVENT_MAX_CHARS,
|
|
@@ -446,7 +446,7 @@ import {
|
|
|
446
446
|
parseAbleTimeOAuthCallbackUrl,
|
|
447
447
|
resolveAbleTimeOAuthHost,
|
|
448
448
|
startAbleTimeOAuth
|
|
449
|
-
} from "./chunk-
|
|
449
|
+
} from "./chunk-QINJKNLR.js";
|
|
450
450
|
import {
|
|
451
451
|
ableTimeDependencyTarget,
|
|
452
452
|
commentAbleTimeTask,
|
|
@@ -471,8 +471,8 @@ import {
|
|
|
471
471
|
toAbleTimeIssueInfo,
|
|
472
472
|
updateAbleTimeTask,
|
|
473
473
|
verifyAbleTimeConnection
|
|
474
|
-
} from "./chunk-
|
|
475
|
-
import "./chunk-
|
|
474
|
+
} from "./chunk-2J34G3WJ.js";
|
|
475
|
+
import "./chunk-NQ7JMVSU.js";
|
|
476
476
|
import {
|
|
477
477
|
ABLETIME_MCP_PATH,
|
|
478
478
|
ABLETIME_MCP_PM_PATH,
|
|
@@ -493,7 +493,7 @@ import {
|
|
|
493
493
|
normalizeAbleTimeCredential,
|
|
494
494
|
normalizeAbleTimeHost,
|
|
495
495
|
rewriteAbleTimeError
|
|
496
|
-
} from "./chunk-
|
|
496
|
+
} from "./chunk-PZURG3XN.js";
|
|
497
497
|
import {
|
|
498
498
|
LEGACY_PLAN_FILE_REL,
|
|
499
499
|
PLAN_FILE_NAME,
|
|
@@ -557,7 +557,7 @@ import {
|
|
|
557
557
|
stageBuffersAsAttachments,
|
|
558
558
|
syncWorkspacesFromThreads,
|
|
559
559
|
takenTeamSlugsForOrchestration
|
|
560
|
-
} from "./chunk-
|
|
560
|
+
} from "./chunk-A2G3ZBJS.js";
|
|
561
561
|
import {
|
|
562
562
|
FAMOUS_SOCCER_TEAMS,
|
|
563
563
|
GITHUB_PR_BODY_MAX_CHARS,
|
|
@@ -657,7 +657,7 @@ import {
|
|
|
657
657
|
worktreeDisplayLabelForGroup,
|
|
658
658
|
worktreeNameFromPath,
|
|
659
659
|
worktreeSlugForTicket
|
|
660
|
-
} from "./chunk-
|
|
660
|
+
} from "./chunk-IPPY3FG5.js";
|
|
661
661
|
import {
|
|
662
662
|
appendMessage,
|
|
663
663
|
createEmptyThread,
|
|
@@ -910,7 +910,7 @@ import {
|
|
|
910
910
|
withTurnScopedCost,
|
|
911
911
|
writeSecureJson,
|
|
912
912
|
writeViewerContext
|
|
913
|
-
} from "./chunk-
|
|
913
|
+
} from "./chunk-YNTXWFXT.js";
|
|
914
914
|
import {
|
|
915
915
|
THINKING_EFFORTS,
|
|
916
916
|
isThinkingEffort,
|
|
@@ -1384,12 +1384,12 @@ async function persistLinearTokens(data, viewer) {
|
|
|
1384
1384
|
organizationName: viewer?.organizationName
|
|
1385
1385
|
});
|
|
1386
1386
|
}
|
|
1387
|
-
async function fetchLinearViewer(
|
|
1387
|
+
async function fetchLinearViewer(accessToken2) {
|
|
1388
1388
|
const res = await httpFetch(LINEAR_GRAPHQL, {
|
|
1389
1389
|
method: "POST",
|
|
1390
1390
|
headers: {
|
|
1391
1391
|
"Content-Type": "application/json",
|
|
1392
|
-
Authorization: linearAuthorizationHeader(
|
|
1392
|
+
Authorization: linearAuthorizationHeader(accessToken2)
|
|
1393
1393
|
},
|
|
1394
1394
|
body: JSON.stringify({
|
|
1395
1395
|
query: "{ viewer { name organization { name } } }"
|
|
@@ -5139,7 +5139,7 @@ async function handleJobStop(params, _ctx) {
|
|
|
5139
5139
|
return stopDetachedJob(jobCwd(rec), id, reason ? { reason } : void 0);
|
|
5140
5140
|
}
|
|
5141
5141
|
async function defaultTurnWait(ref, timeoutMs) {
|
|
5142
|
-
const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-
|
|
5142
|
+
const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QMKVXVUR.js");
|
|
5143
5143
|
const orch = getOrchestrator2();
|
|
5144
5144
|
const thread = await orch.waitForTurn(ref, timeoutMs, {
|
|
5145
5145
|
resolveIfStillRunning: true
|
|
@@ -5633,7 +5633,7 @@ async function startMcpServer() {
|
|
|
5633
5633
|
);
|
|
5634
5634
|
}
|
|
5635
5635
|
try {
|
|
5636
|
-
const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-
|
|
5636
|
+
const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-NI4X5HW3.js");
|
|
5637
5637
|
orch.setMaxConcurrent(maxConcurrentAgents2());
|
|
5638
5638
|
} catch {
|
|
5639
5639
|
}
|
|
@@ -6121,7 +6121,7 @@ async function startMcpServer() {
|
|
|
6121
6121
|
if (!worktreeProfile) {
|
|
6122
6122
|
registerConnectedIssueVendorTools(server);
|
|
6123
6123
|
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-DYJGWK6Y.js");
|
|
6124
|
-
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-
|
|
6124
|
+
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-NI4X5HW3.js");
|
|
6125
6125
|
const accountDefaults = resolveThreadDefaults2();
|
|
6126
6126
|
const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
|
|
6127
6127
|
server.tool(
|
|
@@ -8992,7 +8992,14 @@ function parseRemoteClientMessage(raw) {
|
|
|
8992
8992
|
const hostSecret = str2(parsed.hostSecret);
|
|
8993
8993
|
const deviceLabel = str2(parsed.deviceLabel);
|
|
8994
8994
|
if (!deviceId || !hostSecret || !deviceLabel) return null;
|
|
8995
|
-
|
|
8995
|
+
const accountToken = str2(parsed.accountToken);
|
|
8996
|
+
return {
|
|
8997
|
+
type: "host_register",
|
|
8998
|
+
deviceId,
|
|
8999
|
+
hostSecret,
|
|
9000
|
+
deviceLabel,
|
|
9001
|
+
...accountToken ? { accountToken } : {}
|
|
9002
|
+
};
|
|
8996
9003
|
}
|
|
8997
9004
|
if (msg.type === "pair") {
|
|
8998
9005
|
const code = str2(parsed.code).toUpperCase();
|
|
@@ -9121,6 +9128,10 @@ function newCode() {
|
|
|
9121
9128
|
return code;
|
|
9122
9129
|
}
|
|
9123
9130
|
var RemoteHub = class {
|
|
9131
|
+
constructor(opts = {}) {
|
|
9132
|
+
this.opts = opts;
|
|
9133
|
+
}
|
|
9134
|
+
opts;
|
|
9124
9135
|
hosts = /* @__PURE__ */ new Map();
|
|
9125
9136
|
remembered = /* @__PURE__ */ new Map();
|
|
9126
9137
|
bySocket = /* @__PURE__ */ new Map();
|
|
@@ -9159,7 +9170,7 @@ var RemoteHub = class {
|
|
|
9159
9170
|
}
|
|
9160
9171
|
dispatch(socket, msg) {
|
|
9161
9172
|
if (msg.type === "host_register") {
|
|
9162
|
-
this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret);
|
|
9173
|
+
this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret, msg.accountToken ?? "");
|
|
9163
9174
|
return;
|
|
9164
9175
|
}
|
|
9165
9176
|
if (msg.type === "host_pair") {
|
|
@@ -9186,7 +9197,14 @@ var RemoteHub = class {
|
|
|
9186
9197
|
this.fromHost(socket, msg);
|
|
9187
9198
|
}
|
|
9188
9199
|
}
|
|
9189
|
-
registerHost(socket, deviceId, deviceLabel, hostSecret) {
|
|
9200
|
+
registerHost(socket, deviceId, deviceLabel, hostSecret, accountToken) {
|
|
9201
|
+
if (this.opts.authorizeHost) {
|
|
9202
|
+
const decision = this.opts.authorizeHost({ accountToken, deviceId, deviceLabel, hostSecret });
|
|
9203
|
+
if (!decision.ok) {
|
|
9204
|
+
send2(socket, { type: "error", message: decision.message });
|
|
9205
|
+
return;
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9190
9208
|
const prior = this.remembered.get(deviceId);
|
|
9191
9209
|
const existing = this.hosts.get(deviceId);
|
|
9192
9210
|
if (prior && prior.hostSecret !== hostSecret || existing && existing.hostSecret !== hostSecret) {
|
|
@@ -9218,6 +9236,7 @@ var RemoteHub = class {
|
|
|
9218
9236
|
this.bySocket.set(socket, { role: "host", deviceId });
|
|
9219
9237
|
send2(socket, { type: "registered", deviceId, deviceLabel });
|
|
9220
9238
|
if (host.phone) send2(socket, { type: "phone_joined" });
|
|
9239
|
+
this.opts.onAuthorized?.(socket);
|
|
9221
9240
|
}
|
|
9222
9241
|
issueCode(socket) {
|
|
9223
9242
|
const bound = this.bySocket.get(socket);
|
|
@@ -9279,6 +9298,7 @@ var RemoteHub = class {
|
|
|
9279
9298
|
online: Boolean(host)
|
|
9280
9299
|
};
|
|
9281
9300
|
});
|
|
9301
|
+
if (hosts.some((host) => host.deviceId)) this.opts.onAuthorized?.(socket);
|
|
9282
9302
|
send2(socket, { type: "hosts", hosts });
|
|
9283
9303
|
}
|
|
9284
9304
|
releasePhoneSocket(socket) {
|
|
@@ -9318,6 +9338,7 @@ var RemoteHub = class {
|
|
|
9318
9338
|
deviceLabel: host.deviceLabel
|
|
9319
9339
|
});
|
|
9320
9340
|
this.bySocket.set(socket, { role: "phone", deviceId: host.deviceId });
|
|
9341
|
+
this.opts.onAuthorized?.(socket);
|
|
9321
9342
|
send2(socket, {
|
|
9322
9343
|
type: "paired",
|
|
9323
9344
|
deviceId: host.deviceId,
|
|
@@ -9354,6 +9375,524 @@ var RemoteHub = class {
|
|
|
9354
9375
|
// src/remote/client.ts
|
|
9355
9376
|
import { WebSocket as WsWebSocket3 } from "ws";
|
|
9356
9377
|
|
|
9378
|
+
// src/remote/phone-chats.ts
|
|
9379
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9380
|
+
var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
|
|
9381
|
+
var PHONE_USER_PREFIX = "Phone\n\n";
|
|
9382
|
+
var TRANSCRIPT_LIMIT = 40;
|
|
9383
|
+
var MESSAGE_LIMIT = 4e3;
|
|
9384
|
+
var PREVIEW_LIMIT = 90;
|
|
9385
|
+
var HISTORY_PAGE_SIZE = 40;
|
|
9386
|
+
var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
9387
|
+
var MAX_PHONE_FILES = 8;
|
|
9388
|
+
var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
|
|
9389
|
+
var MAX_PHONE_FILES_CHARS = Math.ceil(20 * 1024 * 1024 * 4 / 3);
|
|
9390
|
+
var MAX_DICTATION_CHARS = Math.ceil(12 * 1024 * 1024 * 4 / 3);
|
|
9391
|
+
function encodePhoneControl(payload) {
|
|
9392
|
+
return PHONE_CONTROL_PREFIX + JSON.stringify(payload);
|
|
9393
|
+
}
|
|
9394
|
+
function takePhoneControl(text6) {
|
|
9395
|
+
if (!text6.startsWith(PHONE_CONTROL_PREFIX)) return null;
|
|
9396
|
+
let parsed;
|
|
9397
|
+
try {
|
|
9398
|
+
parsed = JSON.parse(text6.slice(PHONE_CONTROL_PREFIX.length));
|
|
9399
|
+
} catch {
|
|
9400
|
+
return "invalid";
|
|
9401
|
+
}
|
|
9402
|
+
return parsePhoneControl(parsed);
|
|
9403
|
+
}
|
|
9404
|
+
function parsePhoneControl(value) {
|
|
9405
|
+
if (!value || typeof value !== "object") return "invalid";
|
|
9406
|
+
const op = value.op;
|
|
9407
|
+
if (op === "list") return { op: "list" };
|
|
9408
|
+
if (op === "history") {
|
|
9409
|
+
const query = textField(value, "query");
|
|
9410
|
+
const after = textField(value, "after");
|
|
9411
|
+
return { op: "history", ...query ? { query } : {}, ...after ? { after } : {} };
|
|
9412
|
+
}
|
|
9413
|
+
if (op === "sources") {
|
|
9414
|
+
const repoPath = textField(value, "repoPath");
|
|
9415
|
+
if (!repoPath || !isProjectPath(repoPath)) return "invalid";
|
|
9416
|
+
const query = textField(value, "query");
|
|
9417
|
+
return { op: "sources", repoPath, ...query ? { query } : {} };
|
|
9418
|
+
}
|
|
9419
|
+
if (op === "models") {
|
|
9420
|
+
const agent = agentField(value);
|
|
9421
|
+
if (!agent) return "invalid";
|
|
9422
|
+
return { op: "models", agent };
|
|
9423
|
+
}
|
|
9424
|
+
if (op === "dictate") {
|
|
9425
|
+
const id = textField(value, "id");
|
|
9426
|
+
const audioBase64 = base64Field(value, "audioBase64");
|
|
9427
|
+
if (!id || !audioBase64 || audioBase64.length > MAX_DICTATION_CHARS) return "invalid";
|
|
9428
|
+
return { op: "dictate", id, audioBase64 };
|
|
9429
|
+
}
|
|
9430
|
+
if (op === "create") return parsePhoneCreate(value);
|
|
9431
|
+
const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
|
|
9432
|
+
if (!chatId) return "invalid";
|
|
9433
|
+
if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
|
|
9434
|
+
const draft = parsePhoneDraft(value);
|
|
9435
|
+
if (draft === "invalid") return "invalid";
|
|
9436
|
+
if (op === "options") return { op: "options", chatId, ...draft };
|
|
9437
|
+
if (op === "prompt") {
|
|
9438
|
+
const text6 = typeof value.text === "string" ? value.text.trim() : "";
|
|
9439
|
+
const hasPayload = Boolean(text6 || draft.files?.length || draft.links?.length);
|
|
9440
|
+
if (!hasPayload) return "invalid";
|
|
9441
|
+
return { op: "prompt", chatId, text: text6, ...draft };
|
|
9442
|
+
}
|
|
9443
|
+
return "invalid";
|
|
9444
|
+
}
|
|
9445
|
+
function textField(value, key) {
|
|
9446
|
+
const raw = value[key];
|
|
9447
|
+
return typeof raw === "string" ? raw.trim() : "";
|
|
9448
|
+
}
|
|
9449
|
+
function agentField(value) {
|
|
9450
|
+
const agent = textField(value, "agent");
|
|
9451
|
+
if (!agent) return void 0;
|
|
9452
|
+
return AGENT_KINDS.has(agent) ? agent : void 0;
|
|
9453
|
+
}
|
|
9454
|
+
function base64Field(value, key) {
|
|
9455
|
+
const raw = value[key];
|
|
9456
|
+
if (typeof raw !== "string") return "";
|
|
9457
|
+
return raw.replace(/\s/g, "");
|
|
9458
|
+
}
|
|
9459
|
+
function parsePhoneDraft(value) {
|
|
9460
|
+
const draft = {};
|
|
9461
|
+
if (textField(value, "agent")) {
|
|
9462
|
+
const agent = agentField(value);
|
|
9463
|
+
if (!agent) return "invalid";
|
|
9464
|
+
draft.agent = agent;
|
|
9465
|
+
}
|
|
9466
|
+
if ("model" in value) {
|
|
9467
|
+
const model = value.model;
|
|
9468
|
+
if (model === null || model === "") draft.model = null;
|
|
9469
|
+
else if (typeof model === "string") draft.model = model.trim().slice(0, 200);
|
|
9470
|
+
else return "invalid";
|
|
9471
|
+
}
|
|
9472
|
+
if ("effort" in value && value.effort != null) {
|
|
9473
|
+
const effort = value.effort;
|
|
9474
|
+
if (!isThinkingEffort(effort)) return "invalid";
|
|
9475
|
+
draft.effort = effort;
|
|
9476
|
+
}
|
|
9477
|
+
if ("fast" in value) {
|
|
9478
|
+
const fast = value.fast;
|
|
9479
|
+
if (typeof fast !== "boolean") return "invalid";
|
|
9480
|
+
draft.fast = fast;
|
|
9481
|
+
}
|
|
9482
|
+
if ("planMode" in value) {
|
|
9483
|
+
const planMode = value.planMode;
|
|
9484
|
+
if (typeof planMode !== "boolean") return "invalid";
|
|
9485
|
+
draft.planMode = planMode;
|
|
9486
|
+
}
|
|
9487
|
+
if ("autonomy" in value && value.autonomy != null) {
|
|
9488
|
+
const autonomy = value.autonomy;
|
|
9489
|
+
if (autonomy !== "default" && autonomy !== "full") return "invalid";
|
|
9490
|
+
draft.autonomy = autonomy;
|
|
9491
|
+
}
|
|
9492
|
+
const files = parsePhoneFiles(value.files);
|
|
9493
|
+
if (files === "invalid") return "invalid";
|
|
9494
|
+
if (files.length) draft.files = files;
|
|
9495
|
+
const links = parsePhoneLinks(value.links);
|
|
9496
|
+
if (links === "invalid") return "invalid";
|
|
9497
|
+
if (links.length) draft.links = links;
|
|
9498
|
+
const prompt = textField(value, "prompt");
|
|
9499
|
+
if (prompt) draft.prompt = prompt.slice(0, 2e4);
|
|
9500
|
+
return draft;
|
|
9501
|
+
}
|
|
9502
|
+
function parsePhoneFiles(value) {
|
|
9503
|
+
if (value == null) return [];
|
|
9504
|
+
if (!Array.isArray(value) || value.length > MAX_PHONE_FILES) return "invalid";
|
|
9505
|
+
const files = [];
|
|
9506
|
+
let total = 0;
|
|
9507
|
+
for (const item of value) {
|
|
9508
|
+
if (!item || typeof item !== "object") return "invalid";
|
|
9509
|
+
const name = (textField(item, "name").replace(/[/\\]/g, "_") || "file").slice(0, 180);
|
|
9510
|
+
const dataBase64 = base64Field(item, "dataBase64");
|
|
9511
|
+
if (!dataBase64 || !/^[A-Za-z0-9+/=]+$/.test(dataBase64)) return "invalid";
|
|
9512
|
+
if (dataBase64.length > MAX_PHONE_FILE_CHARS) return "invalid";
|
|
9513
|
+
total += dataBase64.length;
|
|
9514
|
+
if (total > MAX_PHONE_FILES_CHARS) return "invalid";
|
|
9515
|
+
files.push({ name, dataBase64 });
|
|
9516
|
+
}
|
|
9517
|
+
return files;
|
|
9518
|
+
}
|
|
9519
|
+
function parsePhoneLinks(value) {
|
|
9520
|
+
if (value == null) return [];
|
|
9521
|
+
if (!Array.isArray(value) || value.length > 8) return "invalid";
|
|
9522
|
+
const links = [];
|
|
9523
|
+
for (const item of value) {
|
|
9524
|
+
if (!item || typeof item !== "object") return "invalid";
|
|
9525
|
+
const ref = textField(item, "ref").slice(0, 80);
|
|
9526
|
+
if (!ref) return "invalid";
|
|
9527
|
+
const title = (textField(item, "title") || ref).slice(0, 200);
|
|
9528
|
+
const url = textField(item, "url").slice(0, 500);
|
|
9529
|
+
links.push({ ref, title, ...url ? { url } : {} });
|
|
9530
|
+
}
|
|
9531
|
+
return links;
|
|
9532
|
+
}
|
|
9533
|
+
function parsePhoneCreate(value) {
|
|
9534
|
+
const draft = parsePhoneDraft(value);
|
|
9535
|
+
if (draft === "invalid") return "invalid";
|
|
9536
|
+
const goal = textField(value, "goal") || draft.prompt || "";
|
|
9537
|
+
const where = value.where;
|
|
9538
|
+
if (where == null || where === "orchestration") {
|
|
9539
|
+
return {
|
|
9540
|
+
op: "create",
|
|
9541
|
+
where: "orchestration",
|
|
9542
|
+
...draft,
|
|
9543
|
+
...goal ? { goal, prompt: goal } : {}
|
|
9544
|
+
};
|
|
9545
|
+
}
|
|
9546
|
+
if (where === "worktree") {
|
|
9547
|
+
const chatId = textField(value, "chatId");
|
|
9548
|
+
if (!chatId) return "invalid";
|
|
9549
|
+
return { op: "create", where: "worktree", chatId, ...draft };
|
|
9550
|
+
}
|
|
9551
|
+
if (where === "project") {
|
|
9552
|
+
const repoPath = textField(value, "repoPath");
|
|
9553
|
+
if (!repoPath || !isProjectPath(repoPath)) return "invalid";
|
|
9554
|
+
const sourceRaw = textField(value, "sourceType");
|
|
9555
|
+
let sourceType;
|
|
9556
|
+
if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
|
|
9557
|
+
else if (sourceRaw) return "invalid";
|
|
9558
|
+
const sourceRef = textField(value, "sourceRef");
|
|
9559
|
+
if ((sourceType === "pr" || sourceType === "ticket") && !sourceRef) return "invalid";
|
|
9560
|
+
const title = textField(value, "title").slice(0, 200);
|
|
9561
|
+
const cowboy = value.cowboy === true;
|
|
9562
|
+
return {
|
|
9563
|
+
op: "create",
|
|
9564
|
+
where: "project",
|
|
9565
|
+
repoPath,
|
|
9566
|
+
...draft,
|
|
9567
|
+
...sourceType ? { sourceType } : {},
|
|
9568
|
+
...sourceRef ? { sourceRef } : {},
|
|
9569
|
+
...title ? { title } : {},
|
|
9570
|
+
...cowboy ? { cowboy: true } : {}
|
|
9571
|
+
};
|
|
9572
|
+
}
|
|
9573
|
+
return "invalid";
|
|
9574
|
+
}
|
|
9575
|
+
function phoneVisibleText(role, text6) {
|
|
9576
|
+
const trimmed = text6.trim();
|
|
9577
|
+
if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
|
|
9578
|
+
return trimmed.slice(PHONE_USER_PREFIX.length).trim();
|
|
9579
|
+
}
|
|
9580
|
+
return trimmed;
|
|
9581
|
+
}
|
|
9582
|
+
function clip(text6, limit) {
|
|
9583
|
+
if (text6.length <= limit) return text6;
|
|
9584
|
+
return `${text6.slice(0, limit - 1)}\u2026`;
|
|
9585
|
+
}
|
|
9586
|
+
function summarize(thread) {
|
|
9587
|
+
return {
|
|
9588
|
+
id: thread.id,
|
|
9589
|
+
title: thread.title?.trim() || "Untitled",
|
|
9590
|
+
status: thread.status,
|
|
9591
|
+
preview: previewOf(thread),
|
|
9592
|
+
updatedAt: thread.updatedAt
|
|
9593
|
+
};
|
|
9594
|
+
}
|
|
9595
|
+
function previewOf(thread) {
|
|
9596
|
+
const messages = listPhoneMessages(thread);
|
|
9597
|
+
const last = messages[messages.length - 1];
|
|
9598
|
+
if (!last) return "";
|
|
9599
|
+
return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
|
|
9600
|
+
}
|
|
9601
|
+
function listPhoneMessages(thread) {
|
|
9602
|
+
const out = [];
|
|
9603
|
+
for (const message of thread.messages) {
|
|
9604
|
+
if (message.role !== "user" && message.role !== "agent") continue;
|
|
9605
|
+
const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
|
|
9606
|
+
const text6 = clip(
|
|
9607
|
+
[phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
|
|
9608
|
+
MESSAGE_LIMIT
|
|
9609
|
+
);
|
|
9610
|
+
if (!text6) continue;
|
|
9611
|
+
out.push({ role: message.role, text: text6 });
|
|
9612
|
+
}
|
|
9613
|
+
for (const prompt of thread.queue) {
|
|
9614
|
+
const text6 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
|
|
9615
|
+
if (text6) out.push({ role: "user", text: text6 });
|
|
9616
|
+
}
|
|
9617
|
+
return out.slice(-TRANSCRIPT_LIMIT);
|
|
9618
|
+
}
|
|
9619
|
+
function repoName(repoPath) {
|
|
9620
|
+
const parts = repoPath.replace(/\/$/, "").split("/");
|
|
9621
|
+
return parts[parts.length - 1] || repoPath;
|
|
9622
|
+
}
|
|
9623
|
+
function isProjectPath(path2) {
|
|
9624
|
+
if (!path2 || path2 === "/" || path2 === ".") return false;
|
|
9625
|
+
return !isGlobalThread({ repoPath: path2 });
|
|
9626
|
+
}
|
|
9627
|
+
function listPhoneChats() {
|
|
9628
|
+
return listGlobalThreads().slice().sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0).map(summarize);
|
|
9629
|
+
}
|
|
9630
|
+
function listPhoneSidebar() {
|
|
9631
|
+
const threads = listThreads();
|
|
9632
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
9633
|
+
for (const thread of threads) {
|
|
9634
|
+
if (!isProjectPath(thread.repoPath)) continue;
|
|
9635
|
+
const list = byRepo.get(thread.repoPath) ?? [];
|
|
9636
|
+
list.push(thread);
|
|
9637
|
+
byRepo.set(thread.repoPath, list);
|
|
9638
|
+
}
|
|
9639
|
+
for (const workspace of listWorkspaces()) {
|
|
9640
|
+
if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
|
|
9641
|
+
byRepo.set(workspace.path, []);
|
|
9642
|
+
}
|
|
9643
|
+
const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
|
|
9644
|
+
name: repoName(path2),
|
|
9645
|
+
path: path2,
|
|
9646
|
+
worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => ({
|
|
9647
|
+
label: worktreeDisplayLabelForGroup(group),
|
|
9648
|
+
chats: group.map(summarize)
|
|
9649
|
+
}))
|
|
9650
|
+
}));
|
|
9651
|
+
return { orchestration: listPhoneChats(), projects };
|
|
9652
|
+
}
|
|
9653
|
+
function historyStamp(thread) {
|
|
9654
|
+
return thread.archivedAt || thread.updatedAt;
|
|
9655
|
+
}
|
|
9656
|
+
function historyCursor(chat) {
|
|
9657
|
+
return `${chat.updatedAt} ${chat.id}`;
|
|
9658
|
+
}
|
|
9659
|
+
function historyQueryMatch(chat, query) {
|
|
9660
|
+
if (!query) return true;
|
|
9661
|
+
return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
|
|
9662
|
+
}
|
|
9663
|
+
function listPhoneHistory(opts) {
|
|
9664
|
+
const query = opts?.query?.trim().toLowerCase() ?? "";
|
|
9665
|
+
const after = opts?.after?.trim() ?? "";
|
|
9666
|
+
const all = listThreads({ includeArchived: true }).filter((thread) => thread.status === "archived").filter((thread) => isGlobalThread(thread) || isProjectPath(thread.repoPath)).sort((a, b) => historyStamp(a) < historyStamp(b) ? 1 : historyStamp(a) > historyStamp(b) ? -1 : 0).map((thread) => ({
|
|
9667
|
+
id: thread.id,
|
|
9668
|
+
title: thread.title?.trim() || "Untitled",
|
|
9669
|
+
preview: previewOf(thread),
|
|
9670
|
+
updatedAt: historyStamp(thread),
|
|
9671
|
+
where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
|
|
9672
|
+
agent: thread.agent
|
|
9673
|
+
})).filter((chat) => historyQueryMatch(chat, query));
|
|
9674
|
+
let start = 0;
|
|
9675
|
+
if (after) {
|
|
9676
|
+
const index = all.findIndex((chat) => historyCursor(chat) === after);
|
|
9677
|
+
start = index >= 0 ? index + 1 : all.length;
|
|
9678
|
+
}
|
|
9679
|
+
const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
|
|
9680
|
+
const last = chats.at(-1);
|
|
9681
|
+
const more = start + chats.length < all.length;
|
|
9682
|
+
return {
|
|
9683
|
+
chats,
|
|
9684
|
+
total: all.length,
|
|
9685
|
+
...more && last ? { next: historyCursor(last) } : {}
|
|
9686
|
+
};
|
|
9687
|
+
}
|
|
9688
|
+
function readPhoneThread(chatId) {
|
|
9689
|
+
const id = chatId.trim();
|
|
9690
|
+
if (!id) return null;
|
|
9691
|
+
const thread = readThread(id);
|
|
9692
|
+
if (!thread || thread.status === "archived") return null;
|
|
9693
|
+
if (isGlobalThread(thread)) return thread;
|
|
9694
|
+
if (!isProjectPath(thread.repoPath)) return null;
|
|
9695
|
+
return thread;
|
|
9696
|
+
}
|
|
9697
|
+
function phoneComposerOptions(thread) {
|
|
9698
|
+
return {
|
|
9699
|
+
agent: thread.agent,
|
|
9700
|
+
model: thread.model ?? null,
|
|
9701
|
+
effort: thread.effort,
|
|
9702
|
+
fast: Boolean(thread.fast),
|
|
9703
|
+
planMode: Boolean(thread.planMode),
|
|
9704
|
+
autonomy: thread.autonomy ?? "default"
|
|
9705
|
+
};
|
|
9706
|
+
}
|
|
9707
|
+
function phoneTurnAttachments(draft) {
|
|
9708
|
+
const links = (draft.links ?? []).map((link) => ({
|
|
9709
|
+
id: randomUUID7(),
|
|
9710
|
+
name: link.ref,
|
|
9711
|
+
kind: "issue",
|
|
9712
|
+
content: [`Linked issue: ${link.ref} \u2014 ${link.title}`, link.url ? `URL: ${link.url}` : null].filter(Boolean).join("\n")
|
|
9713
|
+
}));
|
|
9714
|
+
const files = (draft.files ?? []).map((file) => ({
|
|
9715
|
+
id: randomUUID7(),
|
|
9716
|
+
name: file.name,
|
|
9717
|
+
kind: "file",
|
|
9718
|
+
previewDataUrl: `data:application/octet-stream;base64,${file.dataBase64}`,
|
|
9719
|
+
content: `File attached: ${file.name}`
|
|
9720
|
+
}));
|
|
9721
|
+
return [...links, ...files];
|
|
9722
|
+
}
|
|
9723
|
+
function openedFrom(thread, place) {
|
|
9724
|
+
const fresh = readThread(thread.id) ?? thread;
|
|
9725
|
+
return {
|
|
9726
|
+
chat: summarize(fresh),
|
|
9727
|
+
messages: listPhoneMessages(fresh),
|
|
9728
|
+
place,
|
|
9729
|
+
options: phoneComposerOptions(fresh)
|
|
9730
|
+
};
|
|
9731
|
+
}
|
|
9732
|
+
function openPhoneChat(chatId) {
|
|
9733
|
+
const thread = readPhoneThread(chatId);
|
|
9734
|
+
if (!thread) return null;
|
|
9735
|
+
const place = isGlobalThread(thread) ? { kind: "orchestration" } : projectPlace(thread);
|
|
9736
|
+
return openedFrom(thread, place);
|
|
9737
|
+
}
|
|
9738
|
+
function sameRepoPath(a, b) {
|
|
9739
|
+
return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
9740
|
+
}
|
|
9741
|
+
function projectPlace(thread) {
|
|
9742
|
+
const group = threadsSharingWorktree(thread.worktreePath);
|
|
9743
|
+
return {
|
|
9744
|
+
kind: "project",
|
|
9745
|
+
repoPath: thread.repoPath,
|
|
9746
|
+
worktree: worktreeDisplayLabelForGroup(group.length > 0 ? group : [thread])
|
|
9747
|
+
};
|
|
9748
|
+
}
|
|
9749
|
+
function assertKnownProject(repoPath) {
|
|
9750
|
+
const path2 = repoPath.trim();
|
|
9751
|
+
if (!isProjectPath(path2)) throw new Error("Pick a project on this Mac.");
|
|
9752
|
+
const known = listWorkspaces().some((workspace) => sameRepoPath(workspace.path, path2)) || listThreads().some(
|
|
9753
|
+
(thread) => thread.status !== "archived" && sameRepoPath(thread.repoPath, path2)
|
|
9754
|
+
);
|
|
9755
|
+
if (!known) throw new Error("That project is not on this Mac.");
|
|
9756
|
+
return path2;
|
|
9757
|
+
}
|
|
9758
|
+
function draftAgentOptions(draft, defaults) {
|
|
9759
|
+
return {
|
|
9760
|
+
agent: draft?.agent ?? defaults.agent,
|
|
9761
|
+
model: draft && "model" in draft ? draft.model ?? null : defaults.model,
|
|
9762
|
+
effort: draft?.effort ?? defaults.effort,
|
|
9763
|
+
fast: draft?.fast ?? defaults.fast,
|
|
9764
|
+
planMode: Boolean(draft?.planMode),
|
|
9765
|
+
autonomy: draft?.autonomy
|
|
9766
|
+
};
|
|
9767
|
+
}
|
|
9768
|
+
function createPhoneChat(agent) {
|
|
9769
|
+
const defaults = resolveOrchestratorDefaults();
|
|
9770
|
+
const thread = createGlobalChat({
|
|
9771
|
+
agent: agent ?? defaults.agent,
|
|
9772
|
+
model: defaults.model,
|
|
9773
|
+
effort: defaults.effort,
|
|
9774
|
+
fast: defaults.fast
|
|
9775
|
+
});
|
|
9776
|
+
return openedFrom(thread, { kind: "orchestration" });
|
|
9777
|
+
}
|
|
9778
|
+
async function createPhoneOrchestration(draft) {
|
|
9779
|
+
const goal = (draft?.goal ?? draft?.prompt ?? "").trim();
|
|
9780
|
+
const attachments = phoneTurnAttachments(draft ?? {});
|
|
9781
|
+
if (!goal && attachments.length === 0) return createPhoneChat(draft?.agent);
|
|
9782
|
+
const defaults = resolveOrchestratorDefaults();
|
|
9783
|
+
const options = draftAgentOptions(draft, defaults);
|
|
9784
|
+
const thread = await startOrchestration({
|
|
9785
|
+
goal: goal || "See the attached files.",
|
|
9786
|
+
agent: options.agent,
|
|
9787
|
+
model: options.model,
|
|
9788
|
+
effort: options.effort,
|
|
9789
|
+
fast: options.fast,
|
|
9790
|
+
planMode: options.planMode,
|
|
9791
|
+
autonomy: options.autonomy,
|
|
9792
|
+
attachments
|
|
9793
|
+
});
|
|
9794
|
+
return openedFrom(thread, { kind: "orchestration" });
|
|
9795
|
+
}
|
|
9796
|
+
function createPhoneWorktreeAgent(fromChatId, draft) {
|
|
9797
|
+
const source = readPhoneThread(fromChatId);
|
|
9798
|
+
if (!source || isGlobalThread(source)) throw new Error("That worktree is not on this Mac.");
|
|
9799
|
+
const defaults = resolveNewThreadOptions();
|
|
9800
|
+
const options = draftAgentOptions(draft, defaults);
|
|
9801
|
+
const attachments = persistPendingFileAttachments(
|
|
9802
|
+
source.worktreePath,
|
|
9803
|
+
phoneTurnAttachments(draft ?? {})
|
|
9804
|
+
);
|
|
9805
|
+
const thread = createChatTab({
|
|
9806
|
+
fromThreadId: source.id,
|
|
9807
|
+
agent: options.agent,
|
|
9808
|
+
model: options.model,
|
|
9809
|
+
effort: options.effort,
|
|
9810
|
+
fast: options.fast,
|
|
9811
|
+
autonomy: options.autonomy,
|
|
9812
|
+
attachments
|
|
9813
|
+
});
|
|
9814
|
+
if (draft?.planMode !== void 0) updateThread(thread.id, { planMode: draft.planMode });
|
|
9815
|
+
return openedFrom(thread, projectPlace(readThread(thread.id) ?? thread));
|
|
9816
|
+
}
|
|
9817
|
+
async function createPhoneProjectWorktree(repoPathOrInput) {
|
|
9818
|
+
const input = typeof repoPathOrInput === "string" ? { repoPath: repoPathOrInput } : repoPathOrInput;
|
|
9819
|
+
const path2 = assertKnownProject(input.repoPath);
|
|
9820
|
+
const defaults = resolveNewThreadOptions();
|
|
9821
|
+
const options = draftAgentOptions(input, defaults);
|
|
9822
|
+
const thread = await createThread({
|
|
9823
|
+
sourceType: input.sourceType ?? "branch",
|
|
9824
|
+
sourceRef: input.sourceRef || "default",
|
|
9825
|
+
repoPath: path2,
|
|
9826
|
+
title: input.title,
|
|
9827
|
+
cowboy: input.cowboy,
|
|
9828
|
+
agent: options.agent,
|
|
9829
|
+
model: options.model,
|
|
9830
|
+
effort: options.effort,
|
|
9831
|
+
fast: options.fast,
|
|
9832
|
+
planMode: options.planMode,
|
|
9833
|
+
autonomy: options.autonomy,
|
|
9834
|
+
attachments: phoneTurnAttachments(input),
|
|
9835
|
+
prompt: input.prompt,
|
|
9836
|
+
reuseExisting: false
|
|
9837
|
+
});
|
|
9838
|
+
return openedFrom(thread, projectPlace(thread));
|
|
9839
|
+
}
|
|
9840
|
+
function sourceWarning(label, err) {
|
|
9841
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9842
|
+
return message ? `${label}: ${message}` : `${label} failed.`;
|
|
9843
|
+
}
|
|
9844
|
+
async function listPhoneSources(repoPath, query) {
|
|
9845
|
+
const path2 = assertKnownProject(repoPath);
|
|
9846
|
+
const q = query?.trim() ?? "";
|
|
9847
|
+
const warnings = [];
|
|
9848
|
+
const [prs, branches, issues] = await Promise.all([
|
|
9849
|
+
listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
|
|
9850
|
+
warnings.push(sourceWarning("Pull requests", err));
|
|
9851
|
+
return [];
|
|
9852
|
+
}),
|
|
9853
|
+
listBranches(path2).catch((err) => {
|
|
9854
|
+
warnings.push(sourceWarning("Branches", err));
|
|
9855
|
+
return [];
|
|
9856
|
+
}),
|
|
9857
|
+
listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
|
|
9858
|
+
warnings.push(sourceWarning("Issues", err));
|
|
9859
|
+
return { issues: [] };
|
|
9860
|
+
})
|
|
9861
|
+
]);
|
|
9862
|
+
const needle = q.toLowerCase();
|
|
9863
|
+
const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
|
|
9864
|
+
ref: branch.name,
|
|
9865
|
+
title: branch.current ? `${branch.name} (current)` : branch.name
|
|
9866
|
+
}));
|
|
9867
|
+
return {
|
|
9868
|
+
prs: prs.slice(0, 40).map((pr) => ({
|
|
9869
|
+
ref: String(pr.number),
|
|
9870
|
+
title: clip(pr.title || `PR #${pr.number}`, 140),
|
|
9871
|
+
...pr.url ? { url: pr.url } : {}
|
|
9872
|
+
})),
|
|
9873
|
+
branches: branchRows,
|
|
9874
|
+
issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
|
|
9875
|
+
ref: issue.identifier,
|
|
9876
|
+
title: clip(issue.title || issue.identifier, 140),
|
|
9877
|
+
...issue.url ? { url: issue.url } : {}
|
|
9878
|
+
})),
|
|
9879
|
+
...warnings.length ? { warnings } : {}
|
|
9880
|
+
};
|
|
9881
|
+
}
|
|
9882
|
+
async function listPhoneModels(agent) {
|
|
9883
|
+
const catalogs = await listModelsForAgent(agent);
|
|
9884
|
+
return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
|
|
9885
|
+
id: model.id,
|
|
9886
|
+
label: model.displayName || model.id
|
|
9887
|
+
}));
|
|
9888
|
+
}
|
|
9889
|
+
function archivePhoneChat(chatId) {
|
|
9890
|
+
const thread = readPhoneThread(chatId);
|
|
9891
|
+
if (!thread) return false;
|
|
9892
|
+
updateThread(thread.id, { status: "archived" });
|
|
9893
|
+
return true;
|
|
9894
|
+
}
|
|
9895
|
+
|
|
9357
9896
|
// src/remote/listen.ts
|
|
9358
9897
|
var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
|
|
9359
9898
|
function formatRemotePrompt(text6) {
|
|
@@ -9366,6 +9905,8 @@ function isRemoteStopCommand(text6) {
|
|
|
9366
9905
|
}
|
|
9367
9906
|
var handleChain2 = Promise.resolve();
|
|
9368
9907
|
var inboundGeneration = 0;
|
|
9908
|
+
var chatChains = /* @__PURE__ */ new Map();
|
|
9909
|
+
var chatGeneration = /* @__PURE__ */ new Map();
|
|
9369
9910
|
function enqueue(fn) {
|
|
9370
9911
|
const run2 = handleChain2.then(fn, fn);
|
|
9371
9912
|
handleChain2 = run2.then(
|
|
@@ -9387,10 +9928,340 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
|
9387
9928
|
return false;
|
|
9388
9929
|
}
|
|
9389
9930
|
}
|
|
9931
|
+
function bumpChat(chatId) {
|
|
9932
|
+
const next = (chatGeneration.get(chatId) ?? 0) + 1;
|
|
9933
|
+
chatGeneration.set(chatId, next);
|
|
9934
|
+
return next;
|
|
9935
|
+
}
|
|
9936
|
+
function enqueueChat(chatId, fn) {
|
|
9937
|
+
const prev = chatChains.get(chatId) ?? Promise.resolve();
|
|
9938
|
+
const run2 = prev.then(fn, fn);
|
|
9939
|
+
chatChains.set(
|
|
9940
|
+
chatId,
|
|
9941
|
+
run2.then(
|
|
9942
|
+
() => void 0,
|
|
9943
|
+
() => void 0
|
|
9944
|
+
)
|
|
9945
|
+
);
|
|
9946
|
+
}
|
|
9947
|
+
function emitControl(opts, reply) {
|
|
9948
|
+
opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
|
|
9949
|
+
}
|
|
9950
|
+
function interruptChat(chatId, log) {
|
|
9951
|
+
try {
|
|
9952
|
+
const thread = readPhoneThread(chatId);
|
|
9953
|
+
if (!thread) return false;
|
|
9954
|
+
if (thread.status !== "running" && thread.status !== "queued") return false;
|
|
9955
|
+
getOrchestrator().stop(thread.id, { clearQueue: true });
|
|
9956
|
+
log(`interrupt phone \u2192 chat ${thread.id.slice(0, 8)} (${thread.status})`);
|
|
9957
|
+
return true;
|
|
9958
|
+
} catch (err) {
|
|
9959
|
+
log(`interrupt chat: ${err instanceof Error ? err.message : String(err)}`);
|
|
9960
|
+
return false;
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
function emitChatTurn(opts, chatId, threadId) {
|
|
9964
|
+
const thread = readThread(threadId);
|
|
9965
|
+
if (!thread) {
|
|
9966
|
+
emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
|
|
9967
|
+
return;
|
|
9968
|
+
}
|
|
9969
|
+
if (thread.status === "stopped") {
|
|
9970
|
+
emitControl(opts, { op: "stopped", chatId });
|
|
9971
|
+
return;
|
|
9972
|
+
}
|
|
9973
|
+
const result = getOrchestrator().getTurnResult(thread.id);
|
|
9974
|
+
const pending = extractPendingPlanQuestions(
|
|
9975
|
+
[...thread.messages].reverse().find((m) => m.role === "agent")?.parts
|
|
9976
|
+
);
|
|
9977
|
+
if (result.taskState === "input-required" && pending) {
|
|
9978
|
+
emitControl(opts, {
|
|
9979
|
+
op: "ask",
|
|
9980
|
+
chatId,
|
|
9981
|
+
text: result.text.trim(),
|
|
9982
|
+
questions: pending.questions.map((q) => ({
|
|
9983
|
+
question: q.question,
|
|
9984
|
+
options: q.options.map(
|
|
9985
|
+
(o) => o.description ? { label: o.label, description: o.description } : { label: o.label }
|
|
9986
|
+
)
|
|
9987
|
+
}))
|
|
9988
|
+
});
|
|
9989
|
+
return;
|
|
9990
|
+
}
|
|
9991
|
+
const reply = outboundReplyFromTurn(result, {
|
|
9992
|
+
inputRequired: "Sideboard is waiting for an answer.",
|
|
9993
|
+
canceled: REMOTE_STOPPED_REPLY,
|
|
9994
|
+
completedEmpty: () => "",
|
|
9995
|
+
failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
|
|
9996
|
+
});
|
|
9997
|
+
emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
|
|
9998
|
+
}
|
|
9999
|
+
function phoneError(err) {
|
|
10000
|
+
return err instanceof Error ? err.message : String(err);
|
|
10001
|
+
}
|
|
10002
|
+
function deliverPhoneTurn(opts, chatId) {
|
|
10003
|
+
const thread = readPhoneThread(chatId);
|
|
10004
|
+
if (!thread || thread.status !== "running" && thread.status !== "queued") return;
|
|
10005
|
+
const generation = bumpChat(chatId);
|
|
10006
|
+
enqueueChat(chatId, async () => {
|
|
10007
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
10008
|
+
try {
|
|
10009
|
+
await getOrchestrator().waitForTurn(chatId, 14 * 60 * 1e3);
|
|
10010
|
+
} catch (err) {
|
|
10011
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
10012
|
+
emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${phoneError(err)}` });
|
|
10013
|
+
return;
|
|
10014
|
+
}
|
|
10015
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
10016
|
+
emitChatTurn(opts, chatId, chatId);
|
|
10017
|
+
});
|
|
10018
|
+
}
|
|
10019
|
+
function applyPhoneOptions(chatId, draft) {
|
|
10020
|
+
const patch = {};
|
|
10021
|
+
if (draft.agent) patch.agent = draft.agent;
|
|
10022
|
+
if ("model" in draft) patch.model = draft.model ?? null;
|
|
10023
|
+
if (draft.effort) patch.effort = draft.effort;
|
|
10024
|
+
if (draft.fast !== void 0) patch.fast = draft.fast;
|
|
10025
|
+
if (draft.planMode !== void 0) patch.planMode = draft.planMode;
|
|
10026
|
+
if (draft.autonomy) patch.autonomy = draft.autonomy;
|
|
10027
|
+
if (Object.keys(patch).length === 0) return;
|
|
10028
|
+
getOrchestrator().setThreadOptions(chatId, patch);
|
|
10029
|
+
}
|
|
10030
|
+
function stagePhoneTurnFiles(worktreePath, draft) {
|
|
10031
|
+
const links = phoneTurnAttachments({ links: draft.links });
|
|
10032
|
+
const files = stageBuffersAsAttachments(
|
|
10033
|
+
worktreePath,
|
|
10034
|
+
(draft.files ?? []).map((file) => ({ name: file.name, dataBase64: file.dataBase64 }))
|
|
10035
|
+
);
|
|
10036
|
+
return [...links, ...files];
|
|
10037
|
+
}
|
|
10038
|
+
function handlePhoneControl(cmd, opts) {
|
|
10039
|
+
const log = opts.onLog ?? (() => void 0);
|
|
10040
|
+
if (cmd.op === "list") {
|
|
10041
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
10042
|
+
return;
|
|
10043
|
+
}
|
|
10044
|
+
if (cmd.op === "history") {
|
|
10045
|
+
const page = listPhoneHistory({ query: cmd.query, after: cmd.after });
|
|
10046
|
+
emitControl(opts, {
|
|
10047
|
+
op: "history",
|
|
10048
|
+
chats: page.chats,
|
|
10049
|
+
total: page.total,
|
|
10050
|
+
...page.next ? { next: page.next } : {},
|
|
10051
|
+
...cmd.query ? { query: cmd.query } : {},
|
|
10052
|
+
...cmd.after ? { after: cmd.after } : {}
|
|
10053
|
+
});
|
|
10054
|
+
return;
|
|
10055
|
+
}
|
|
10056
|
+
if (cmd.op === "sources") {
|
|
10057
|
+
const repoPath = cmd.repoPath;
|
|
10058
|
+
const query = cmd.query;
|
|
10059
|
+
void listPhoneSources(repoPath, query).then(
|
|
10060
|
+
(page) => emitControl(opts, {
|
|
10061
|
+
op: "sources",
|
|
10062
|
+
repoPath,
|
|
10063
|
+
...query ? { query } : {},
|
|
10064
|
+
...page
|
|
10065
|
+
})
|
|
10066
|
+
).catch((err) => {
|
|
10067
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10068
|
+
});
|
|
10069
|
+
return;
|
|
10070
|
+
}
|
|
10071
|
+
if (cmd.op === "models") {
|
|
10072
|
+
const agent = cmd.agent;
|
|
10073
|
+
void listPhoneModels(agent).then((models) => emitControl(opts, { op: "models", agent, models })).catch((err) => {
|
|
10074
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10075
|
+
});
|
|
10076
|
+
return;
|
|
10077
|
+
}
|
|
10078
|
+
if (cmd.op === "dictate") {
|
|
10079
|
+
const id = cmd.id;
|
|
10080
|
+
const transcribe = opts.transcribeWav;
|
|
10081
|
+
if (!transcribe) {
|
|
10082
|
+
emitControl(opts, { op: "error", message: "Speech is not available on this Mac." });
|
|
10083
|
+
return;
|
|
10084
|
+
}
|
|
10085
|
+
void transcribe(cmd.audioBase64).then((text6) => emitControl(opts, { op: "dictated", id, text: text6.trim() })).catch((err) => {
|
|
10086
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10087
|
+
});
|
|
10088
|
+
return;
|
|
10089
|
+
}
|
|
10090
|
+
if (cmd.op === "options") {
|
|
10091
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
10092
|
+
if (!thread) {
|
|
10093
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
10094
|
+
return;
|
|
10095
|
+
}
|
|
10096
|
+
try {
|
|
10097
|
+
applyPhoneOptions(thread.id, cmd);
|
|
10098
|
+
const fresh = readPhoneThread(thread.id) ?? thread;
|
|
10099
|
+
emitControl(opts, { op: "options", chatId: fresh.id, ...phoneComposerOptions(fresh) });
|
|
10100
|
+
} catch (err) {
|
|
10101
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: phoneError(err) });
|
|
10102
|
+
}
|
|
10103
|
+
return;
|
|
10104
|
+
}
|
|
10105
|
+
if (cmd.op === "create") {
|
|
10106
|
+
if (cmd.where === "worktree") {
|
|
10107
|
+
void (async () => {
|
|
10108
|
+
try {
|
|
10109
|
+
const opened = createPhoneWorktreeAgent(cmd.chatId, cmd);
|
|
10110
|
+
const prompt = cmd.prompt?.trim() || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
|
|
10111
|
+
if (prompt) {
|
|
10112
|
+
try {
|
|
10113
|
+
await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
|
|
10114
|
+
} catch (err) {
|
|
10115
|
+
emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
|
|
10116
|
+
}
|
|
10117
|
+
}
|
|
10118
|
+
const fresh = openPhoneChat(opened.chat.id) ?? opened;
|
|
10119
|
+
emitControl(opts, { op: "opened", ...fresh });
|
|
10120
|
+
if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
|
|
10121
|
+
} catch (err) {
|
|
10122
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10123
|
+
}
|
|
10124
|
+
})();
|
|
10125
|
+
return;
|
|
10126
|
+
}
|
|
10127
|
+
if (cmd.where === "project") {
|
|
10128
|
+
void createPhoneProjectWorktree(cmd).then(async (opened) => {
|
|
10129
|
+
const prompt = resolveCreateFirstPrompt({
|
|
10130
|
+
sourceType: cmd.sourceType ?? "branch",
|
|
10131
|
+
prompt: cmd.prompt
|
|
10132
|
+
}) || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
|
|
10133
|
+
if (prompt) {
|
|
10134
|
+
try {
|
|
10135
|
+
await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
|
|
10136
|
+
} catch (err) {
|
|
10137
|
+
emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
|
|
10138
|
+
}
|
|
10139
|
+
}
|
|
10140
|
+
const fresh = openPhoneChat(opened.chat.id) ?? opened;
|
|
10141
|
+
emitControl(opts, { op: "opened", ...fresh });
|
|
10142
|
+
if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
|
|
10143
|
+
}).catch((err) => {
|
|
10144
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10145
|
+
});
|
|
10146
|
+
return;
|
|
10147
|
+
}
|
|
10148
|
+
const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
|
|
10149
|
+
if (goal || cmd.files?.length || cmd.links?.length) {
|
|
10150
|
+
void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
|
|
10151
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
10152
|
+
deliverPhoneTurn(opts, opened.chat.id);
|
|
10153
|
+
}).catch((err) => {
|
|
10154
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10155
|
+
});
|
|
10156
|
+
return;
|
|
10157
|
+
}
|
|
10158
|
+
try {
|
|
10159
|
+
const agent = coerceOrchestratorAgent(
|
|
10160
|
+
cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
|
|
10161
|
+
);
|
|
10162
|
+
emitControl(opts, { op: "opened", ...createPhoneChat(agent) });
|
|
10163
|
+
} catch (err) {
|
|
10164
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10165
|
+
}
|
|
10166
|
+
return;
|
|
10167
|
+
}
|
|
10168
|
+
if (cmd.op === "open") {
|
|
10169
|
+
const opened = openPhoneChat(cmd.chatId);
|
|
10170
|
+
if (!opened) {
|
|
10171
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
10172
|
+
return;
|
|
10173
|
+
}
|
|
10174
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
10175
|
+
return;
|
|
10176
|
+
}
|
|
10177
|
+
if (cmd.op === "archive") {
|
|
10178
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
10179
|
+
if (!thread) {
|
|
10180
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
10181
|
+
return;
|
|
10182
|
+
}
|
|
10183
|
+
bumpChat(thread.id);
|
|
10184
|
+
interruptChat(thread.id, log);
|
|
10185
|
+
archivePhoneChat(thread.id);
|
|
10186
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
10187
|
+
return;
|
|
10188
|
+
}
|
|
10189
|
+
if (cmd.op === "restore") {
|
|
10190
|
+
const chatId2 = cmd.chatId;
|
|
10191
|
+
void getOrchestrator().restore(chatId2).then(() => {
|
|
10192
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
10193
|
+
emitControl(opts, { op: "restored", chatId: chatId2 });
|
|
10194
|
+
}).catch((err) => {
|
|
10195
|
+
emitControl(opts, {
|
|
10196
|
+
op: "error",
|
|
10197
|
+
chatId: chatId2,
|
|
10198
|
+
message: err instanceof Error ? err.message : String(err)
|
|
10199
|
+
});
|
|
10200
|
+
});
|
|
10201
|
+
return;
|
|
10202
|
+
}
|
|
10203
|
+
if (cmd.op === "stop") {
|
|
10204
|
+
if (!readPhoneThread(cmd.chatId)) {
|
|
10205
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
10206
|
+
return;
|
|
10207
|
+
}
|
|
10208
|
+
bumpChat(cmd.chatId);
|
|
10209
|
+
interruptChat(cmd.chatId, log);
|
|
10210
|
+
emitControl(opts, { op: "stopped", chatId: cmd.chatId });
|
|
10211
|
+
return;
|
|
10212
|
+
}
|
|
10213
|
+
const chatId = cmd.chatId;
|
|
10214
|
+
const generation = bumpChat(chatId);
|
|
10215
|
+
interruptChat(chatId, log);
|
|
10216
|
+
enqueueChat(chatId, async () => {
|
|
10217
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
10218
|
+
const thread = readPhoneThread(chatId);
|
|
10219
|
+
if (!thread) {
|
|
10220
|
+
emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
|
|
10221
|
+
return;
|
|
10222
|
+
}
|
|
10223
|
+
if (thread.status === "running" || thread.status === "queued") {
|
|
10224
|
+
try {
|
|
10225
|
+
getOrchestrator().stop(thread.id, { clearQueue: true });
|
|
10226
|
+
} catch {
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10229
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
10230
|
+
try {
|
|
10231
|
+
try {
|
|
10232
|
+
applyPhoneOptions(thread.id, cmd);
|
|
10233
|
+
} catch (err) {
|
|
10234
|
+
emitControl(opts, { op: "error", chatId, message: phoneError(err) });
|
|
10235
|
+
}
|
|
10236
|
+
const live = readPhoneThread(thread.id) ?? thread;
|
|
10237
|
+
const staged = stagePhoneTurnFiles(live.worktreePath, cmd);
|
|
10238
|
+
const text6 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
|
|
10239
|
+
const formatted = formatRemotePrompt(text6);
|
|
10240
|
+
await getOrchestrator().send(thread.id, formatted, { attachments: staged });
|
|
10241
|
+
await getOrchestrator().waitForTurn(thread.id, 14 * 60 * 1e3);
|
|
10242
|
+
} catch (err) {
|
|
10243
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
10244
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10245
|
+
emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
|
|
10246
|
+
return;
|
|
10247
|
+
}
|
|
10248
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
10249
|
+
emitChatTurn(opts, chatId, thread.id);
|
|
10250
|
+
});
|
|
10251
|
+
}
|
|
9390
10252
|
function handleRemoteInbound(text6, opts) {
|
|
9391
|
-
const generation = ++inboundGeneration;
|
|
9392
10253
|
const log = opts.onLog ?? (() => void 0);
|
|
9393
10254
|
const body = text6.trim();
|
|
10255
|
+
const control = takePhoneControl(body);
|
|
10256
|
+
if (control) {
|
|
10257
|
+
if (control === "invalid") {
|
|
10258
|
+
emitControl(opts, { op: "error", message: "invalid phone command" });
|
|
10259
|
+
return;
|
|
10260
|
+
}
|
|
10261
|
+
handlePhoneControl(control, opts);
|
|
10262
|
+
return;
|
|
10263
|
+
}
|
|
10264
|
+
const generation = ++inboundGeneration;
|
|
9394
10265
|
if (body) interruptRemoteCoordinator(opts.deviceId, log);
|
|
9395
10266
|
enqueue(async () => {
|
|
9396
10267
|
if (generation !== inboundGeneration) return;
|
|
@@ -9505,7 +10376,8 @@ function runRemoteHost(opts) {
|
|
|
9505
10376
|
type: "host_register",
|
|
9506
10377
|
deviceId: opts.deviceId,
|
|
9507
10378
|
deviceLabel,
|
|
9508
|
-
hostSecret: opts.hostSecret
|
|
10379
|
+
hostSecret: opts.hostSecret,
|
|
10380
|
+
...opts.accountToken?.trim() ? { accountToken: opts.accountToken.trim() } : {}
|
|
9509
10381
|
});
|
|
9510
10382
|
};
|
|
9511
10383
|
const loop = async () => {
|
|
@@ -9621,7 +10493,8 @@ function runRemoteHost(opts) {
|
|
|
9621
10493
|
handleRemoteInbound(msg.text, {
|
|
9622
10494
|
deviceId: opts.deviceId,
|
|
9623
10495
|
onLog: log,
|
|
9624
|
-
onOutbound: emit
|
|
10496
|
+
onOutbound: emit,
|
|
10497
|
+
transcribeWav: opts.transcribeWav
|
|
9625
10498
|
});
|
|
9626
10499
|
}
|
|
9627
10500
|
};
|
|
@@ -9648,6 +10521,606 @@ function runRemoteHost(opts) {
|
|
|
9648
10521
|
};
|
|
9649
10522
|
}
|
|
9650
10523
|
|
|
10524
|
+
// src/relay/account-login.ts
|
|
10525
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
10526
|
+
|
|
10527
|
+
// src/relay/accounts.ts
|
|
10528
|
+
import { createHash as createHash2, randomBytes as randomBytes6, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
10529
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, realpathSync } from "fs";
|
|
10530
|
+
import { createRequire } from "module";
|
|
10531
|
+
import { dirname, join as join6 } from "path";
|
|
10532
|
+
var GITHUB_ACCOUNT_ISSUER = "https://github.com";
|
|
10533
|
+
var RELAY_DEVICE_CAP = 3;
|
|
10534
|
+
function thisModuleFile() {
|
|
10535
|
+
const cjsFile = typeof __filename !== "undefined" ? __filename : "";
|
|
10536
|
+
return cjsFile || process.argv[1] || join6(process.cwd(), "package.json");
|
|
10537
|
+
}
|
|
10538
|
+
function openDatabase(filePath) {
|
|
10539
|
+
const req = createRequire(thisModuleFile());
|
|
10540
|
+
const specifier = "node:sqlite";
|
|
10541
|
+
const { DatabaseSync: SqliteDatabase } = req(specifier);
|
|
10542
|
+
const db = new SqliteDatabase(filePath);
|
|
10543
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
10544
|
+
return db;
|
|
10545
|
+
}
|
|
10546
|
+
function sha256(value) {
|
|
10547
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
10548
|
+
}
|
|
10549
|
+
function sameHash(a, b) {
|
|
10550
|
+
const left = Buffer.from(a);
|
|
10551
|
+
const right = Buffer.from(b);
|
|
10552
|
+
return left.length === right.length && timingSafeEqual2(left, right);
|
|
10553
|
+
}
|
|
10554
|
+
function normalizeEmail(email) {
|
|
10555
|
+
const trimmed = email?.trim().toLowerCase() ?? "";
|
|
10556
|
+
return trimmed || null;
|
|
10557
|
+
}
|
|
10558
|
+
function longestMountPoint(dir, mounts) {
|
|
10559
|
+
let best = "/";
|
|
10560
|
+
for (const line of mounts.split("\n")) {
|
|
10561
|
+
const point = line.split(" ")[1]?.replace(/\\040/g, " ");
|
|
10562
|
+
if (!point) continue;
|
|
10563
|
+
if (dir === point || dir.startsWith(point.endsWith("/") ? point : `${point}/`)) {
|
|
10564
|
+
if (point.length > best.length) best = point;
|
|
10565
|
+
}
|
|
10566
|
+
}
|
|
10567
|
+
return best;
|
|
10568
|
+
}
|
|
10569
|
+
function assertRelayAccountsDurable(filePath) {
|
|
10570
|
+
if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
|
|
10571
|
+
if (process.env.NODE_ENV !== "production") return;
|
|
10572
|
+
if (process.platform !== "linux") return;
|
|
10573
|
+
const dir = realpathSync(dirname(filePath));
|
|
10574
|
+
const mounts = readFileSync5("/proc/mounts", "utf8");
|
|
10575
|
+
const point = longestMountPoint(dir, mounts);
|
|
10576
|
+
if (point === "/") {
|
|
10577
|
+
throw new Error(
|
|
10578
|
+
`Relay accounts at ${filePath} are on the machine root filesystem. Mount a Fly volume so sign-ins survive deploys.`
|
|
10579
|
+
);
|
|
10580
|
+
}
|
|
10581
|
+
}
|
|
10582
|
+
var RelayAccountStore = class {
|
|
10583
|
+
db;
|
|
10584
|
+
constructor(filePath) {
|
|
10585
|
+
mkdirSync4(dirname(filePath), { recursive: true });
|
|
10586
|
+
this.db = openDatabase(filePath);
|
|
10587
|
+
this.db.exec(`
|
|
10588
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
10589
|
+
id TEXT PRIMARY KEY,
|
|
10590
|
+
created_at INTEGER NOT NULL,
|
|
10591
|
+
device_cap INTEGER NOT NULL
|
|
10592
|
+
);
|
|
10593
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
10594
|
+
issuer TEXT NOT NULL,
|
|
10595
|
+
subject TEXT NOT NULL,
|
|
10596
|
+
account_id TEXT NOT NULL,
|
|
10597
|
+
email TEXT,
|
|
10598
|
+
email_verified INTEGER NOT NULL,
|
|
10599
|
+
PRIMARY KEY (issuer, subject)
|
|
10600
|
+
);
|
|
10601
|
+
CREATE INDEX IF NOT EXISTS identities_email ON identities(email);
|
|
10602
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
10603
|
+
token_hash TEXT PRIMARY KEY,
|
|
10604
|
+
account_id TEXT NOT NULL,
|
|
10605
|
+
created_at INTEGER NOT NULL
|
|
10606
|
+
);
|
|
10607
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
10608
|
+
device_id TEXT PRIMARY KEY,
|
|
10609
|
+
account_id TEXT NOT NULL,
|
|
10610
|
+
host_secret_hash TEXT NOT NULL,
|
|
10611
|
+
device_label TEXT NOT NULL,
|
|
10612
|
+
created_at INTEGER NOT NULL
|
|
10613
|
+
);
|
|
10614
|
+
`);
|
|
10615
|
+
}
|
|
10616
|
+
close() {
|
|
10617
|
+
this.db.close();
|
|
10618
|
+
}
|
|
10619
|
+
/**
|
|
10620
|
+
* First sign-in creates an account. The same issuer+subject returns that
|
|
10621
|
+
* account. A verified email match attaches a second provider. `linkAccountId`
|
|
10622
|
+
* attaches while that account’s credential is already signed in.
|
|
10623
|
+
* Each call mints a new Mac credential. Older credentials stay valid.
|
|
10624
|
+
*/
|
|
10625
|
+
signIn(identity, linkAccountId) {
|
|
10626
|
+
const issuer = identity.issuer.trim();
|
|
10627
|
+
const subject = identity.subject.trim();
|
|
10628
|
+
if (!issuer || !subject) throw new Error("Sign-in is missing an identity.");
|
|
10629
|
+
const email = normalizeEmail(identity.email);
|
|
10630
|
+
const verified = Boolean(identity.emailVerified && email);
|
|
10631
|
+
const link = linkAccountId?.trim() || null;
|
|
10632
|
+
this.db.exec("BEGIN");
|
|
10633
|
+
try {
|
|
10634
|
+
const existing = this.db.prepare("SELECT account_id, email FROM identities WHERE issuer = ? AND subject = ?").get(issuer, subject);
|
|
10635
|
+
let accountId;
|
|
10636
|
+
if (existing) {
|
|
10637
|
+
if (link && link !== existing.account_id) {
|
|
10638
|
+
throw new Error("That sign-in already belongs to another Sideboard account.");
|
|
10639
|
+
}
|
|
10640
|
+
accountId = existing.account_id;
|
|
10641
|
+
this.requireAccount(accountId);
|
|
10642
|
+
if (email) {
|
|
10643
|
+
this.db.prepare("UPDATE identities SET email = ?, email_verified = ? WHERE issuer = ? AND subject = ?").run(email, verified ? 1 : 0, issuer, subject);
|
|
10644
|
+
}
|
|
10645
|
+
} else if (link) {
|
|
10646
|
+
this.requireAccount(link);
|
|
10647
|
+
accountId = link;
|
|
10648
|
+
this.insertIdentity(issuer, subject, accountId, email, verified);
|
|
10649
|
+
} else {
|
|
10650
|
+
const byEmail = verified ? this.accountIdForVerifiedEmail(email) : void 0;
|
|
10651
|
+
accountId = byEmail ?? randomUUID8();
|
|
10652
|
+
if (!byEmail) {
|
|
10653
|
+
this.db.prepare("INSERT INTO accounts (id, created_at, device_cap) VALUES (?, ?, ?)").run(accountId, Date.now(), RELAY_DEVICE_CAP);
|
|
10654
|
+
}
|
|
10655
|
+
this.insertIdentity(issuer, subject, accountId, email, verified);
|
|
10656
|
+
}
|
|
10657
|
+
const credential = this.mintCredential(accountId);
|
|
10658
|
+
const stored = this.db.prepare("SELECT email FROM identities WHERE account_id = ? AND email IS NOT NULL ORDER BY email_verified DESC LIMIT 1").get(accountId);
|
|
10659
|
+
this.db.exec("COMMIT");
|
|
10660
|
+
return { accountId, credential, email: stored?.email ?? email };
|
|
10661
|
+
} catch (err) {
|
|
10662
|
+
this.db.exec("ROLLBACK");
|
|
10663
|
+
throw err;
|
|
10664
|
+
}
|
|
10665
|
+
}
|
|
10666
|
+
verifyCredential(token) {
|
|
10667
|
+
const trimmed = token.trim();
|
|
10668
|
+
if (!trimmed) return null;
|
|
10669
|
+
const row = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
|
|
10670
|
+
return row?.account_id ?? null;
|
|
10671
|
+
}
|
|
10672
|
+
/** Drop one Mac credential. The account and its other Macs stay. */
|
|
10673
|
+
logout(token) {
|
|
10674
|
+
const trimmed = token.trim();
|
|
10675
|
+
if (!trimmed) return;
|
|
10676
|
+
this.db.prepare("DELETE FROM credentials WHERE token_hash = ?").run(sha256(trimmed));
|
|
10677
|
+
}
|
|
10678
|
+
/** Delete the account and every identity, credential, and Mac bound to it. */
|
|
10679
|
+
revokeAccount(accountId) {
|
|
10680
|
+
const id = accountId.trim();
|
|
10681
|
+
if (!id) return;
|
|
10682
|
+
this.db.exec("BEGIN");
|
|
10683
|
+
try {
|
|
10684
|
+
this.db.prepare("DELETE FROM devices WHERE account_id = ?").run(id);
|
|
10685
|
+
this.db.prepare("DELETE FROM credentials WHERE account_id = ?").run(id);
|
|
10686
|
+
this.db.prepare("DELETE FROM identities WHERE account_id = ?").run(id);
|
|
10687
|
+
this.db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
|
|
10688
|
+
this.db.exec("COMMIT");
|
|
10689
|
+
} catch (err) {
|
|
10690
|
+
this.db.exec("ROLLBACK");
|
|
10691
|
+
throw err;
|
|
10692
|
+
}
|
|
10693
|
+
}
|
|
10694
|
+
/**
|
|
10695
|
+
* Accept `host_register` for a signed-in account. The device row survives
|
|
10696
|
+
* process restart, so a later register with a different host secret is rejected
|
|
10697
|
+
* and a new device id counts against the cap.
|
|
10698
|
+
*/
|
|
10699
|
+
authorizeHost(input) {
|
|
10700
|
+
const deviceId = input.deviceId.trim();
|
|
10701
|
+
const hostSecret = input.hostSecret.trim();
|
|
10702
|
+
const deviceLabel = input.deviceLabel.trim() || "This Mac";
|
|
10703
|
+
if (!deviceId || !hostSecret) return { ok: false, message: "sign in required" };
|
|
10704
|
+
const accountId = this.verifyCredential(input.accountToken);
|
|
10705
|
+
if (!accountId) return { ok: false, message: "sign in required" };
|
|
10706
|
+
this.db.exec("BEGIN");
|
|
10707
|
+
try {
|
|
10708
|
+
const account = this.db.prepare("SELECT id, device_cap FROM accounts WHERE id = ?").get(accountId);
|
|
10709
|
+
if (!account) {
|
|
10710
|
+
this.db.exec("ROLLBACK");
|
|
10711
|
+
return { ok: false, message: "sign in required" };
|
|
10712
|
+
}
|
|
10713
|
+
const device = this.db.prepare("SELECT device_id, account_id, host_secret_hash FROM devices WHERE device_id = ?").get(deviceId);
|
|
10714
|
+
const secretHash = sha256(hostSecret);
|
|
10715
|
+
if (!device) {
|
|
10716
|
+
const count = this.db.prepare("SELECT COUNT(*) AS n FROM devices WHERE account_id = ?").get(accountId);
|
|
10717
|
+
if (count.n >= account.device_cap) {
|
|
10718
|
+
this.db.exec("ROLLBACK");
|
|
10719
|
+
return { ok: false, message: "This account already has the maximum number of Macs." };
|
|
10720
|
+
}
|
|
10721
|
+
this.db.prepare(
|
|
10722
|
+
"INSERT INTO devices (device_id, account_id, host_secret_hash, device_label, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
10723
|
+
).run(deviceId, accountId, secretHash, deviceLabel, Date.now());
|
|
10724
|
+
this.db.exec("COMMIT");
|
|
10725
|
+
return { ok: true, accountId };
|
|
10726
|
+
}
|
|
10727
|
+
if (device.account_id !== accountId) {
|
|
10728
|
+
this.db.exec("ROLLBACK");
|
|
10729
|
+
return { ok: false, message: "This Mac is registered to another account." };
|
|
10730
|
+
}
|
|
10731
|
+
if (!sameHash(device.host_secret_hash, secretHash)) {
|
|
10732
|
+
this.db.exec("ROLLBACK");
|
|
10733
|
+
return { ok: false, message: "host secret rejected" };
|
|
10734
|
+
}
|
|
10735
|
+
this.db.prepare("UPDATE devices SET device_label = ? WHERE device_id = ?").run(deviceLabel, deviceId);
|
|
10736
|
+
this.db.exec("COMMIT");
|
|
10737
|
+
return { ok: true, accountId };
|
|
10738
|
+
} catch (err) {
|
|
10739
|
+
this.db.exec("ROLLBACK");
|
|
10740
|
+
throw err;
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
requireAccount(accountId) {
|
|
10744
|
+
const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
|
|
10745
|
+
if (!row) throw new Error("That Sideboard account is no longer on the relay.");
|
|
10746
|
+
}
|
|
10747
|
+
accountIdForVerifiedEmail(email) {
|
|
10748
|
+
const row = this.db.prepare(
|
|
10749
|
+
"SELECT account_id FROM identities WHERE email_verified = 1 AND email = ? LIMIT 1"
|
|
10750
|
+
).get(email);
|
|
10751
|
+
return row?.account_id;
|
|
10752
|
+
}
|
|
10753
|
+
insertIdentity(issuer, subject, accountId, email, verified) {
|
|
10754
|
+
this.db.prepare(
|
|
10755
|
+
"INSERT INTO identities (issuer, subject, account_id, email, email_verified) VALUES (?, ?, ?, ?, ?)"
|
|
10756
|
+
).run(issuer, subject, accountId, email, verified ? 1 : 0);
|
|
10757
|
+
}
|
|
10758
|
+
mintCredential(accountId) {
|
|
10759
|
+
const credential = randomBytes6(32).toString("hex");
|
|
10760
|
+
this.db.prepare("INSERT INTO credentials (token_hash, account_id, created_at) VALUES (?, ?, ?)").run(sha256(credential), accountId, Date.now());
|
|
10761
|
+
return credential;
|
|
10762
|
+
}
|
|
10763
|
+
};
|
|
10764
|
+
|
|
10765
|
+
// src/relay/account-oauth.ts
|
|
10766
|
+
var RELAY_GIT_HOSTS = ["github"];
|
|
10767
|
+
var REMOTE_LOGIN_PATH = "/remote/login";
|
|
10768
|
+
var REMOTE_OAUTH_CALLBACK_PATH = "/remote/oauth/callback";
|
|
10769
|
+
var REMOTE_OAUTH_RESULT_PATH = "/remote/oauth/result";
|
|
10770
|
+
var REMOTE_LOGOUT_PATH = "/remote/logout";
|
|
10771
|
+
var STATE_RE = /^[A-Za-z0-9_-]{16,128}$/;
|
|
10772
|
+
var LOGIN_TTL_MS = 10 * 60 * 1e3;
|
|
10773
|
+
var RelayLoginSessions = class {
|
|
10774
|
+
waiting = /* @__PURE__ */ new Map();
|
|
10775
|
+
results = /* @__PURE__ */ new Map();
|
|
10776
|
+
begin(state, provider, accountId) {
|
|
10777
|
+
this.sweep();
|
|
10778
|
+
this.waiting.set(state, { provider, accountId, expiresAt: Date.now() + LOGIN_TTL_MS });
|
|
10779
|
+
}
|
|
10780
|
+
takeLogin(state) {
|
|
10781
|
+
this.sweep();
|
|
10782
|
+
const row = this.waiting.get(state);
|
|
10783
|
+
if (!row) return null;
|
|
10784
|
+
this.waiting.delete(state);
|
|
10785
|
+
if (row.expiresAt < Date.now()) return null;
|
|
10786
|
+
return row;
|
|
10787
|
+
}
|
|
10788
|
+
putResult(state, value) {
|
|
10789
|
+
this.results.set(state, { value, expiresAt: Date.now() + LOGIN_TTL_MS });
|
|
10790
|
+
}
|
|
10791
|
+
/** `undefined` means the browser flow has not finished. */
|
|
10792
|
+
takeResult(state) {
|
|
10793
|
+
this.sweep();
|
|
10794
|
+
const row = this.results.get(state);
|
|
10795
|
+
if (!row) return this.waiting.has(state) ? void 0 : null;
|
|
10796
|
+
this.results.delete(state);
|
|
10797
|
+
if (row.expiresAt < Date.now()) return null;
|
|
10798
|
+
return row.value;
|
|
10799
|
+
}
|
|
10800
|
+
sweep() {
|
|
10801
|
+
const now = Date.now();
|
|
10802
|
+
for (const [state, row] of this.waiting) {
|
|
10803
|
+
if (row.expiresAt < now) this.waiting.delete(state);
|
|
10804
|
+
}
|
|
10805
|
+
for (const [state, row] of this.results) {
|
|
10806
|
+
if (row.expiresAt < now) this.results.delete(state);
|
|
10807
|
+
}
|
|
10808
|
+
}
|
|
10809
|
+
};
|
|
10810
|
+
var GIT_HOST_AUTH = {
|
|
10811
|
+
github: {
|
|
10812
|
+
label: "GitHub",
|
|
10813
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
10814
|
+
scope: "user:email",
|
|
10815
|
+
fetchIdentity: fetchGithubIdentity
|
|
10816
|
+
}
|
|
10817
|
+
};
|
|
10818
|
+
function providerOf(value) {
|
|
10819
|
+
return RELAY_GIT_HOSTS.includes(value) ? value : null;
|
|
10820
|
+
}
|
|
10821
|
+
function relayAuthorizeUrl(provider, client, redirectUri, state) {
|
|
10822
|
+
const host = GIT_HOST_AUTH[provider];
|
|
10823
|
+
const url = new URL(host.authorizeUrl);
|
|
10824
|
+
url.searchParams.set("client_id", client.clientId);
|
|
10825
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
10826
|
+
url.searchParams.set("scope", host.scope);
|
|
10827
|
+
url.searchParams.set("state", state);
|
|
10828
|
+
return url.toString();
|
|
10829
|
+
}
|
|
10830
|
+
async function readJson(res) {
|
|
10831
|
+
const text6 = await res.text();
|
|
10832
|
+
if (!text6) return {};
|
|
10833
|
+
try {
|
|
10834
|
+
return JSON.parse(text6);
|
|
10835
|
+
} catch {
|
|
10836
|
+
throw new Error("Sign-in provider returned a non-JSON response.");
|
|
10837
|
+
}
|
|
10838
|
+
}
|
|
10839
|
+
function accessToken(body) {
|
|
10840
|
+
const token = body.access_token;
|
|
10841
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
10842
|
+
const message = body.error_description;
|
|
10843
|
+
throw new Error(typeof message === "string" && message.trim() ? message : "Sign-in provider rejected the code.");
|
|
10844
|
+
}
|
|
10845
|
+
return token.trim();
|
|
10846
|
+
}
|
|
10847
|
+
async function fetchGithubIdentity(client, code, redirectUri, fetchImpl) {
|
|
10848
|
+
const tokenRes = await fetchImpl("https://github.com/login/oauth/access_token", {
|
|
10849
|
+
method: "POST",
|
|
10850
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
10851
|
+
body: JSON.stringify({
|
|
10852
|
+
client_id: client.clientId,
|
|
10853
|
+
client_secret: client.clientSecret,
|
|
10854
|
+
code,
|
|
10855
|
+
redirect_uri: redirectUri
|
|
10856
|
+
})
|
|
10857
|
+
});
|
|
10858
|
+
const token = accessToken(await readJson(tokenRes));
|
|
10859
|
+
const userRes = await fetchImpl("https://api.github.com/user", {
|
|
10860
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
|
|
10861
|
+
});
|
|
10862
|
+
const user = await readJson(userRes);
|
|
10863
|
+
const subject = typeof user.id === "number" ? String(user.id) : "";
|
|
10864
|
+
if (!subject) throw new Error("GitHub did not return an account id.");
|
|
10865
|
+
const emailRes = await fetchImpl("https://api.github.com/user/emails", {
|
|
10866
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
|
|
10867
|
+
});
|
|
10868
|
+
const emails = await readJson(emailRes);
|
|
10869
|
+
const list = Array.isArray(emails) ? emails : [];
|
|
10870
|
+
const primary = list.find((row) => row.primary === true && row.verified === true);
|
|
10871
|
+
const anyVerified = list.find((row) => row.verified === true);
|
|
10872
|
+
const chosen = primary ?? anyVerified;
|
|
10873
|
+
const email = chosen && typeof chosen.email === "string" ? chosen.email : null;
|
|
10874
|
+
return { issuer: GITHUB_ACCOUNT_ISSUER, subject, email, emailVerified: Boolean(email) };
|
|
10875
|
+
}
|
|
10876
|
+
async function fetchRelayIdentity(provider, client, code, redirectUri, fetchImpl) {
|
|
10877
|
+
return GIT_HOST_AUTH[provider].fetchIdentity(client, code, redirectUri, fetchImpl);
|
|
10878
|
+
}
|
|
10879
|
+
async function readBody(req) {
|
|
10880
|
+
const chunks = [];
|
|
10881
|
+
let size = 0;
|
|
10882
|
+
for await (const chunk of req) {
|
|
10883
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
10884
|
+
size += buf.length;
|
|
10885
|
+
if (size > 8192) throw new Error("body too large");
|
|
10886
|
+
chunks.push(buf);
|
|
10887
|
+
}
|
|
10888
|
+
const text6 = Buffer.concat(chunks).toString("utf8").trim();
|
|
10889
|
+
if (!text6) return {};
|
|
10890
|
+
return JSON.parse(text6);
|
|
10891
|
+
}
|
|
10892
|
+
function sendJson2(res, status, body) {
|
|
10893
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
10894
|
+
res.end(JSON.stringify(body));
|
|
10895
|
+
}
|
|
10896
|
+
function sendHtml(res, status, title, body) {
|
|
10897
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
10898
|
+
res.end(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body>${body}</body></html>`);
|
|
10899
|
+
}
|
|
10900
|
+
async function handleRelayAccountHttp(opts) {
|
|
10901
|
+
const reqUrl = opts.req.url || "/";
|
|
10902
|
+
const path2 = reqUrl.split("?")[0];
|
|
10903
|
+
const method = opts.req.method || "GET";
|
|
10904
|
+
const origin = opts.publicOrigin.replace(/\/+$/, "");
|
|
10905
|
+
const redirectUri = `${origin}${REMOTE_OAUTH_CALLBACK_PATH}`;
|
|
10906
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
10907
|
+
if (method === "POST" && path2 === REMOTE_LOGIN_PATH) {
|
|
10908
|
+
let body;
|
|
10909
|
+
try {
|
|
10910
|
+
body = await readBody(opts.req);
|
|
10911
|
+
} catch {
|
|
10912
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
|
|
10913
|
+
return true;
|
|
10914
|
+
}
|
|
10915
|
+
const provider = providerOf(typeof body.provider === "string" ? body.provider : "");
|
|
10916
|
+
const state = typeof body.state === "string" ? body.state.trim() : "";
|
|
10917
|
+
if (!provider || !STATE_RE.test(state)) {
|
|
10918
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
|
|
10919
|
+
return true;
|
|
10920
|
+
}
|
|
10921
|
+
const client = opts.oauth[provider];
|
|
10922
|
+
if (!client?.clientId || !client.clientSecret) {
|
|
10923
|
+
sendJson2(opts.res, 503, {
|
|
10924
|
+
ok: false,
|
|
10925
|
+
error: `${GIT_HOST_AUTH[provider].label} sign-in is not configured on this relay.`
|
|
10926
|
+
});
|
|
10927
|
+
return true;
|
|
10928
|
+
}
|
|
10929
|
+
let accountId = null;
|
|
10930
|
+
const linkToken = typeof body.accountToken === "string" ? body.accountToken.trim() : "";
|
|
10931
|
+
if (linkToken) {
|
|
10932
|
+
accountId = opts.store.verifyCredential(linkToken);
|
|
10933
|
+
if (!accountId) {
|
|
10934
|
+
sendJson2(opts.res, 401, { ok: false, error: "sign in required" });
|
|
10935
|
+
return true;
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10938
|
+
opts.sessions.begin(state, provider, accountId);
|
|
10939
|
+
sendJson2(opts.res, 200, { ok: true, url: relayAuthorizeUrl(provider, client, redirectUri, state) });
|
|
10940
|
+
return true;
|
|
10941
|
+
}
|
|
10942
|
+
if (method === "GET" && path2 === REMOTE_OAUTH_CALLBACK_PATH) {
|
|
10943
|
+
const url = new URL(reqUrl, origin);
|
|
10944
|
+
const state = url.searchParams.get("state")?.trim() ?? "";
|
|
10945
|
+
const code = url.searchParams.get("code")?.trim() ?? "";
|
|
10946
|
+
const providerError = url.searchParams.get("error")?.trim() ?? "";
|
|
10947
|
+
const pending = STATE_RE.test(state) ? opts.sessions.takeLogin(state) : null;
|
|
10948
|
+
if (!pending) {
|
|
10949
|
+
sendHtml(opts.res, 400, "Sideboard", "<h1>Sign-in expired</h1><p>Return to Sideboard and try again.</p>");
|
|
10950
|
+
return true;
|
|
10951
|
+
}
|
|
10952
|
+
if (providerError || !code) {
|
|
10953
|
+
const message = providerError || "Sign-in was cancelled.";
|
|
10954
|
+
opts.sessions.putResult(state, { ok: false, error: message });
|
|
10955
|
+
sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml(message)}</p>`);
|
|
10956
|
+
return true;
|
|
10957
|
+
}
|
|
10958
|
+
const client = opts.oauth[pending.provider];
|
|
10959
|
+
if (!client) {
|
|
10960
|
+
opts.sessions.putResult(state, { ok: false, error: "Sign-in is not configured on this relay." });
|
|
10961
|
+
sendHtml(opts.res, 503, "Sideboard", "<h1>Sign-in is not configured</h1>");
|
|
10962
|
+
return true;
|
|
10963
|
+
}
|
|
10964
|
+
try {
|
|
10965
|
+
const identity = await fetchRelayIdentity(pending.provider, client, code, redirectUri, fetchImpl);
|
|
10966
|
+
const signed = opts.store.signIn(identity, pending.accountId);
|
|
10967
|
+
opts.sessions.putResult(state, {
|
|
10968
|
+
ok: true,
|
|
10969
|
+
credential: signed.credential,
|
|
10970
|
+
email: signed.email,
|
|
10971
|
+
accountId: signed.accountId
|
|
10972
|
+
});
|
|
10973
|
+
sendHtml(
|
|
10974
|
+
opts.res,
|
|
10975
|
+
200,
|
|
10976
|
+
"Sideboard",
|
|
10977
|
+
"<h1>Signed in</h1><p>Return to Sideboard. This window can close.</p>"
|
|
10978
|
+
);
|
|
10979
|
+
} catch (err) {
|
|
10980
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10981
|
+
opts.sessions.putResult(state, { ok: false, error: message });
|
|
10982
|
+
sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml(message)}</p>`);
|
|
10983
|
+
}
|
|
10984
|
+
return true;
|
|
10985
|
+
}
|
|
10986
|
+
if (method === "GET" && path2 === REMOTE_OAUTH_RESULT_PATH) {
|
|
10987
|
+
const url = new URL(reqUrl, origin);
|
|
10988
|
+
const state = url.searchParams.get("state")?.trim() ?? "";
|
|
10989
|
+
if (!STATE_RE.test(state)) {
|
|
10990
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
|
|
10991
|
+
return true;
|
|
10992
|
+
}
|
|
10993
|
+
const result = opts.sessions.takeResult(state);
|
|
10994
|
+
if (result === void 0 || result === null) {
|
|
10995
|
+
sendJson2(opts.res, 404, { ok: false, error: "pending" });
|
|
10996
|
+
return true;
|
|
10997
|
+
}
|
|
10998
|
+
if (!result.ok) {
|
|
10999
|
+
sendJson2(opts.res, 400, { ok: false, error: result.error });
|
|
11000
|
+
return true;
|
|
11001
|
+
}
|
|
11002
|
+
sendJson2(opts.res, 200, {
|
|
11003
|
+
ok: true,
|
|
11004
|
+
credential: result.credential,
|
|
11005
|
+
email: result.email,
|
|
11006
|
+
accountId: result.accountId
|
|
11007
|
+
});
|
|
11008
|
+
return true;
|
|
11009
|
+
}
|
|
11010
|
+
if (method === "POST" && path2 === REMOTE_LOGOUT_PATH) {
|
|
11011
|
+
let body;
|
|
11012
|
+
try {
|
|
11013
|
+
body = await readBody(opts.req);
|
|
11014
|
+
} catch {
|
|
11015
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid logout request" });
|
|
11016
|
+
return true;
|
|
11017
|
+
}
|
|
11018
|
+
const token = typeof body.accountToken === "string" ? body.accountToken : "";
|
|
11019
|
+
opts.store.logout(token);
|
|
11020
|
+
sendJson2(opts.res, 200, { ok: true });
|
|
11021
|
+
return true;
|
|
11022
|
+
}
|
|
11023
|
+
return false;
|
|
11024
|
+
}
|
|
11025
|
+
|
|
11026
|
+
// src/relay/account-login.ts
|
|
11027
|
+
var RemoteAccountLoginCancelled = class extends Error {
|
|
11028
|
+
constructor() {
|
|
11029
|
+
super("Sign-in cancelled");
|
|
11030
|
+
this.name = "RemoteAccountLoginCancelled";
|
|
11031
|
+
}
|
|
11032
|
+
};
|
|
11033
|
+
function isRemoteAccountLoginCancelled(err) {
|
|
11034
|
+
return err instanceof RemoteAccountLoginCancelled;
|
|
11035
|
+
}
|
|
11036
|
+
function remoteAccountHttpOrigin(wsUrl = remoteRelayUrl()) {
|
|
11037
|
+
const url = new URL(wsUrl);
|
|
11038
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
11039
|
+
url.pathname = "";
|
|
11040
|
+
url.search = "";
|
|
11041
|
+
url.hash = "";
|
|
11042
|
+
return url.origin;
|
|
11043
|
+
}
|
|
11044
|
+
function sleep2(ms, signal) {
|
|
11045
|
+
return new Promise((resolve, reject) => {
|
|
11046
|
+
if (signal?.aborted) {
|
|
11047
|
+
reject(new RemoteAccountLoginCancelled());
|
|
11048
|
+
return;
|
|
11049
|
+
}
|
|
11050
|
+
const timer = setTimeout(() => {
|
|
11051
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11052
|
+
resolve();
|
|
11053
|
+
}, ms);
|
|
11054
|
+
const onAbort = () => {
|
|
11055
|
+
clearTimeout(timer);
|
|
11056
|
+
reject(new RemoteAccountLoginCancelled());
|
|
11057
|
+
};
|
|
11058
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
11059
|
+
});
|
|
11060
|
+
}
|
|
11061
|
+
async function startRemoteAccountLogin(opts) {
|
|
11062
|
+
if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
|
|
11063
|
+
const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
|
|
11064
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
11065
|
+
const state = randomBytes7(16).toString("hex");
|
|
11066
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
11067
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 400;
|
|
11068
|
+
const linkToken = opts.linkToken?.trim() ?? "";
|
|
11069
|
+
const started = await fetchImpl(`${origin}${REMOTE_LOGIN_PATH}`, {
|
|
11070
|
+
method: "POST",
|
|
11071
|
+
headers: { "Content-Type": "application/json" },
|
|
11072
|
+
body: JSON.stringify({
|
|
11073
|
+
provider: opts.provider,
|
|
11074
|
+
state,
|
|
11075
|
+
...linkToken ? { accountToken: linkToken } : {}
|
|
11076
|
+
})
|
|
11077
|
+
});
|
|
11078
|
+
const startBody = await started.json();
|
|
11079
|
+
if (!started.ok || !startBody.url) {
|
|
11080
|
+
throw new Error(startBody.error || "Could not start sign-in.");
|
|
11081
|
+
}
|
|
11082
|
+
await Promise.resolve(opts.openUrl?.(startBody.url));
|
|
11083
|
+
const deadline = Date.now() + timeoutMs;
|
|
11084
|
+
const resultUrl = `${origin}${REMOTE_OAUTH_RESULT_PATH}?state=${encodeURIComponent(state)}`;
|
|
11085
|
+
while (Date.now() < deadline) {
|
|
11086
|
+
if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
|
|
11087
|
+
let res;
|
|
11088
|
+
try {
|
|
11089
|
+
res = await fetchImpl(resultUrl);
|
|
11090
|
+
} catch {
|
|
11091
|
+
if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
|
|
11092
|
+
await sleep2(pollIntervalMs, opts.signal);
|
|
11093
|
+
continue;
|
|
11094
|
+
}
|
|
11095
|
+
let data;
|
|
11096
|
+
try {
|
|
11097
|
+
data = await res.json();
|
|
11098
|
+
} catch {
|
|
11099
|
+
await sleep2(pollIntervalMs, opts.signal);
|
|
11100
|
+
continue;
|
|
11101
|
+
}
|
|
11102
|
+
if (data.ok && data.credential && data.accountId) {
|
|
11103
|
+
return {
|
|
11104
|
+
credential: data.credential,
|
|
11105
|
+
email: data.email ?? null,
|
|
11106
|
+
accountId: data.accountId
|
|
11107
|
+
};
|
|
11108
|
+
}
|
|
11109
|
+
if (data.error && data.error !== "pending") throw new Error(data.error);
|
|
11110
|
+
await sleep2(pollIntervalMs, opts.signal);
|
|
11111
|
+
}
|
|
11112
|
+
throw new Error("Sign-in timed out.");
|
|
11113
|
+
}
|
|
11114
|
+
async function disconnectRemoteAccount(opts) {
|
|
11115
|
+
const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
|
|
11116
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
11117
|
+
await fetchImpl(`${origin}${REMOTE_LOGOUT_PATH}`, {
|
|
11118
|
+
method: "POST",
|
|
11119
|
+
headers: { "Content-Type": "application/json" },
|
|
11120
|
+
body: JSON.stringify({ accountToken: opts.accountToken })
|
|
11121
|
+
});
|
|
11122
|
+
}
|
|
11123
|
+
|
|
9651
11124
|
// src/slack/app-home.ts
|
|
9652
11125
|
var SLACK_APP_HOME_DOWNLOAD = "https://sideboard-downloads.t3.tigrisfiles.io/Sideboard-latest-arm64.dmg";
|
|
9653
11126
|
var SLACK_APP_HOME_LANDING = "https://www.sideboard.cloud/slack/";
|
|
@@ -10104,14 +11577,19 @@ async function tryServeStatic(req, res, root) {
|
|
|
10104
11577
|
}
|
|
10105
11578
|
|
|
10106
11579
|
// src/relay/server.ts
|
|
10107
|
-
function
|
|
11580
|
+
function sendHtml2(res, status, title, body) {
|
|
10108
11581
|
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
10109
11582
|
res.end(slackOAuthHtmlPage(title, body));
|
|
10110
11583
|
}
|
|
10111
|
-
function
|
|
11584
|
+
function sendJson3(res, status, body) {
|
|
10112
11585
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
10113
11586
|
res.end(JSON.stringify(body));
|
|
10114
11587
|
}
|
|
11588
|
+
function relayClientIp(req) {
|
|
11589
|
+
const fly = req.headers["fly-client-ip"];
|
|
11590
|
+
if (typeof fly === "string" && fly.trim()) return fly.trim();
|
|
11591
|
+
return req.socket.remoteAddress ?? "unknown";
|
|
11592
|
+
}
|
|
10115
11593
|
function startRelaySocketPing(ws, intervalMs) {
|
|
10116
11594
|
const ping = setInterval(() => {
|
|
10117
11595
|
if (ws.readyState !== ws.OPEN) {
|
|
@@ -10139,11 +11617,26 @@ async function startRelayServer(opts) {
|
|
|
10139
11617
|
const clientSecret = (opts.clientSecret ?? process.env.SIDEBOARD_SLACK_CLIENT_SECRET ?? "").trim();
|
|
10140
11618
|
const redirectUri = (opts.oauthRedirectUri ?? slackOAuthRelayRedirectUri()).trim();
|
|
10141
11619
|
const pending = new SlackOAuthPendingStore();
|
|
11620
|
+
let boundPort = 0;
|
|
10142
11621
|
const hub = opts.hub ?? new SlackRelayHub({
|
|
10143
11622
|
fetchImpl: opts.fetchImpl ?? fetch,
|
|
10144
11623
|
onLog: log
|
|
10145
11624
|
});
|
|
10146
|
-
const
|
|
11625
|
+
const accountsPath = opts.accountsPath?.trim() ?? "";
|
|
11626
|
+
if (accountsPath) assertRelayAccountsDurable(accountsPath);
|
|
11627
|
+
const accountStore = accountsPath ? new RelayAccountStore(accountsPath) : null;
|
|
11628
|
+
const accountSessions = new RelayLoginSessions();
|
|
11629
|
+
const relayOAuth = opts.relayOAuth ?? {};
|
|
11630
|
+
const authMarks = /* @__PURE__ */ new Map();
|
|
11631
|
+
const remoteByIp = /* @__PURE__ */ new Map();
|
|
11632
|
+
let remoteSockets = 0;
|
|
11633
|
+
const remoteHub = new RemoteHub({
|
|
11634
|
+
authorizeHost: accountStore ? (input) => {
|
|
11635
|
+
const decision = accountStore.authorizeHost(input);
|
|
11636
|
+
return decision.ok ? { ok: true } : { ok: false, message: decision.message };
|
|
11637
|
+
} : void 0,
|
|
11638
|
+
onAuthorized: (socket) => authMarks.get(socket)?.()
|
|
11639
|
+
});
|
|
10147
11640
|
const handleCallback = async (reqUrl, res) => {
|
|
10148
11641
|
const url2 = parseSlackOAuthCallbackUrl(reqUrl);
|
|
10149
11642
|
if (!url2) return false;
|
|
@@ -10152,7 +11645,7 @@ async function startRelayServer(opts) {
|
|
|
10152
11645
|
const code = url2.searchParams.get("code")?.trim() ?? "";
|
|
10153
11646
|
if (error) {
|
|
10154
11647
|
if (state) pending.put(state, { ok: false, error: `Slack OAuth: ${error}` });
|
|
10155
|
-
|
|
11648
|
+
sendHtml2(
|
|
10156
11649
|
res,
|
|
10157
11650
|
400,
|
|
10158
11651
|
"Slack",
|
|
@@ -10161,7 +11654,7 @@ async function startRelayServer(opts) {
|
|
|
10161
11654
|
return true;
|
|
10162
11655
|
}
|
|
10163
11656
|
if (!state || !code) {
|
|
10164
|
-
|
|
11657
|
+
sendHtml2(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
|
|
10165
11658
|
return true;
|
|
10166
11659
|
}
|
|
10167
11660
|
if (!clientSecret) {
|
|
@@ -10169,7 +11662,7 @@ async function startRelayServer(opts) {
|
|
|
10169
11662
|
ok: false,
|
|
10170
11663
|
error: "Relay is missing SIDEBOARD_SLACK_CLIENT_SECRET"
|
|
10171
11664
|
});
|
|
10172
|
-
|
|
11665
|
+
sendHtml2(
|
|
10173
11666
|
res,
|
|
10174
11667
|
500,
|
|
10175
11668
|
"Slack",
|
|
@@ -10192,7 +11685,7 @@ async function startRelayServer(opts) {
|
|
|
10192
11685
|
void hub.handleAppHomeOpened(payload.team_id, payload.user_id);
|
|
10193
11686
|
}
|
|
10194
11687
|
}
|
|
10195
|
-
|
|
11688
|
+
sendHtml2(
|
|
10196
11689
|
res,
|
|
10197
11690
|
200,
|
|
10198
11691
|
"Slack connected",
|
|
@@ -10201,7 +11694,7 @@ async function startRelayServer(opts) {
|
|
|
10201
11694
|
} catch (err) {
|
|
10202
11695
|
const message = err instanceof Error ? err.message : String(err);
|
|
10203
11696
|
pending.put(state, { ok: false, error: message });
|
|
10204
|
-
|
|
11697
|
+
sendHtml2(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml(message)}</p>`);
|
|
10205
11698
|
}
|
|
10206
11699
|
return true;
|
|
10207
11700
|
};
|
|
@@ -10211,19 +11704,30 @@ async function startRelayServer(opts) {
|
|
|
10211
11704
|
const state = url2.searchParams.get("state")?.trim() ?? "";
|
|
10212
11705
|
const value = pending.take(state);
|
|
10213
11706
|
if (!value) {
|
|
10214
|
-
|
|
11707
|
+
sendJson3(res, 404, { ok: false, error: "pending" });
|
|
10215
11708
|
return true;
|
|
10216
11709
|
}
|
|
10217
11710
|
if (!value.ok) {
|
|
10218
|
-
|
|
11711
|
+
sendJson3(res, 400, { ok: false, error: value.error });
|
|
10219
11712
|
return true;
|
|
10220
11713
|
}
|
|
10221
|
-
|
|
11714
|
+
sendJson3(res, 200, { ok: true, ...value.payload });
|
|
10222
11715
|
return true;
|
|
10223
11716
|
};
|
|
10224
11717
|
const httpServer = createServer3((req, res) => {
|
|
10225
11718
|
void (async () => {
|
|
10226
11719
|
const reqUrl = req.url || "/";
|
|
11720
|
+
if (accountStore && await handleRelayAccountHttp({
|
|
11721
|
+
req,
|
|
11722
|
+
res,
|
|
11723
|
+
store: accountStore,
|
|
11724
|
+
oauth: relayOAuth,
|
|
11725
|
+
sessions: accountSessions,
|
|
11726
|
+
publicOrigin: opts.relayPublicOrigin?.trim() || process.env.SIDEBOARD_RELAY_PUBLIC_ORIGIN?.trim() || `http://127.0.0.1:${boundPort}`,
|
|
11727
|
+
fetchImpl: opts.fetchImpl
|
|
11728
|
+
})) {
|
|
11729
|
+
return;
|
|
11730
|
+
}
|
|
10227
11731
|
if (await handleCallback(reqUrl, res)) return;
|
|
10228
11732
|
if (handleResult(reqUrl, res)) return;
|
|
10229
11733
|
const ableTimeBounce = ableTimeOAuthLocalBounceUrl(reqUrl);
|
|
@@ -10237,7 +11741,7 @@ async function startRelayServer(opts) {
|
|
|
10237
11741
|
return;
|
|
10238
11742
|
}
|
|
10239
11743
|
if (req.url === "/health") {
|
|
10240
|
-
|
|
11744
|
+
sendJson3(res, 200, {
|
|
10241
11745
|
ok: true,
|
|
10242
11746
|
service: "sideboard-relay",
|
|
10243
11747
|
sessions: hub.listSessions().length,
|
|
@@ -10256,7 +11760,7 @@ async function startRelayServer(opts) {
|
|
|
10256
11760
|
return;
|
|
10257
11761
|
}
|
|
10258
11762
|
if (req.url === "/") {
|
|
10259
|
-
|
|
11763
|
+
sendJson3(res, 200, {
|
|
10260
11764
|
ok: true,
|
|
10261
11765
|
service: "sideboard-relay",
|
|
10262
11766
|
sessions: hub.listSessions().length,
|
|
@@ -10283,6 +11787,14 @@ async function startRelayServer(opts) {
|
|
|
10283
11787
|
socket.destroy();
|
|
10284
11788
|
return;
|
|
10285
11789
|
}
|
|
11790
|
+
if (target === remoteWss) {
|
|
11791
|
+
const ip = relayClientIp(req);
|
|
11792
|
+
const count = remoteByIp.get(ip) ?? 0;
|
|
11793
|
+
if (count >= 20 || remoteSockets >= 4e3) {
|
|
11794
|
+
socket.destroy();
|
|
11795
|
+
return;
|
|
11796
|
+
}
|
|
11797
|
+
}
|
|
10286
11798
|
target.handleUpgrade(req, socket, head, (ws) => {
|
|
10287
11799
|
target.emit("connection", ws, req);
|
|
10288
11800
|
});
|
|
@@ -10317,7 +11829,10 @@ async function startRelayServer(opts) {
|
|
|
10317
11829
|
hub.detachSocket(socket);
|
|
10318
11830
|
});
|
|
10319
11831
|
});
|
|
10320
|
-
remoteWss.on("connection", (ws) => {
|
|
11832
|
+
remoteWss.on("connection", (ws, req) => {
|
|
11833
|
+
const ip = relayClientIp(req);
|
|
11834
|
+
remoteByIp.set(ip, (remoteByIp.get(ip) ?? 0) + 1);
|
|
11835
|
+
remoteSockets += 1;
|
|
10321
11836
|
const socket = {
|
|
10322
11837
|
send: (data) => {
|
|
10323
11838
|
if (ws.readyState === ws.OPEN) ws.send(data);
|
|
@@ -10329,14 +11844,35 @@ async function startRelayServer(opts) {
|
|
|
10329
11844
|
}
|
|
10330
11845
|
}
|
|
10331
11846
|
};
|
|
11847
|
+
let released = false;
|
|
11848
|
+
const authTimer = accountStore ? setTimeout(() => {
|
|
11849
|
+
try {
|
|
11850
|
+
ws.close(4001, "sign in required");
|
|
11851
|
+
} catch {
|
|
11852
|
+
}
|
|
11853
|
+
}, 12e4) : null;
|
|
11854
|
+
authMarks.set(socket, () => {
|
|
11855
|
+
if (authTimer) clearTimeout(authTimer);
|
|
11856
|
+
});
|
|
11857
|
+
const release = () => {
|
|
11858
|
+
if (released) return;
|
|
11859
|
+
released = true;
|
|
11860
|
+
if (authTimer) clearTimeout(authTimer);
|
|
11861
|
+
authMarks.delete(socket);
|
|
11862
|
+
remoteSockets -= 1;
|
|
11863
|
+
const next = (remoteByIp.get(ip) ?? 1) - 1;
|
|
11864
|
+
if (next <= 0) remoteByIp.delete(ip);
|
|
11865
|
+
else remoteByIp.set(ip, next);
|
|
11866
|
+
remoteHub.detachSocket(socket);
|
|
11867
|
+
};
|
|
10332
11868
|
startRelaySocketPing(ws, REMOTE_PING_INTERVAL_MS);
|
|
10333
11869
|
log("remote client connected");
|
|
10334
11870
|
ws.on("message", (data) => {
|
|
10335
11871
|
const raw = typeof data === "string" ? data : data.toString("utf8");
|
|
10336
11872
|
remoteHub.handleClientMessage(socket, raw);
|
|
10337
11873
|
});
|
|
10338
|
-
ws.on("close",
|
|
10339
|
-
ws.on("error",
|
|
11874
|
+
ws.on("close", release);
|
|
11875
|
+
ws.on("error", release);
|
|
10340
11876
|
});
|
|
10341
11877
|
const port = opts.port ?? 0;
|
|
10342
11878
|
const host = opts.host ?? "0.0.0.0";
|
|
@@ -10345,7 +11881,7 @@ async function startRelayServer(opts) {
|
|
|
10345
11881
|
httpServer.listen(port, host, () => resolve());
|
|
10346
11882
|
});
|
|
10347
11883
|
const address = httpServer.address();
|
|
10348
|
-
|
|
11884
|
+
boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
|
|
10349
11885
|
const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${SLACK_RELAY_DESKTOP_PATH}`;
|
|
10350
11886
|
log(`relay listening on ${url}`);
|
|
10351
11887
|
const ac = new AbortController();
|
|
@@ -10388,13 +11924,14 @@ async function startRelayServer(opts) {
|
|
|
10388
11924
|
httpServer.close((err) => err ? reject(err) : resolve());
|
|
10389
11925
|
});
|
|
10390
11926
|
await socketModeDone.catch(() => void 0);
|
|
11927
|
+
accountStore?.close();
|
|
10391
11928
|
}
|
|
10392
11929
|
};
|
|
10393
11930
|
}
|
|
10394
11931
|
|
|
10395
11932
|
// src/slack/outbound-watch.ts
|
|
10396
|
-
import { existsSync as existsSync5, readFileSync as
|
|
10397
|
-
import { join as
|
|
11933
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
11934
|
+
import { join as join7 } from "path";
|
|
10398
11935
|
var MAX_WATCHES = 40;
|
|
10399
11936
|
var MAX_REPLIES_PER_WATCH = 30;
|
|
10400
11937
|
var WATCH_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -10402,7 +11939,7 @@ var POLL_INTERVAL_MS = 12e3;
|
|
|
10402
11939
|
var lastPollMs = 0;
|
|
10403
11940
|
var nameCache = /* @__PURE__ */ new Map();
|
|
10404
11941
|
function storePath3() {
|
|
10405
|
-
return
|
|
11942
|
+
return join7(appDataDir(), "slack-outbound-watch.json");
|
|
10406
11943
|
}
|
|
10407
11944
|
function watchId(teamId, channelId, ts) {
|
|
10408
11945
|
return `${teamId}:${channelId}:${ts}`;
|
|
@@ -10417,7 +11954,7 @@ function readStore3() {
|
|
|
10417
11954
|
const path2 = storePath3();
|
|
10418
11955
|
if (!existsSync5(path2)) return [];
|
|
10419
11956
|
try {
|
|
10420
|
-
const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse(
|
|
11957
|
+
const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse(readFileSync6(path2, "utf8"));
|
|
10421
11958
|
return Array.isArray(parsed?.watches) ? parsed.watches : [];
|
|
10422
11959
|
} catch {
|
|
10423
11960
|
return [];
|
|
@@ -10469,7 +12006,7 @@ async function continueSourceThread(threadId, prompt) {
|
|
|
10469
12006
|
await continueOnReply(threadId, prompt);
|
|
10470
12007
|
return;
|
|
10471
12008
|
}
|
|
10472
|
-
const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-
|
|
12009
|
+
const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QMKVXVUR.js");
|
|
10473
12010
|
await getOrchestrator2().send(threadId, prompt);
|
|
10474
12011
|
} catch {
|
|
10475
12012
|
}
|
|
@@ -11061,6 +12598,7 @@ export {
|
|
|
11061
12598
|
disconnectLinear,
|
|
11062
12599
|
disconnectLinearConnection,
|
|
11063
12600
|
disconnectOptionalService,
|
|
12601
|
+
disconnectRemoteAccount,
|
|
11064
12602
|
disconnectSlackWorkspace,
|
|
11065
12603
|
discoverSkills,
|
|
11066
12604
|
downloadAbleTimeAttachment,
|
|
@@ -11317,6 +12855,7 @@ export {
|
|
|
11317
12855
|
isPrNotMergeableError,
|
|
11318
12856
|
isPresentPlanToolName,
|
|
11319
12857
|
isPrimaryCheckoutThread,
|
|
12858
|
+
isRemoteAccountLoginCancelled,
|
|
11320
12859
|
isRemoteStopCommand,
|
|
11321
12860
|
isSelfWrittenRecord,
|
|
11322
12861
|
isSessionQuotaLimit,
|
|
@@ -11678,6 +13217,7 @@ export {
|
|
|
11678
13217
|
startMcpServer,
|
|
11679
13218
|
startOrchestration,
|
|
11680
13219
|
startRelayServer,
|
|
13220
|
+
startRemoteAccountLogin,
|
|
11681
13221
|
startSlackOAuth,
|
|
11682
13222
|
statWorktreePath,
|
|
11683
13223
|
stopAgentRpcServer,
|