@threadbase-sh/streamer 1.58.1 → 1.58.3
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 +260 -123
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +179 -43
- 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 +221 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -907,6 +907,21 @@ var import_child_process = require("child_process");
|
|
|
907
907
|
var import_fs3 = require("fs");
|
|
908
908
|
var import_os2 = require("os");
|
|
909
909
|
var import_path3 = require("path");
|
|
910
|
+
|
|
911
|
+
// src/providers.ts
|
|
912
|
+
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
913
|
+
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
914
|
+
function isProviderName(value) {
|
|
915
|
+
return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
|
|
916
|
+
}
|
|
917
|
+
function coerceProviderForRunner(value) {
|
|
918
|
+
return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;
|
|
919
|
+
}
|
|
920
|
+
function isProviderResumable(_provider, availabilityResumable) {
|
|
921
|
+
return availabilityResumable;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/platform.ts
|
|
910
925
|
var isWindows = (0, import_os2.platform)() === "win32";
|
|
911
926
|
var WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".exe", ".cmd", ".bat"]);
|
|
912
927
|
function isWindowsExecutablePath(path) {
|
|
@@ -1036,18 +1051,38 @@ function resolveCodexExe() {
|
|
|
1036
1051
|
_codexExe = "codex";
|
|
1037
1052
|
return _codexExe;
|
|
1038
1053
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1054
|
+
function isExecutableFile(path) {
|
|
1055
|
+
try {
|
|
1056
|
+
if (!(0, import_fs3.statSync)(path).isFile()) return false;
|
|
1057
|
+
(0, import_fs3.accessSync)(path, import_fs3.constants.X_OK);
|
|
1058
|
+
return true;
|
|
1059
|
+
} catch {
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1045
1062
|
}
|
|
1046
|
-
function
|
|
1047
|
-
return
|
|
1063
|
+
function locateExecutable(exe) {
|
|
1064
|
+
if (/[\\/]/.test(exe)) return isExecutableFile(exe) ? exe : null;
|
|
1065
|
+
const names = isWindows ? [
|
|
1066
|
+
...isWindowsExecutablePath(exe) ? [exe] : [],
|
|
1067
|
+
...[...WINDOWS_EXECUTABLE_EXTENSIONS].map((ext) => `${exe}${ext}`)
|
|
1068
|
+
] : [exe];
|
|
1069
|
+
for (const dir of (process.env.PATH ?? "").split(import_path3.delimiter)) {
|
|
1070
|
+
if (!dir) continue;
|
|
1071
|
+
for (const name of names) {
|
|
1072
|
+
const candidate = (0, import_path3.join)(dir, name);
|
|
1073
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1048
1077
|
}
|
|
1049
|
-
function
|
|
1050
|
-
|
|
1078
|
+
function locateProviderExe(provider) {
|
|
1079
|
+
const isCodex = provider === CODEX_CLI_PROVIDER;
|
|
1080
|
+
const found = locateExecutable(isCodex ? resolveCodexExe() : resolveClaudeExe());
|
|
1081
|
+
if (found === null) {
|
|
1082
|
+
if (isCodex) clearCodexExeCache();
|
|
1083
|
+
else clearClaudeExeCache();
|
|
1084
|
+
}
|
|
1085
|
+
return found;
|
|
1051
1086
|
}
|
|
1052
1087
|
|
|
1053
1088
|
// src/pty-shared.ts
|
|
@@ -3674,11 +3709,13 @@ var LiveSessionManager = class {
|
|
|
3674
3709
|
async start(sessionId, options) {
|
|
3675
3710
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3676
3711
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3712
|
+
this.assertProviderInstalled(provider);
|
|
3677
3713
|
return runner.start(sessionId, options);
|
|
3678
3714
|
}
|
|
3679
3715
|
async startFresh(options) {
|
|
3680
3716
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3681
3717
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3718
|
+
this.assertProviderInstalled(provider);
|
|
3682
3719
|
return runner.startFresh(options);
|
|
3683
3720
|
}
|
|
3684
3721
|
/**
|
|
@@ -3697,6 +3734,7 @@ var LiveSessionManager = class {
|
|
|
3697
3734
|
err.statusCode = 501;
|
|
3698
3735
|
throw err;
|
|
3699
3736
|
}
|
|
3737
|
+
this.assertProviderInstalled(provider);
|
|
3700
3738
|
return runner.startFork(options);
|
|
3701
3739
|
}
|
|
3702
3740
|
sendInput(sessionId, input) {
|
|
@@ -3778,6 +3816,30 @@ var LiveSessionManager = class {
|
|
|
3778
3816
|
}
|
|
3779
3817
|
throw new Error(`Session not found: ${sessionId}`);
|
|
3780
3818
|
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Refuse before spawning when the provider's CLI is not on this machine.
|
|
3821
|
+
*
|
|
3822
|
+
* Without this the spawn "succeeds": on POSIX execvp fails inside the forked
|
|
3823
|
+
* child, so a session appears, exits ~12ms later with code 1 and no output,
|
|
3824
|
+
* and the caller is told only that it "exited before becoming ready" — or,
|
|
3825
|
+
* on the Claude resume path, is told nothing at all, since that path answers
|
|
3826
|
+
* 200 before the process has had a chance to die. Every start route funnels
|
|
3827
|
+
* through here, so one check covers start, resume, adopt and fork.
|
|
3828
|
+
*
|
|
3829
|
+
* 503, not 500: the request was well-formed and the fault is this machine's
|
|
3830
|
+
* environment. `code` is what mobile branches on (it reads `errBody.code`),
|
|
3831
|
+
* and `PROVIDER_NOT_INSTALLED` is a remediation string it already knows.
|
|
3832
|
+
*/
|
|
3833
|
+
assertProviderInstalled(provider) {
|
|
3834
|
+
if (locateProviderExe(provider) !== null) return;
|
|
3835
|
+
const command = provider === CODEX_CLI_PROVIDER ? "codex" : "claude";
|
|
3836
|
+
const err = new Error(
|
|
3837
|
+
`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.`
|
|
3838
|
+
);
|
|
3839
|
+
err.statusCode = 503;
|
|
3840
|
+
err.code = "PROVIDER_NOT_INSTALLED";
|
|
3841
|
+
throw err;
|
|
3842
|
+
}
|
|
3781
3843
|
assertSupportedProvider(provider, projectPath) {
|
|
3782
3844
|
if (this.remoteRunner) return this.remoteRunner;
|
|
3783
3845
|
const runner = this.runners.get(provider);
|
|
@@ -4460,7 +4522,9 @@ var corsMiddleware = (configValue) => {
|
|
|
4460
4522
|
// src/api/middleware/error.middleware.ts
|
|
4461
4523
|
var errorMiddleware = (err, c) => {
|
|
4462
4524
|
const message = err instanceof Error ? err.message : "Internal server error";
|
|
4463
|
-
|
|
4525
|
+
const { statusCode, code } = err;
|
|
4526
|
+
const status = typeof statusCode === "number" && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
|
|
4527
|
+
return c.json(typeof code === "string" ? { error: message, code } : { error: message }, status);
|
|
4464
4528
|
};
|
|
4465
4529
|
|
|
4466
4530
|
// src/api/routes/backup.routes.ts
|
|
@@ -5015,24 +5079,26 @@ function computeBootToken() {
|
|
|
5015
5079
|
}
|
|
5016
5080
|
|
|
5017
5081
|
// src/api/routes/diagnostics.routes.ts
|
|
5018
|
-
function providerCheck(name
|
|
5082
|
+
function providerCheck(name) {
|
|
5019
5083
|
try {
|
|
5020
|
-
const exe =
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5084
|
+
const exe = locateProviderExe(name);
|
|
5085
|
+
if (exe !== null) {
|
|
5086
|
+
return {
|
|
5087
|
+
id: `provider:${name}`,
|
|
5088
|
+
status: "ok",
|
|
5089
|
+
summary: `${name} CLI is installed.`,
|
|
5090
|
+
remediation: "NONE",
|
|
5091
|
+
detail: { location: redactPath(exe) }
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
5028
5094
|
} catch {
|
|
5029
|
-
return {
|
|
5030
|
-
id: `provider:${name}`,
|
|
5031
|
-
status: "failed",
|
|
5032
|
-
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
5033
|
-
remediation: "PROVIDER_NOT_INSTALLED"
|
|
5034
|
-
};
|
|
5035
5095
|
}
|
|
5096
|
+
return {
|
|
5097
|
+
id: `provider:${name}`,
|
|
5098
|
+
status: "failed",
|
|
5099
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
5100
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
5101
|
+
};
|
|
5036
5102
|
}
|
|
5037
5103
|
var createDiagnosticsRoutes = (deps) => {
|
|
5038
5104
|
const app = new import_hono8.Hono();
|
|
@@ -5045,8 +5111,8 @@ var createDiagnosticsRoutes = (deps) => {
|
|
|
5045
5111
|
remediation: "NONE",
|
|
5046
5112
|
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
5047
5113
|
});
|
|
5048
|
-
checks.push(providerCheck(
|
|
5049
|
-
checks.push(providerCheck(
|
|
5114
|
+
checks.push(providerCheck(CLAUDE_CODE_PROVIDER));
|
|
5115
|
+
checks.push(providerCheck(CODEX_CLI_PROVIDER));
|
|
5050
5116
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
5051
5117
|
checks.push(
|
|
5052
5118
|
cacheAlert ? {
|
|
@@ -6118,7 +6184,10 @@ function parseVersionOutput(output) {
|
|
|
6118
6184
|
const match = output.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
|
|
6119
6185
|
return match ? match[0] : null;
|
|
6120
6186
|
}
|
|
6187
|
+
var versionByExe = /* @__PURE__ */ new Map();
|
|
6121
6188
|
function runVersion(exe) {
|
|
6189
|
+
const cached3 = versionByExe.get(exe);
|
|
6190
|
+
if (cached3 !== void 0) return Promise.resolve(cached3);
|
|
6122
6191
|
const viaShell = isWindows && /\.(?:cmd|bat)$/i.test(exe);
|
|
6123
6192
|
const file = viaShell ? `"${exe}"` : exe;
|
|
6124
6193
|
return new Promise((resolve2) => {
|
|
@@ -6128,7 +6197,9 @@ function runVersion(exe) {
|
|
|
6128
6197
|
{ timeout: VERSION_TIMEOUT_MS, shell: viaShell, windowsHide: true },
|
|
6129
6198
|
(err, stdout, stderr) => {
|
|
6130
6199
|
if (err && !stdout && !stderr) return resolve2(null);
|
|
6131
|
-
|
|
6200
|
+
const version = parseVersionOutput(`${stdout}${stderr}`);
|
|
6201
|
+
if (version !== null) versionByExe.set(exe, version);
|
|
6202
|
+
resolve2(version);
|
|
6132
6203
|
}
|
|
6133
6204
|
);
|
|
6134
6205
|
});
|
|
@@ -6172,13 +6243,16 @@ function compareSemver(a, b) {
|
|
|
6172
6243
|
if (pb.pre === null) return -1;
|
|
6173
6244
|
return pa.pre < pb.pre ? -1 : 1;
|
|
6174
6245
|
}
|
|
6175
|
-
async function providerHealth(name,
|
|
6246
|
+
async function providerHealth(name, locateExe = () => locateProviderExe(name), detect = runVersion) {
|
|
6176
6247
|
const verifiedAgainst = VERIFIED_AGAINST[name];
|
|
6177
6248
|
const capabilities = capabilitiesFor(name);
|
|
6178
|
-
let exe;
|
|
6249
|
+
let exe = null;
|
|
6179
6250
|
try {
|
|
6180
|
-
exe =
|
|
6251
|
+
exe = locateExe();
|
|
6181
6252
|
} catch {
|
|
6253
|
+
exe = null;
|
|
6254
|
+
}
|
|
6255
|
+
if (exe === null) {
|
|
6182
6256
|
return {
|
|
6183
6257
|
name,
|
|
6184
6258
|
available: false,
|
|
@@ -6215,8 +6289,8 @@ var createProviderRoutes = () => {
|
|
|
6215
6289
|
const app = new import_hono14.Hono();
|
|
6216
6290
|
app.get("/", async (c) => {
|
|
6217
6291
|
const providers = await Promise.all([
|
|
6218
|
-
providerHealth(CLAUDE_CODE_PROVIDER
|
|
6219
|
-
providerHealth(CODEX_CLI_PROVIDER
|
|
6292
|
+
providerHealth(CLAUDE_CODE_PROVIDER),
|
|
6293
|
+
providerHealth(CODEX_CLI_PROVIDER)
|
|
6220
6294
|
]);
|
|
6221
6295
|
return c.json({ providers });
|
|
6222
6296
|
});
|
|
@@ -10687,7 +10761,10 @@ var SessionHandlers = class {
|
|
|
10687
10761
|
});
|
|
10688
10762
|
this.sessionStore.addManaged(session);
|
|
10689
10763
|
this.registryBoot.recordSessionSpawn(session);
|
|
10690
|
-
const { outcome } = await this.deps.waitForStartupOutcome(
|
|
10764
|
+
const { outcome, session: settled } = await this.deps.waitForStartupOutcome(
|
|
10765
|
+
session.id,
|
|
10766
|
+
START_READY_TIMEOUT_MS
|
|
10767
|
+
);
|
|
10691
10768
|
const current = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());
|
|
10692
10769
|
if (outcome === "ready" && current) {
|
|
10693
10770
|
json(res, 200, { session: current });
|
|
@@ -10695,7 +10772,7 @@ var SessionHandlers = class {
|
|
|
10695
10772
|
json(res, 502, {
|
|
10696
10773
|
id: session.id,
|
|
10697
10774
|
status: "idle",
|
|
10698
|
-
error:
|
|
10775
|
+
error: settled?.failureReason ?? "Session exited before becoming ready"
|
|
10699
10776
|
});
|
|
10700
10777
|
} else {
|
|
10701
10778
|
json(res, 202, { id: session.id, status: "pending" });
|
|
@@ -10709,11 +10786,16 @@ var SessionHandlers = class {
|
|
|
10709
10786
|
} catch (err) {
|
|
10710
10787
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
10711
10788
|
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
10789
|
+
const code = err.code;
|
|
10712
10790
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
10713
10791
|
event: "session.start_failed",
|
|
10714
10792
|
error: message
|
|
10715
10793
|
});
|
|
10716
|
-
json(
|
|
10794
|
+
json(
|
|
10795
|
+
res,
|
|
10796
|
+
statusCode,
|
|
10797
|
+
typeof code === "string" ? { error: message, code } : { error: message }
|
|
10798
|
+
);
|
|
10717
10799
|
}
|
|
10718
10800
|
}
|
|
10719
10801
|
async handleSetSessionName(sessionId, req, res) {
|
|
@@ -11398,7 +11480,7 @@ var PairTokenStore = class {
|
|
|
11398
11480
|
};
|
|
11399
11481
|
}
|
|
11400
11482
|
/**
|
|
11401
|
-
*
|
|
11483
|
+
* What `consume` would answer right now, WITHOUT spending the token.
|
|
11402
11484
|
*
|
|
11403
11485
|
* Exists so a caller can reject a bad token before doing any work, and still
|
|
11404
11486
|
* spend the token only once the work has succeeded. A pair token is
|
|
@@ -11407,10 +11489,14 @@ var PairTokenStore = class {
|
|
|
11407
11489
|
* indistinguishable from an attacker replaying a photographed code, which is
|
|
11408
11490
|
* the one signal `design.md` §2.6 designates as replay detection.
|
|
11409
11491
|
*
|
|
11410
|
-
*
|
|
11411
|
-
*
|
|
11492
|
+
* **Named for its relationship to `consume`, deliberately.** It reserves
|
|
11493
|
+
* nothing and takes no lock, so two callers can both be told `{ ok: true }`
|
|
11494
|
+
* for the same token. A name like `verify` would read as a gate that had
|
|
11495
|
+
* decided something, and inviting a caller to act on this alone is precisely
|
|
11496
|
+
* the misuse the ordering it enables exists to prevent. A reader who sees
|
|
11497
|
+
* `wouldConsume` asks where the `consume` is, which is the right question.
|
|
11412
11498
|
*/
|
|
11413
|
-
|
|
11499
|
+
wouldConsume(token) {
|
|
11414
11500
|
const result = this.check(token);
|
|
11415
11501
|
return result.ok ? { ok: true } : result;
|
|
11416
11502
|
}
|
|
@@ -12334,7 +12420,15 @@ function createLiveSessionOptions(deps) {
|
|
|
12334
12420
|
// grace-timer/idle-reaper hold (statusSource "shutdown") apart from a
|
|
12335
12421
|
// genuine process exit ("process-exit"), and reports both as
|
|
12336
12422
|
// `lifecycle: "completed"`. See managedToResponse in session-store.ts.
|
|
12337
|
-
...session.statusSource != null && { statusSource: session.statusSource }
|
|
12423
|
+
...session.statusSource != null && { statusSource: session.statusSource },
|
|
12424
|
+
// Why a session died, not just that it did. Without this the store's
|
|
12425
|
+
// copy has no failureReason, so managedToResponse falls through to
|
|
12426
|
+
// `lifecycle: "completed"` (see session-store.ts) and a session that
|
|
12427
|
+
// never started — missing CLI, missing project dir — is reported to
|
|
12428
|
+
// every client as one that finished normally. Guarded like its
|
|
12429
|
+
// neighbours: a later transition must not blank a recorded failure.
|
|
12430
|
+
...session.failureReason != null && { failureReason: session.failureReason },
|
|
12431
|
+
...session.failureCode != null && { failureCode: session.failureCode }
|
|
12338
12432
|
});
|
|
12339
12433
|
deps.managedSessionsRepo()?.recordStatus(
|
|
12340
12434
|
session.id,
|
|
@@ -15789,6 +15883,41 @@ var StreamerServer = class {
|
|
|
15789
15883
|
json(res, 503, body);
|
|
15790
15884
|
return true;
|
|
15791
15885
|
}
|
|
15886
|
+
/**
|
|
15887
|
+
* Say which provider CLIs this machine can actually launch.
|
|
15888
|
+
*
|
|
15889
|
+
* The operator cannot discover this case unaided: under launchd/Task
|
|
15890
|
+
* Scheduler the service inherits a stripped PATH, so a CLI that works
|
|
15891
|
+
* perfectly in their terminal is invisible to the service, and every session
|
|
15892
|
+
* start dies milliseconds in. `/api/diagnostics` answers it too, but only for
|
|
15893
|
+
* someone who already suspects it.
|
|
15894
|
+
*
|
|
15895
|
+
* Availability only, never a version — `--version` costs a process spawn per
|
|
15896
|
+
* provider (85ms for claude here) and belongs on the first request that wants
|
|
15897
|
+
* it, not on boot.
|
|
15898
|
+
*
|
|
15899
|
+
* Called AFTER the port is bound, which is not cosmetic. This is the first
|
|
15900
|
+
* caller of the exe resolvers in the process, so the memo is cold by
|
|
15901
|
+
* definition and each provider pays one synchronous `which` / `where.exe`
|
|
15902
|
+
* (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing.
|
|
15903
|
+
* Windows is the risk — `where.exe` is slower, `execFileSync` blocks the
|
|
15904
|
+
* event loop, and Task Scheduler's stripped PATH is exactly where a miss
|
|
15905
|
+
* pays the full timeout — so the worst case is ~6s of two blocking lookups.
|
|
15906
|
+
* After `listen()` that delays the first requests on a box that cannot start
|
|
15907
|
+
* a session anyway; before it, it would have delayed binding the port.
|
|
15908
|
+
*/
|
|
15909
|
+
logProviderAvailability() {
|
|
15910
|
+
for (const provider of [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER]) {
|
|
15911
|
+
if (locateProviderExe(provider)) {
|
|
15912
|
+
this.log.info(`Provider ${provider}: found`, { event: "config.provider", provider });
|
|
15913
|
+
} else {
|
|
15914
|
+
this.log.warn(`Provider ${provider}: not found on PATH \u2014 sessions cannot start`, {
|
|
15915
|
+
event: "config.provider_missing",
|
|
15916
|
+
provider
|
|
15917
|
+
});
|
|
15918
|
+
}
|
|
15919
|
+
}
|
|
15920
|
+
}
|
|
15792
15921
|
async listen(port, opts) {
|
|
15793
15922
|
if (this.featureFlags.ptyHost) {
|
|
15794
15923
|
try {
|
|
@@ -15849,6 +15978,7 @@ var StreamerServer = class {
|
|
|
15849
15978
|
event: "server.listening",
|
|
15850
15979
|
...this.host !== void 0 && { host: this.host }
|
|
15851
15980
|
});
|
|
15981
|
+
this.logProviderAvailability();
|
|
15852
15982
|
try {
|
|
15853
15983
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
15854
15984
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
@@ -16225,8 +16355,14 @@ var StreamerServer = class {
|
|
|
16225
16355
|
json(res, 400, { error: "Missing token or clientPublicKey" });
|
|
16226
16356
|
return;
|
|
16227
16357
|
}
|
|
16228
|
-
const precheck = this.pairTokens.
|
|
16358
|
+
const precheck = this.pairTokens.wouldConsume(token);
|
|
16229
16359
|
if (!precheck.ok) {
|
|
16360
|
+
if (precheck.reason === "used") {
|
|
16361
|
+
this.log.warn(
|
|
16362
|
+
"[pair] a pair token was replayed. If you did not just pair a device, check the paired-devices list and revoke anything you do not recognise.",
|
|
16363
|
+
{ event: "pair.token_replayed", ip }
|
|
16364
|
+
);
|
|
16365
|
+
}
|
|
16230
16366
|
json(res, 401, { error: `Pair token ${precheck.reason}` });
|
|
16231
16367
|
return;
|
|
16232
16368
|
}
|