@threadbase-sh/streamer 1.58.0 → 1.58.2
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/cli.cjs +296 -129
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +215 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +257 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -851,9 +851,24 @@ import { basename } from "path";
|
|
|
851
851
|
|
|
852
852
|
// src/platform.ts
|
|
853
853
|
import { execFileSync } from "child_process";
|
|
854
|
-
import { existsSync } from "fs";
|
|
854
|
+
import { accessSync, constants, existsSync, statSync } from "fs";
|
|
855
855
|
import { homedir as homedir2, platform } from "os";
|
|
856
|
-
import { join as join4 } from "path";
|
|
856
|
+
import { delimiter, join as join4 } from "path";
|
|
857
|
+
|
|
858
|
+
// src/providers.ts
|
|
859
|
+
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
860
|
+
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
861
|
+
function isProviderName(value) {
|
|
862
|
+
return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
|
|
863
|
+
}
|
|
864
|
+
function coerceProviderForRunner(value) {
|
|
865
|
+
return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;
|
|
866
|
+
}
|
|
867
|
+
function isProviderResumable(_provider, availabilityResumable) {
|
|
868
|
+
return availabilityResumable;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/platform.ts
|
|
857
872
|
var isWindows = platform() === "win32";
|
|
858
873
|
var WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".exe", ".cmd", ".bat"]);
|
|
859
874
|
function isWindowsExecutablePath(path) {
|
|
@@ -983,18 +998,38 @@ function resolveCodexExe() {
|
|
|
983
998
|
_codexExe = "codex";
|
|
984
999
|
return _codexExe;
|
|
985
1000
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1001
|
+
function isExecutableFile(path) {
|
|
1002
|
+
try {
|
|
1003
|
+
if (!statSync(path).isFile()) return false;
|
|
1004
|
+
accessSync(path, constants.X_OK);
|
|
1005
|
+
return true;
|
|
1006
|
+
} catch {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
992
1009
|
}
|
|
993
|
-
function
|
|
994
|
-
return
|
|
1010
|
+
function locateExecutable(exe) {
|
|
1011
|
+
if (/[\\/]/.test(exe)) return isExecutableFile(exe) ? exe : null;
|
|
1012
|
+
const names = isWindows ? [
|
|
1013
|
+
...isWindowsExecutablePath(exe) ? [exe] : [],
|
|
1014
|
+
...[...WINDOWS_EXECUTABLE_EXTENSIONS].map((ext) => `${exe}${ext}`)
|
|
1015
|
+
] : [exe];
|
|
1016
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
1017
|
+
if (!dir) continue;
|
|
1018
|
+
for (const name of names) {
|
|
1019
|
+
const candidate = join4(dir, name);
|
|
1020
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
995
1024
|
}
|
|
996
|
-
function
|
|
997
|
-
|
|
1025
|
+
function locateProviderExe(provider) {
|
|
1026
|
+
const isCodex = provider === CODEX_CLI_PROVIDER;
|
|
1027
|
+
const found = locateExecutable(isCodex ? resolveCodexExe() : resolveClaudeExe());
|
|
1028
|
+
if (found === null) {
|
|
1029
|
+
if (isCodex) clearCodexExeCache();
|
|
1030
|
+
else clearClaudeExeCache();
|
|
1031
|
+
}
|
|
1032
|
+
return found;
|
|
998
1033
|
}
|
|
999
1034
|
|
|
1000
1035
|
// src/pty-shared.ts
|
|
@@ -1252,11 +1287,19 @@ var CodexPtyRunner = class {
|
|
|
1252
1287
|
onStatusChange;
|
|
1253
1288
|
onPhaseChange;
|
|
1254
1289
|
onReady;
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1290
|
+
// Every Codex prompt the client can answer — startup gates (directory trust,
|
|
1291
|
+
// hooks review), command approvals, and the rate-limit model picker — is
|
|
1292
|
+
// broadcast through this one channel; null dismisses the card once the prompt
|
|
1293
|
+
// leaves the screen.
|
|
1294
|
+
//
|
|
1295
|
+
// Deliberately NOT onLiveQuestion/onLiveQuestionGone, which is Claude's
|
|
1296
|
+
// AskUserQuestion transport. Both channels land on the same mobile
|
|
1297
|
+
// QuestionCard, and the permission one is the correct fit for Codex: its menus
|
|
1298
|
+
// are answered by the option's real on-screen number (parseCodexNumberedOptions
|
|
1299
|
+
// emits `answerKeys: "2\r"`), which is exactly what `permissionIndices` carries
|
|
1300
|
+
// and what AskUserQuestion's down-arrow-count model cannot express. Wiring the
|
|
1301
|
+
// question channel as well would be a second path to the same card.
|
|
1257
1302
|
onPermissionChange;
|
|
1258
|
-
onLiveQuestion;
|
|
1259
|
-
onLiveQuestionGone;
|
|
1260
1303
|
onUserMessage;
|
|
1261
1304
|
log;
|
|
1262
1305
|
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
@@ -1306,8 +1349,6 @@ var CodexPtyRunner = class {
|
|
|
1306
1349
|
this.onPhaseChange = options.onPhaseChange;
|
|
1307
1350
|
this.onReady = options.onReady;
|
|
1308
1351
|
this.onPermissionChange = options.onPermissionChange;
|
|
1309
|
-
this.onLiveQuestion = options.onLiveQuestion;
|
|
1310
|
-
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
1311
1352
|
this.onUserMessage = options.onUserMessage;
|
|
1312
1353
|
this.log = options.logger ?? getLogger("codex-pty");
|
|
1313
1354
|
}
|
|
@@ -3615,11 +3656,13 @@ var LiveSessionManager = class {
|
|
|
3615
3656
|
async start(sessionId, options) {
|
|
3616
3657
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3617
3658
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3659
|
+
this.assertProviderInstalled(provider);
|
|
3618
3660
|
return runner.start(sessionId, options);
|
|
3619
3661
|
}
|
|
3620
3662
|
async startFresh(options) {
|
|
3621
3663
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3622
3664
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3665
|
+
this.assertProviderInstalled(provider);
|
|
3623
3666
|
return runner.startFresh(options);
|
|
3624
3667
|
}
|
|
3625
3668
|
/**
|
|
@@ -3638,6 +3681,7 @@ var LiveSessionManager = class {
|
|
|
3638
3681
|
err.statusCode = 501;
|
|
3639
3682
|
throw err;
|
|
3640
3683
|
}
|
|
3684
|
+
this.assertProviderInstalled(provider);
|
|
3641
3685
|
return runner.startFork(options);
|
|
3642
3686
|
}
|
|
3643
3687
|
sendInput(sessionId, input) {
|
|
@@ -3719,6 +3763,30 @@ var LiveSessionManager = class {
|
|
|
3719
3763
|
}
|
|
3720
3764
|
throw new Error(`Session not found: ${sessionId}`);
|
|
3721
3765
|
}
|
|
3766
|
+
/**
|
|
3767
|
+
* Refuse before spawning when the provider's CLI is not on this machine.
|
|
3768
|
+
*
|
|
3769
|
+
* Without this the spawn "succeeds": on POSIX execvp fails inside the forked
|
|
3770
|
+
* child, so a session appears, exits ~12ms later with code 1 and no output,
|
|
3771
|
+
* and the caller is told only that it "exited before becoming ready" — or,
|
|
3772
|
+
* on the Claude resume path, is told nothing at all, since that path answers
|
|
3773
|
+
* 200 before the process has had a chance to die. Every start route funnels
|
|
3774
|
+
* through here, so one check covers start, resume, adopt and fork.
|
|
3775
|
+
*
|
|
3776
|
+
* 503, not 500: the request was well-formed and the fault is this machine's
|
|
3777
|
+
* environment. `code` is what mobile branches on (it reads `errBody.code`),
|
|
3778
|
+
* and `PROVIDER_NOT_INSTALLED` is a remediation string it already knows.
|
|
3779
|
+
*/
|
|
3780
|
+
assertProviderInstalled(provider) {
|
|
3781
|
+
if (locateProviderExe(provider) !== null) return;
|
|
3782
|
+
const command = provider === CODEX_CLI_PROVIDER ? "codex" : "claude";
|
|
3783
|
+
const err = new Error(
|
|
3784
|
+
`The ${command} command was not found on this server. Install the ${provider} CLI, or make sure it is on the PATH the streamer runs with.`
|
|
3785
|
+
);
|
|
3786
|
+
err.statusCode = 503;
|
|
3787
|
+
err.code = "PROVIDER_NOT_INSTALLED";
|
|
3788
|
+
throw err;
|
|
3789
|
+
}
|
|
3722
3790
|
assertSupportedProvider(provider, projectPath) {
|
|
3723
3791
|
if (this.remoteRunner) return this.remoteRunner;
|
|
3724
3792
|
const runner = this.runners.get(provider);
|
|
@@ -4401,7 +4469,9 @@ var corsMiddleware = (configValue) => {
|
|
|
4401
4469
|
// src/api/middleware/error.middleware.ts
|
|
4402
4470
|
var errorMiddleware = (err, c) => {
|
|
4403
4471
|
const message = err instanceof Error ? err.message : "Internal server error";
|
|
4404
|
-
|
|
4472
|
+
const { statusCode, code } = err;
|
|
4473
|
+
const status = typeof statusCode === "number" && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
|
|
4474
|
+
return c.json(typeof code === "string" ? { error: message, code } : { error: message }, status);
|
|
4405
4475
|
};
|
|
4406
4476
|
|
|
4407
4477
|
// src/api/routes/backup.routes.ts
|
|
@@ -4956,24 +5026,26 @@ function computeBootToken() {
|
|
|
4956
5026
|
}
|
|
4957
5027
|
|
|
4958
5028
|
// src/api/routes/diagnostics.routes.ts
|
|
4959
|
-
function providerCheck(name
|
|
5029
|
+
function providerCheck(name) {
|
|
4960
5030
|
try {
|
|
4961
|
-
const exe =
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
5031
|
+
const exe = locateProviderExe(name);
|
|
5032
|
+
if (exe !== null) {
|
|
5033
|
+
return {
|
|
5034
|
+
id: `provider:${name}`,
|
|
5035
|
+
status: "ok",
|
|
5036
|
+
summary: `${name} CLI is installed.`,
|
|
5037
|
+
remediation: "NONE",
|
|
5038
|
+
detail: { location: redactPath(exe) }
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
4969
5041
|
} catch {
|
|
4970
|
-
return {
|
|
4971
|
-
id: `provider:${name}`,
|
|
4972
|
-
status: "failed",
|
|
4973
|
-
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
4974
|
-
remediation: "PROVIDER_NOT_INSTALLED"
|
|
4975
|
-
};
|
|
4976
5042
|
}
|
|
5043
|
+
return {
|
|
5044
|
+
id: `provider:${name}`,
|
|
5045
|
+
status: "failed",
|
|
5046
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
5047
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
5048
|
+
};
|
|
4977
5049
|
}
|
|
4978
5050
|
var createDiagnosticsRoutes = (deps) => {
|
|
4979
5051
|
const app = new Hono8();
|
|
@@ -4986,8 +5058,8 @@ var createDiagnosticsRoutes = (deps) => {
|
|
|
4986
5058
|
remediation: "NONE",
|
|
4987
5059
|
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
4988
5060
|
});
|
|
4989
|
-
checks.push(providerCheck(
|
|
4990
|
-
checks.push(providerCheck(
|
|
5061
|
+
checks.push(providerCheck(CLAUDE_CODE_PROVIDER));
|
|
5062
|
+
checks.push(providerCheck(CODEX_CLI_PROVIDER));
|
|
4991
5063
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
4992
5064
|
checks.push(
|
|
4993
5065
|
cacheAlert ? {
|
|
@@ -5092,7 +5164,7 @@ var createHealthRoutes = (deps) => {
|
|
|
5092
5164
|
};
|
|
5093
5165
|
|
|
5094
5166
|
// src/api/routes/logs.routes.ts
|
|
5095
|
-
import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
|
|
5167
|
+
import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync as statSync2 } from "fs";
|
|
5096
5168
|
import { join as join8 } from "path";
|
|
5097
5169
|
import { Hono as Hono10 } from "hono";
|
|
5098
5170
|
|
|
@@ -5112,7 +5184,7 @@ function resolveLogPath(source) {
|
|
|
5112
5184
|
function pickDefaultSource() {
|
|
5113
5185
|
for (const source of ["stdout", "stderr", "dev"]) {
|
|
5114
5186
|
const p = resolveLogPath(source);
|
|
5115
|
-
if (existsSync5(p) &&
|
|
5187
|
+
if (existsSync5(p) && statSync2(p).size > 0) return source;
|
|
5116
5188
|
}
|
|
5117
5189
|
return "stdout";
|
|
5118
5190
|
}
|
|
@@ -5170,7 +5242,7 @@ function createLogsRoutes() {
|
|
|
5170
5242
|
});
|
|
5171
5243
|
}
|
|
5172
5244
|
const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
|
|
5173
|
-
const stats =
|
|
5245
|
+
const stats = statSync2(logPath);
|
|
5174
5246
|
return c.json({
|
|
5175
5247
|
logs: lines,
|
|
5176
5248
|
offset,
|
|
@@ -5200,7 +5272,7 @@ function createLogsRoutes() {
|
|
|
5200
5272
|
if (!existsSync5(logPath)) {
|
|
5201
5273
|
return { source, exists: false, total: 0, fileSize: 0 };
|
|
5202
5274
|
}
|
|
5203
|
-
const stats =
|
|
5275
|
+
const stats = statSync2(logPath);
|
|
5204
5276
|
return {
|
|
5205
5277
|
source,
|
|
5206
5278
|
exists: true,
|
|
@@ -5578,7 +5650,7 @@ function publicKeyOf(privateKey) {
|
|
|
5578
5650
|
|
|
5579
5651
|
// src/services/push/apnsClient.ts
|
|
5580
5652
|
import { createSign } from "crypto";
|
|
5581
|
-
import { connect, constants } from "http2";
|
|
5653
|
+
import { connect, constants as constants2 } from "http2";
|
|
5582
5654
|
var log = getLogger("apns");
|
|
5583
5655
|
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
5584
5656
|
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
@@ -5685,27 +5757,27 @@ var ApnsClient = class {
|
|
|
5685
5757
|
}
|
|
5686
5758
|
const session = this.getSession();
|
|
5687
5759
|
const headers = {
|
|
5688
|
-
[
|
|
5689
|
-
[
|
|
5690
|
-
[
|
|
5760
|
+
[constants2.HTTP2_HEADER_METHOD]: "POST",
|
|
5761
|
+
[constants2.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
5762
|
+
[constants2.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
5691
5763
|
"apns-push-type": "liveactivity",
|
|
5692
5764
|
"apns-topic": this.topic,
|
|
5693
5765
|
"apns-priority": String(args.priority ?? 10),
|
|
5694
5766
|
...args.expirationSeconds != null && {
|
|
5695
5767
|
"apns-expiration": String(args.expirationSeconds)
|
|
5696
5768
|
},
|
|
5697
|
-
[
|
|
5698
|
-
[
|
|
5769
|
+
[constants2.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
5770
|
+
[constants2.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
5699
5771
|
};
|
|
5700
5772
|
return new Promise((resolve2, reject) => {
|
|
5701
5773
|
const req = session.request(headers);
|
|
5702
5774
|
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
5703
|
-
req.close(
|
|
5775
|
+
req.close(constants2.NGHTTP2_CANCEL);
|
|
5704
5776
|
resolve2({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
5705
5777
|
});
|
|
5706
5778
|
let status = 0;
|
|
5707
5779
|
req.on("response", (resHeaders) => {
|
|
5708
|
-
status = Number(resHeaders[
|
|
5780
|
+
status = Number(resHeaders[constants2.HTTP2_HEADER_STATUS] ?? 0);
|
|
5709
5781
|
});
|
|
5710
5782
|
const chunks = [];
|
|
5711
5783
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -6059,7 +6131,10 @@ function parseVersionOutput(output) {
|
|
|
6059
6131
|
const match = output.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
|
|
6060
6132
|
return match ? match[0] : null;
|
|
6061
6133
|
}
|
|
6134
|
+
var versionByExe = /* @__PURE__ */ new Map();
|
|
6062
6135
|
function runVersion(exe) {
|
|
6136
|
+
const cached3 = versionByExe.get(exe);
|
|
6137
|
+
if (cached3 !== void 0) return Promise.resolve(cached3);
|
|
6063
6138
|
const viaShell = isWindows && /\.(?:cmd|bat)$/i.test(exe);
|
|
6064
6139
|
const file = viaShell ? `"${exe}"` : exe;
|
|
6065
6140
|
return new Promise((resolve2) => {
|
|
@@ -6069,7 +6144,9 @@ function runVersion(exe) {
|
|
|
6069
6144
|
{ timeout: VERSION_TIMEOUT_MS, shell: viaShell, windowsHide: true },
|
|
6070
6145
|
(err, stdout, stderr) => {
|
|
6071
6146
|
if (err && !stdout && !stderr) return resolve2(null);
|
|
6072
|
-
|
|
6147
|
+
const version = parseVersionOutput(`${stdout}${stderr}`);
|
|
6148
|
+
if (version !== null) versionByExe.set(exe, version);
|
|
6149
|
+
resolve2(version);
|
|
6073
6150
|
}
|
|
6074
6151
|
);
|
|
6075
6152
|
});
|
|
@@ -6113,13 +6190,16 @@ function compareSemver(a, b) {
|
|
|
6113
6190
|
if (pb.pre === null) return -1;
|
|
6114
6191
|
return pa.pre < pb.pre ? -1 : 1;
|
|
6115
6192
|
}
|
|
6116
|
-
async function providerHealth(name,
|
|
6193
|
+
async function providerHealth(name, locateExe = () => locateProviderExe(name), detect = runVersion) {
|
|
6117
6194
|
const verifiedAgainst = VERIFIED_AGAINST[name];
|
|
6118
6195
|
const capabilities = capabilitiesFor(name);
|
|
6119
|
-
let exe;
|
|
6196
|
+
let exe = null;
|
|
6120
6197
|
try {
|
|
6121
|
-
exe =
|
|
6198
|
+
exe = locateExe();
|
|
6122
6199
|
} catch {
|
|
6200
|
+
exe = null;
|
|
6201
|
+
}
|
|
6202
|
+
if (exe === null) {
|
|
6123
6203
|
return {
|
|
6124
6204
|
name,
|
|
6125
6205
|
available: false,
|
|
@@ -6156,8 +6236,8 @@ var createProviderRoutes = () => {
|
|
|
6156
6236
|
const app = new Hono14();
|
|
6157
6237
|
app.get("/", async (c) => {
|
|
6158
6238
|
const providers = await Promise.all([
|
|
6159
|
-
providerHealth(CLAUDE_CODE_PROVIDER
|
|
6160
|
-
providerHealth(CODEX_CLI_PROVIDER
|
|
6239
|
+
providerHealth(CLAUDE_CODE_PROVIDER),
|
|
6240
|
+
providerHealth(CODEX_CLI_PROVIDER)
|
|
6161
6241
|
]);
|
|
6162
6242
|
return c.json({ providers });
|
|
6163
6243
|
});
|
|
@@ -6388,7 +6468,7 @@ import {
|
|
|
6388
6468
|
parseJsonlLine
|
|
6389
6469
|
} from "@threadbase-sh/scanner";
|
|
6390
6470
|
import Database from "better-sqlite3";
|
|
6391
|
-
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync3, readSync as readSync3, statSync as
|
|
6471
|
+
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync3, readSync as readSync3, statSync as statSync4 } from "fs";
|
|
6392
6472
|
import { open as openAsync } from "fs/promises";
|
|
6393
6473
|
import { dirname as dirname8 } from "path";
|
|
6394
6474
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
@@ -6506,7 +6586,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
6506
6586
|
}
|
|
6507
6587
|
|
|
6508
6588
|
// src/services/conversations/isAgentConversation.ts
|
|
6509
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as
|
|
6589
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync3 } from "fs";
|
|
6510
6590
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
6511
6591
|
var CHUNK_BYTES = 64 * 1024;
|
|
6512
6592
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -6533,7 +6613,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
6533
6613
|
return false;
|
|
6534
6614
|
}
|
|
6535
6615
|
try {
|
|
6536
|
-
const fileSize =
|
|
6616
|
+
const fileSize = statSync3(filePath).size;
|
|
6537
6617
|
if (fileSize === 0) {
|
|
6538
6618
|
fileDecisionCache.set(key, false);
|
|
6539
6619
|
return false;
|
|
@@ -7171,7 +7251,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7171
7251
|
if (!fileState) return null;
|
|
7172
7252
|
let stat3;
|
|
7173
7253
|
try {
|
|
7174
|
-
stat3 =
|
|
7254
|
+
stat3 = statSync4(filePath);
|
|
7175
7255
|
} catch {
|
|
7176
7256
|
return null;
|
|
7177
7257
|
}
|
|
@@ -7230,7 +7310,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7230
7310
|
isAgentFileCached(filePath) {
|
|
7231
7311
|
let s;
|
|
7232
7312
|
try {
|
|
7233
|
-
s =
|
|
7313
|
+
s = statSync4(filePath);
|
|
7234
7314
|
} catch {
|
|
7235
7315
|
return false;
|
|
7236
7316
|
}
|
|
@@ -7441,7 +7521,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7441
7521
|
let mtimeMs = null;
|
|
7442
7522
|
let fileSize = null;
|
|
7443
7523
|
try {
|
|
7444
|
-
const s =
|
|
7524
|
+
const s = statSync4(m.filePath);
|
|
7445
7525
|
mtimeMs = s.mtimeMs;
|
|
7446
7526
|
fileSize = s.size;
|
|
7447
7527
|
} catch {
|
|
@@ -7501,7 +7581,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7501
7581
|
let fileSize;
|
|
7502
7582
|
let fd;
|
|
7503
7583
|
try {
|
|
7504
|
-
fileSize =
|
|
7584
|
+
fileSize = statSync4(filePath).size;
|
|
7505
7585
|
fd = openSync3(filePath, "r");
|
|
7506
7586
|
} catch {
|
|
7507
7587
|
return false;
|
|
@@ -9497,7 +9577,7 @@ async function findRolloutOwner(rolloutPath, options = {}) {
|
|
|
9497
9577
|
}
|
|
9498
9578
|
|
|
9499
9579
|
// src/services/sessions/conversationBusy.ts
|
|
9500
|
-
import { statSync as
|
|
9580
|
+
import { statSync as statSync5 } from "fs";
|
|
9501
9581
|
|
|
9502
9582
|
// src/utils/canonicalizeProjectPath.ts
|
|
9503
9583
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -9521,7 +9601,7 @@ function conversationBusy(input) {
|
|
|
9521
9601
|
let lastActivityMs = null;
|
|
9522
9602
|
if (input.jsonlPath) {
|
|
9523
9603
|
try {
|
|
9524
|
-
const mtimeMs =
|
|
9604
|
+
const mtimeMs = statSync5(input.jsonlPath).mtimeMs;
|
|
9525
9605
|
const age = now - mtimeMs;
|
|
9526
9606
|
lastActivityMs = Math.max(0, age);
|
|
9527
9607
|
const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
|
|
@@ -10636,7 +10716,10 @@ var SessionHandlers = class {
|
|
|
10636
10716
|
});
|
|
10637
10717
|
this.sessionStore.addManaged(session);
|
|
10638
10718
|
this.registryBoot.recordSessionSpawn(session);
|
|
10639
|
-
const { outcome } = await this.deps.waitForStartupOutcome(
|
|
10719
|
+
const { outcome, session: settled } = await this.deps.waitForStartupOutcome(
|
|
10720
|
+
session.id,
|
|
10721
|
+
START_READY_TIMEOUT_MS
|
|
10722
|
+
);
|
|
10640
10723
|
const current = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());
|
|
10641
10724
|
if (outcome === "ready" && current) {
|
|
10642
10725
|
json(res, 200, { session: current });
|
|
@@ -10644,7 +10727,7 @@ var SessionHandlers = class {
|
|
|
10644
10727
|
json(res, 502, {
|
|
10645
10728
|
id: session.id,
|
|
10646
10729
|
status: "idle",
|
|
10647
|
-
error:
|
|
10730
|
+
error: settled?.failureReason ?? "Session exited before becoming ready"
|
|
10648
10731
|
});
|
|
10649
10732
|
} else {
|
|
10650
10733
|
json(res, 202, { id: session.id, status: "pending" });
|
|
@@ -10658,11 +10741,16 @@ var SessionHandlers = class {
|
|
|
10658
10741
|
} catch (err) {
|
|
10659
10742
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
10660
10743
|
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
10744
|
+
const code = err.code;
|
|
10661
10745
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
10662
10746
|
event: "session.start_failed",
|
|
10663
10747
|
error: message
|
|
10664
10748
|
});
|
|
10665
|
-
json(
|
|
10749
|
+
json(
|
|
10750
|
+
res,
|
|
10751
|
+
statusCode,
|
|
10752
|
+
typeof code === "string" ? { error: message, code } : { error: message }
|
|
10753
|
+
);
|
|
10666
10754
|
}
|
|
10667
10755
|
}
|
|
10668
10756
|
async handleSetSessionName(sessionId, req, res) {
|
|
@@ -11152,7 +11240,7 @@ var RuntimeStore = class _RuntimeStore {
|
|
|
11152
11240
|
};
|
|
11153
11241
|
|
|
11154
11242
|
// src/external-tails.ts
|
|
11155
|
-
import { statSync as
|
|
11243
|
+
import { statSync as statSync6 } from "fs";
|
|
11156
11244
|
var EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;
|
|
11157
11245
|
var EXTERNAL_TAIL_MAX = 32;
|
|
11158
11246
|
var EXTERNAL_TAIL_IDLE_MS = 3e5;
|
|
@@ -11185,7 +11273,7 @@ var ExternalTailManager = class {
|
|
|
11185
11273
|
if (this.isManagedTailPath(key)) return;
|
|
11186
11274
|
let mtimeMs;
|
|
11187
11275
|
try {
|
|
11188
|
-
mtimeMs =
|
|
11276
|
+
mtimeMs = statSync6(filePath).mtimeMs;
|
|
11189
11277
|
} catch {
|
|
11190
11278
|
return;
|
|
11191
11279
|
}
|
|
@@ -11346,7 +11434,37 @@ var PairTokenStore = class {
|
|
|
11346
11434
|
expiresInSeconds: Math.floor(this.ttlMs / 1e3)
|
|
11347
11435
|
};
|
|
11348
11436
|
}
|
|
11437
|
+
/**
|
|
11438
|
+
* Whether `consume` would succeed right now, WITHOUT spending the token.
|
|
11439
|
+
*
|
|
11440
|
+
* Exists so a caller can reject a bad token before doing any work, and still
|
|
11441
|
+
* spend the token only once the work has succeeded. A pair token is
|
|
11442
|
+
* single-use and lives 180 seconds, so spending it on a request that then
|
|
11443
|
+
* fails costs the user a whole new QR — and, worse, makes their retry
|
|
11444
|
+
* indistinguishable from an attacker replaying a photographed code, which is
|
|
11445
|
+
* the one signal `design.md` §2.6 designates as replay detection.
|
|
11446
|
+
*
|
|
11447
|
+
* Advisory, not a reservation: it takes no lock and holds nothing. The
|
|
11448
|
+
* authoritative answer is still `consume`'s.
|
|
11449
|
+
*/
|
|
11450
|
+
verify(token) {
|
|
11451
|
+
const result = this.check(token);
|
|
11452
|
+
return result.ok ? { ok: true } : result;
|
|
11453
|
+
}
|
|
11349
11454
|
consume(token) {
|
|
11455
|
+
const result = this.check(token);
|
|
11456
|
+
if (!result.ok) return result;
|
|
11457
|
+
result.record.used = true;
|
|
11458
|
+
return { ok: true };
|
|
11459
|
+
}
|
|
11460
|
+
/**
|
|
11461
|
+
* The shared predicate behind `verify` and `consume`.
|
|
11462
|
+
*
|
|
11463
|
+
* One implementation on purpose: two copies of "is this token usable" is two
|
|
11464
|
+
* places for the expiry or single-use rule to drift, and a drift in this
|
|
11465
|
+
* direction fails open.
|
|
11466
|
+
*/
|
|
11467
|
+
check(token) {
|
|
11350
11468
|
const record2 = this.current;
|
|
11351
11469
|
if (!record2 || record2.token !== token) return { ok: false, reason: "unknown" };
|
|
11352
11470
|
if (Date.now() > record2.expiresAt) {
|
|
@@ -11354,8 +11472,7 @@ var PairTokenStore = class {
|
|
|
11354
11472
|
return { ok: false, reason: "expired" };
|
|
11355
11473
|
}
|
|
11356
11474
|
if (record2.used) return { ok: false, reason: "used" };
|
|
11357
|
-
|
|
11358
|
-
return { ok: true };
|
|
11475
|
+
return { ok: true, record: record2 };
|
|
11359
11476
|
}
|
|
11360
11477
|
peek() {
|
|
11361
11478
|
return this.current;
|
|
@@ -11461,7 +11578,7 @@ function spawnDetachedHost(socketPath, entryPoint) {
|
|
|
11461
11578
|
import {
|
|
11462
11579
|
ConversationScanner
|
|
11463
11580
|
} from "@threadbase-sh/scanner";
|
|
11464
|
-
import { statSync as
|
|
11581
|
+
import { statSync as statSync8 } from "fs";
|
|
11465
11582
|
import { homedir as homedir10 } from "os";
|
|
11466
11583
|
import { join as join20 } from "path";
|
|
11467
11584
|
|
|
@@ -11562,14 +11679,14 @@ function refreshConversationCache(deps) {
|
|
|
11562
11679
|
}
|
|
11563
11680
|
|
|
11564
11681
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
11565
|
-
import { readdirSync as readdirSync4, statSync as
|
|
11682
|
+
import { readdirSync as readdirSync4, statSync as statSync7 } from "fs";
|
|
11566
11683
|
import { homedir as homedir9 } from "os";
|
|
11567
11684
|
import { join as join19 } from "path";
|
|
11568
11685
|
var DEFAULT_PROJECTS_DIR = join19(homedir9(), ".claude", "projects");
|
|
11569
11686
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
11570
11687
|
let maxMs;
|
|
11571
11688
|
try {
|
|
11572
|
-
maxMs =
|
|
11689
|
+
maxMs = statSync7(projectsDir).mtimeMs;
|
|
11573
11690
|
} catch {
|
|
11574
11691
|
return null;
|
|
11575
11692
|
}
|
|
@@ -11577,7 +11694,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
|
|
|
11577
11694
|
for (const ent of readdirSync4(projectsDir, { withFileTypes: true })) {
|
|
11578
11695
|
if (!ent.isDirectory()) continue;
|
|
11579
11696
|
try {
|
|
11580
|
-
const childMs =
|
|
11697
|
+
const childMs = statSync7(join19(projectsDir, ent.name)).mtimeMs;
|
|
11581
11698
|
if (childMs > maxMs) maxMs = childMs;
|
|
11582
11699
|
} catch {
|
|
11583
11700
|
}
|
|
@@ -11789,7 +11906,7 @@ var ScannerManager = class {
|
|
|
11789
11906
|
if (!conv.filePath) return false;
|
|
11790
11907
|
let mtimeMs = null;
|
|
11791
11908
|
try {
|
|
11792
|
-
mtimeMs =
|
|
11909
|
+
mtimeMs = statSync8(conv.filePath).mtimeMs;
|
|
11793
11910
|
} catch {
|
|
11794
11911
|
return false;
|
|
11795
11912
|
}
|
|
@@ -12028,10 +12145,10 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
12028
12145
|
}
|
|
12029
12146
|
|
|
12030
12147
|
// src/server-wiring.ts
|
|
12031
|
-
import { statSync as
|
|
12148
|
+
import { statSync as statSync10 } from "fs";
|
|
12032
12149
|
|
|
12033
12150
|
// src/handlers/handleListProjects.ts
|
|
12034
|
-
import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync5, readSync as readSync4, statSync as
|
|
12151
|
+
import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync5, readSync as readSync4, statSync as statSync9 } from "fs";
|
|
12035
12152
|
import { homedir as homedir11 } from "os";
|
|
12036
12153
|
import { join as join21 } from "path";
|
|
12037
12154
|
var HEAD_BYTES = 64 * 1024;
|
|
@@ -12077,7 +12194,7 @@ function handleListProjects(url, res) {
|
|
|
12077
12194
|
const fullPath = join21(projectsDir, dirName);
|
|
12078
12195
|
let mtime = 0;
|
|
12079
12196
|
try {
|
|
12080
|
-
mtime =
|
|
12197
|
+
mtime = statSync9(fullPath).mtimeMs;
|
|
12081
12198
|
} catch {
|
|
12082
12199
|
}
|
|
12083
12200
|
return { dirName: String(dirName), mtime };
|
|
@@ -12111,7 +12228,7 @@ function createConversationWatcherEvents(deps) {
|
|
|
12111
12228
|
const seqs = cache.extendMessageIndex(
|
|
12112
12229
|
filePath,
|
|
12113
12230
|
spans,
|
|
12114
|
-
|
|
12231
|
+
statSync10(filePath),
|
|
12115
12232
|
readFrom,
|
|
12116
12233
|
endOffset
|
|
12117
12234
|
);
|
|
@@ -12158,7 +12275,7 @@ function createConversationWatcherEvents(deps) {
|
|
|
12158
12275
|
},
|
|
12159
12276
|
onConversationChanged: (filePath) => {
|
|
12160
12277
|
try {
|
|
12161
|
-
|
|
12278
|
+
statSync10(filePath);
|
|
12162
12279
|
} catch {
|
|
12163
12280
|
deps.externalTailManager().handleJsonlDeleted(filePath);
|
|
12164
12281
|
return;
|
|
@@ -12256,7 +12373,15 @@ function createLiveSessionOptions(deps) {
|
|
|
12256
12373
|
// grace-timer/idle-reaper hold (statusSource "shutdown") apart from a
|
|
12257
12374
|
// genuine process exit ("process-exit"), and reports both as
|
|
12258
12375
|
// `lifecycle: "completed"`. See managedToResponse in session-store.ts.
|
|
12259
|
-
...session.statusSource != null && { statusSource: session.statusSource }
|
|
12376
|
+
...session.statusSource != null && { statusSource: session.statusSource },
|
|
12377
|
+
// Why a session died, not just that it did. Without this the store's
|
|
12378
|
+
// copy has no failureReason, so managedToResponse falls through to
|
|
12379
|
+
// `lifecycle: "completed"` (see session-store.ts) and a session that
|
|
12380
|
+
// never started — missing CLI, missing project dir — is reported to
|
|
12381
|
+
// every client as one that finished normally. Guarded like its
|
|
12382
|
+
// neighbours: a later transition must not blank a recorded failure.
|
|
12383
|
+
...session.failureReason != null && { failureReason: session.failureReason },
|
|
12384
|
+
...session.failureCode != null && { failureCode: session.failureCode }
|
|
12260
12385
|
});
|
|
12261
12386
|
deps.managedSessionsRepo()?.recordStatus(
|
|
12262
12387
|
session.id,
|
|
@@ -12511,7 +12636,7 @@ function saveAlertState(state) {
|
|
|
12511
12636
|
}
|
|
12512
12637
|
|
|
12513
12638
|
// src/services/cache-integrity/backup.ts
|
|
12514
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync6, statSync as
|
|
12639
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync6, statSync as statSync11, unlinkSync } from "fs";
|
|
12515
12640
|
import { join as join23 } from "path";
|
|
12516
12641
|
var DEFAULT_RETAIN = 3;
|
|
12517
12642
|
function retainCount() {
|
|
@@ -12530,7 +12655,7 @@ async function backupCacheDb(db, cacheDir) {
|
|
|
12530
12655
|
const retain = retainCount();
|
|
12531
12656
|
const backups = readdirSync6(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
12532
12657
|
const full = join23(backupsDir, f);
|
|
12533
|
-
return { full, mtime:
|
|
12658
|
+
return { full, mtime: statSync11(full).mtimeMs };
|
|
12534
12659
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
12535
12660
|
for (const stale of backups.slice(retain)) {
|
|
12536
12661
|
if (existsSync11(stale.full)) unlinkSync(stale.full);
|
|
@@ -12796,7 +12921,7 @@ var CacheIntegrityMonitor = class {
|
|
|
12796
12921
|
|
|
12797
12922
|
// src/services/conversations/conversationWatcher.ts
|
|
12798
12923
|
import chokidar from "chokidar";
|
|
12799
|
-
import { statSync as
|
|
12924
|
+
import { statSync as statSync12 } from "fs";
|
|
12800
12925
|
import { open, stat as stat2 } from "fs/promises";
|
|
12801
12926
|
var ConversationWatcher = class {
|
|
12802
12927
|
files = /* @__PURE__ */ new Map();
|
|
@@ -12822,7 +12947,7 @@ var ConversationWatcher = class {
|
|
|
12822
12947
|
if (this.files.has(key)) return;
|
|
12823
12948
|
let offset;
|
|
12824
12949
|
try {
|
|
12825
|
-
offset =
|
|
12950
|
+
offset = statSync12(filePath).size;
|
|
12826
12951
|
} catch {
|
|
12827
12952
|
offset = 0;
|
|
12828
12953
|
}
|
|
@@ -14497,7 +14622,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14497
14622
|
}
|
|
14498
14623
|
|
|
14499
14624
|
// src/session-watchers.ts
|
|
14500
|
-
import { existsSync as existsSync15, watch as fsWatch, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as
|
|
14625
|
+
import { existsSync as existsSync15, watch as fsWatch, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as statSync13 } from "fs";
|
|
14501
14626
|
import { homedir as homedir13 } from "os";
|
|
14502
14627
|
import { basename as basename6, join as join24 } from "path";
|
|
14503
14628
|
var SessionWatchers = class {
|
|
@@ -14604,7 +14729,7 @@ var SessionWatchers = class {
|
|
|
14604
14729
|
if (!resolvedFilePath && existsSync15(projectsDir)) {
|
|
14605
14730
|
try {
|
|
14606
14731
|
const now = Date.now();
|
|
14607
|
-
const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime:
|
|
14732
|
+
const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync13(join24(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
14608
14733
|
({ f }) => basename6(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join24(projectsDir, f)) === sessionId
|
|
14609
14734
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
14610
14735
|
if (match) resolvedFilePath = join24(projectsDir, match.f);
|
|
@@ -14701,7 +14826,7 @@ var SessionWatchers = class {
|
|
|
14701
14826
|
continue;
|
|
14702
14827
|
}
|
|
14703
14828
|
const nowMs = Date.now();
|
|
14704
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime:
|
|
14829
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync13(join24(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
14705
14830
|
for (const { f } of recentCandidates) {
|
|
14706
14831
|
const candidatePath = join24(sessionsDir, f);
|
|
14707
14832
|
const match = matchesProjectPath(candidatePath);
|
|
@@ -15711,6 +15836,41 @@ var StreamerServer = class {
|
|
|
15711
15836
|
json(res, 503, body);
|
|
15712
15837
|
return true;
|
|
15713
15838
|
}
|
|
15839
|
+
/**
|
|
15840
|
+
* Say which provider CLIs this machine can actually launch.
|
|
15841
|
+
*
|
|
15842
|
+
* The operator cannot discover this case unaided: under launchd/Task
|
|
15843
|
+
* Scheduler the service inherits a stripped PATH, so a CLI that works
|
|
15844
|
+
* perfectly in their terminal is invisible to the service, and every session
|
|
15845
|
+
* start dies milliseconds in. `/api/diagnostics` answers it too, but only for
|
|
15846
|
+
* someone who already suspects it.
|
|
15847
|
+
*
|
|
15848
|
+
* Availability only, never a version — `--version` costs a process spawn per
|
|
15849
|
+
* provider (85ms for claude here) and belongs on the first request that wants
|
|
15850
|
+
* it, not on boot.
|
|
15851
|
+
*
|
|
15852
|
+
* Called AFTER the port is bound, which is not cosmetic. This is the first
|
|
15853
|
+
* caller of the exe resolvers in the process, so the memo is cold by
|
|
15854
|
+
* definition and each provider pays one synchronous `which` / `where.exe`
|
|
15855
|
+
* (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing.
|
|
15856
|
+
* Windows is the risk — `where.exe` is slower, `execFileSync` blocks the
|
|
15857
|
+
* event loop, and Task Scheduler's stripped PATH is exactly where a miss
|
|
15858
|
+
* pays the full timeout — so the worst case is ~6s of two blocking lookups.
|
|
15859
|
+
* After `listen()` that delays the first requests on a box that cannot start
|
|
15860
|
+
* a session anyway; before it, it would have delayed binding the port.
|
|
15861
|
+
*/
|
|
15862
|
+
logProviderAvailability() {
|
|
15863
|
+
for (const provider of [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER]) {
|
|
15864
|
+
if (locateProviderExe(provider)) {
|
|
15865
|
+
this.log.info(`Provider ${provider}: found`, { event: "config.provider", provider });
|
|
15866
|
+
} else {
|
|
15867
|
+
this.log.warn(`Provider ${provider}: not found on PATH \u2014 sessions cannot start`, {
|
|
15868
|
+
event: "config.provider_missing",
|
|
15869
|
+
provider
|
|
15870
|
+
});
|
|
15871
|
+
}
|
|
15872
|
+
}
|
|
15873
|
+
}
|
|
15714
15874
|
async listen(port, opts) {
|
|
15715
15875
|
if (this.featureFlags.ptyHost) {
|
|
15716
15876
|
try {
|
|
@@ -15771,6 +15931,7 @@ var StreamerServer = class {
|
|
|
15771
15931
|
event: "server.listening",
|
|
15772
15932
|
...this.host !== void 0 && { host: this.host }
|
|
15773
15933
|
});
|
|
15934
|
+
this.logProviderAvailability();
|
|
15774
15935
|
try {
|
|
15775
15936
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
15776
15937
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
@@ -16147,9 +16308,9 @@ var StreamerServer = class {
|
|
|
16147
16308
|
json(res, 400, { error: "Missing token or clientPublicKey" });
|
|
16148
16309
|
return;
|
|
16149
16310
|
}
|
|
16150
|
-
const
|
|
16151
|
-
if (!
|
|
16152
|
-
json(res, 401, { error: `Pair token ${
|
|
16311
|
+
const precheck = this.pairTokens.verify(token);
|
|
16312
|
+
if (!precheck.ok) {
|
|
16313
|
+
json(res, 401, { error: `Pair token ${precheck.reason}` });
|
|
16153
16314
|
return;
|
|
16154
16315
|
}
|
|
16155
16316
|
let sealed;
|
|
@@ -16160,6 +16321,11 @@ var StreamerServer = class {
|
|
|
16160
16321
|
json(res, 400, { error: message });
|
|
16161
16322
|
return;
|
|
16162
16323
|
}
|
|
16324
|
+
const result = this.pairTokens.consume(token);
|
|
16325
|
+
if (!result.ok) {
|
|
16326
|
+
json(res, 401, { error: `Pair token ${result.reason}` });
|
|
16327
|
+
return;
|
|
16328
|
+
}
|
|
16163
16329
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
16164
16330
|
this.log.info(`[pair] token exchanged from ${ip} at ${ts}`, {
|
|
16165
16331
|
event: "pair.token_exchanged",
|