@triflux/remote 10.43.0 → 10.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cto/hygiene.mjs +1 -1
- package/hub/lib/cto-env.mjs +1 -0
- package/hub/pipe.mjs +10 -9
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +276 -132
- package/hub/store.mjs +699 -41
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/conductor.mjs +101 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +50 -9
- package/hub/team/headless.mjs +539 -92
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/launcher-template.mjs +17 -1
- package/hub/team/notify.mjs +31 -11
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +41 -13
- package/hub/team/session.mjs +57 -0
- package/hub/team/swarm-hypervisor.mjs +4 -2
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +22 -1
- package/hub/workers/codex-mcp.mjs +26 -5
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +367 -0
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +48 -4
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +24 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// triflux machine profile 공용 리더 (dep-free)
|
|
2
|
+
//
|
|
3
|
+
// machine profile 은 이 머신에서 어떤 CLI 실행을 허용했는지 기록한 정책 파일이다.
|
|
4
|
+
// 지금까지 판독 로직이 scripts/setup.mjs(JS)와 scripts/tfx-route.sh(bash) 두 벌로
|
|
5
|
+
// 갈라져 있었다. 이 모듈이 JS 쪽 단일 정본이고, bash 판독과 같은 의미를 유지한다.
|
|
6
|
+
//
|
|
7
|
+
// 신뢰 경계: 파일을 source 하거나 eval 하지 않는다. allowlist key 와 안전한
|
|
8
|
+
// literal 값만 읽으므로 셸 표현식은 실행되지 않는다.
|
|
9
|
+
//
|
|
10
|
+
// 부수효과: 모듈 로드 시 파일을 읽지 않고 환경도 바꾸지 않는다. hub/lib/cto-env.mjs
|
|
11
|
+
// 처럼 순수 함수만 노출하므로 HUD, hook, hub 어느 소비자에서도 안전하게 import 한다.
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
export const MACHINE_PROFILE_FILENAME = "machine-profile.env";
|
|
18
|
+
|
|
19
|
+
export const MACHINE_PROFILE_KEYS = Object.freeze([
|
|
20
|
+
"TFX_MACHINE_PROFILE_VERSION",
|
|
21
|
+
"TFX_MACHINE_OS",
|
|
22
|
+
"TFX_MULTIPLEXER_POLICY",
|
|
23
|
+
"TFX_DISABLE_CODEX",
|
|
24
|
+
"TFX_DISABLE_ANTIGRAVITY",
|
|
25
|
+
"TFX_TIMEOUT_POLICY",
|
|
26
|
+
"TFX_HARD_CEILING_SEC",
|
|
27
|
+
"TFX_STALL_THRESHOLD",
|
|
28
|
+
"TFX_STALL_KILL",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const MACHINE_PROFILE_KEY_SET = new Set(MACHINE_PROFILE_KEYS);
|
|
32
|
+
|
|
33
|
+
export const MACHINE_PROFILE_VALUE_RE = /^[A-Za-z0-9_.-]+$/u;
|
|
34
|
+
|
|
35
|
+
// CLI 이름과 disable 키의 대응. 이 두 개가 TFX_DISABLE_* SSOT 의 전부다.
|
|
36
|
+
const CLI_DISABLE_KEYS = Object.freeze({
|
|
37
|
+
codex: "TFX_DISABLE_CODEX",
|
|
38
|
+
antigravity: "TFX_DISABLE_ANTIGRAVITY",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 호출자가 쓰는 CLI 표기를 정책 키 두 개로 접는다. agy 와 gemini 는 antigravity
|
|
42
|
+
// 레인의 다른 이름이므로 같은 정책을 적용한다.
|
|
43
|
+
const CLI_ALIASES = Object.freeze({
|
|
44
|
+
codex: "codex",
|
|
45
|
+
antigravity: "antigravity",
|
|
46
|
+
agy: "antigravity",
|
|
47
|
+
gemini: "antigravity",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* setup.mjs 가 쓰던 home 해석과 같은 결과를 낸다.
|
|
52
|
+
*
|
|
53
|
+
* Windows 의 os.homedir() 는 USERPROFILE 만 보고 process.env.HOME swap 을 무시해서,
|
|
54
|
+
* fixture 로 격리한 자식 프로세스가 실제 홈의 설정을 건드릴 수 있다 (#193 회귀).
|
|
55
|
+
* 우선순위: TRIFLUX_TEST_HOME, 그다음 win32 면 USERPROFILE, HOME 순, 마지막이 homedir().
|
|
56
|
+
*
|
|
57
|
+
* @param {{ env?: NodeJS.ProcessEnv, platform?: string }} [options]
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function resolveTrifluxHome({
|
|
61
|
+
env = process.env,
|
|
62
|
+
platform = process.platform,
|
|
63
|
+
} = {}) {
|
|
64
|
+
if (env?.TRIFLUX_TEST_HOME) return env.TRIFLUX_TEST_HOME;
|
|
65
|
+
if (platform === "win32") {
|
|
66
|
+
return env?.USERPROFILE || env?.HOME || homedir();
|
|
67
|
+
}
|
|
68
|
+
return env?.HOME || homedir();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* machine profile 파일 경로를 해석한다. TFX_MACHINE_PROFILE_PATH 가 있으면
|
|
73
|
+
* 그것만 쓰고, 없으면 OS 별 user-state 경로를 만든다.
|
|
74
|
+
*
|
|
75
|
+
* @param {{ platform?: string, env?: NodeJS.ProcessEnv, home?: string }} [options]
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function resolveMachineProfilePath({
|
|
79
|
+
platform = process.platform,
|
|
80
|
+
env = process.env,
|
|
81
|
+
home,
|
|
82
|
+
} = {}) {
|
|
83
|
+
if (env?.TFX_MACHINE_PROFILE_PATH) {
|
|
84
|
+
return resolve(env.TFX_MACHINE_PROFILE_PATH);
|
|
85
|
+
}
|
|
86
|
+
const resolvedHome = home ?? resolveTrifluxHome({ env, platform });
|
|
87
|
+
if (platform === "win32") {
|
|
88
|
+
const appData = env?.APPDATA || join(resolvedHome, "AppData", "Roaming");
|
|
89
|
+
return join(appData, "triflux", MACHINE_PROFILE_FILENAME);
|
|
90
|
+
}
|
|
91
|
+
const configRoot = env?.XDG_CONFIG_HOME || join(resolvedHome, ".config");
|
|
92
|
+
return join(configRoot, "triflux", MACHINE_PROFILE_FILENAME);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* KEY=VALUE 본문을 allowlist 기준으로 파싱한다. 허용되지 않은 key 와 안전하지
|
|
97
|
+
* 않은 literal 은 값으로 인정하지 않고 warnings 에만 남긴다.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} content
|
|
100
|
+
* @returns {{ values: Record<string, string>, warnings: string[] }}
|
|
101
|
+
*/
|
|
102
|
+
export function parseMachineProfileContent(content) {
|
|
103
|
+
const values = {};
|
|
104
|
+
const warnings = [];
|
|
105
|
+
const lines = String(content || "").split(/\r?\n/u);
|
|
106
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
107
|
+
const line = lines[index].trim();
|
|
108
|
+
if (!line || line.startsWith("#")) continue;
|
|
109
|
+
const separator = line.indexOf("=");
|
|
110
|
+
if (separator <= 0) {
|
|
111
|
+
warnings.push(`line ${index + 1}: KEY=VALUE 형식이 아님`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = line.slice(0, separator).trim();
|
|
115
|
+
const value = line.slice(separator + 1).trim();
|
|
116
|
+
if (!MACHINE_PROFILE_KEY_SET.has(key)) {
|
|
117
|
+
warnings.push(`line ${index + 1}: 허용되지 않은 key ${key}`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!value || !MACHINE_PROFILE_VALUE_RE.test(value)) {
|
|
121
|
+
warnings.push(`line ${index + 1}: ${key} 값이 안전한 literal이 아님`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
values[key] = value;
|
|
125
|
+
}
|
|
126
|
+
return { values, warnings };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* machine profile 파일을 읽는다. 파일이 없거나 읽기에 실패해도 예외를 던지지
|
|
131
|
+
* 않는다. HUD 같은 소비자가 프로파일 부재로 죽으면 안 되기 때문이다.
|
|
132
|
+
*
|
|
133
|
+
* @param {{ env?: NodeJS.ProcessEnv, platform?: string, home?: string }} [options]
|
|
134
|
+
* @returns {{ path: string, exists: boolean, values: Record<string, string>, warnings: string[] }}
|
|
135
|
+
*/
|
|
136
|
+
export function readMachineProfile({
|
|
137
|
+
env = process.env,
|
|
138
|
+
platform = process.platform,
|
|
139
|
+
home,
|
|
140
|
+
} = {}) {
|
|
141
|
+
const path = resolveMachineProfilePath({ platform, env, home });
|
|
142
|
+
let content;
|
|
143
|
+
try {
|
|
144
|
+
content = readFileSync(path, "utf8");
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const warnings =
|
|
147
|
+
error?.code === "ENOENT"
|
|
148
|
+
? []
|
|
149
|
+
: [`machine profile 읽기 실패: ${error?.message || String(error)}`];
|
|
150
|
+
return { path, exists: false, values: {}, warnings };
|
|
151
|
+
}
|
|
152
|
+
const parsed = parseMachineProfileContent(content);
|
|
153
|
+
return {
|
|
154
|
+
path,
|
|
155
|
+
exists: true,
|
|
156
|
+
values: parsed.values,
|
|
157
|
+
warnings: parsed.warnings,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// env 에 키가 존재하는지 판정한다. bash 의 `${VAR+x}` 와 같은 의미라서 값이 빈
|
|
162
|
+
// 문자열이어도 "존재"로 본다. 다만 값이 undefined 인 항목은 child_process 가
|
|
163
|
+
// 자식에게 넘기지 않으므로 부재로 취급한다.
|
|
164
|
+
function hasEnvKey(env, key) {
|
|
165
|
+
if (!env) return false;
|
|
166
|
+
if (!Object.hasOwn(env, key)) return false;
|
|
167
|
+
return env[key] !== undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// env 원값은 셸이 준 임의 문자열이라 줄바꿈이나 제어문자가 섞일 수 있다. 경고
|
|
171
|
+
// 문장은 세션 문맥 한 줄에 그대로 실리므로 공백으로 접고 길이를 자른다.
|
|
172
|
+
function describeUnsafeValue(value) {
|
|
173
|
+
const folded = String(value ?? "")
|
|
174
|
+
.replace(/[\p{Cc}\s]+/gu, " ")
|
|
175
|
+
.trim();
|
|
176
|
+
return folded.length > 40 ? `${folded.slice(0, 40)}...` : folded;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* CLI 실행 허용 정책을 해석한다. 우선순위는 scripts/tfx-route.sh 와 같다:
|
|
181
|
+
* 런타임 env 에 키가 있으면 그 값, 없으면 machine profile, 둘 다 없으면 "0".
|
|
182
|
+
*
|
|
183
|
+
* 값 해석도 bash 와 같다. 정확히 "1" 이면 차단이고, "0" 과 빈 문자열은 허용이다.
|
|
184
|
+
* tfx-route.sh 는 그 밖의 값에서 exit 하지만, 여기서는 허용으로 두고 경고만
|
|
185
|
+
* 남긴다. HUD 나 hook 같은 소비자가 오타 하나로 죽으면 안 되기 때문이다.
|
|
186
|
+
*
|
|
187
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
188
|
+
* @param {{ profile?: ReturnType<typeof readMachineProfile>, platform?: string, home?: string }} [opts]
|
|
189
|
+
*/
|
|
190
|
+
export function resolveCliPolicy(env = process.env, opts = {}) {
|
|
191
|
+
const profile =
|
|
192
|
+
opts.profile ??
|
|
193
|
+
readMachineProfile({
|
|
194
|
+
env,
|
|
195
|
+
platform: opts.platform ?? process.platform,
|
|
196
|
+
home: opts.home,
|
|
197
|
+
});
|
|
198
|
+
const warnings = [...(profile?.warnings || [])];
|
|
199
|
+
const disabled = {};
|
|
200
|
+
const source = {};
|
|
201
|
+
|
|
202
|
+
for (const cli of Object.keys(CLI_DISABLE_KEYS)) {
|
|
203
|
+
const key = CLI_DISABLE_KEYS[cli];
|
|
204
|
+
let raw;
|
|
205
|
+
if (hasEnvKey(env, key)) {
|
|
206
|
+
raw = env[key];
|
|
207
|
+
source[cli] = "env";
|
|
208
|
+
} else if (Object.hasOwn(profile?.values || {}, key)) {
|
|
209
|
+
raw = profile.values[key];
|
|
210
|
+
source[cli] = "profile";
|
|
211
|
+
} else {
|
|
212
|
+
raw = "0";
|
|
213
|
+
source[cli] = "default";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const normalized = String(raw ?? "").trim();
|
|
217
|
+
if (normalized === "1") {
|
|
218
|
+
disabled[cli] = true;
|
|
219
|
+
} else if (normalized === "0" || normalized === "") {
|
|
220
|
+
disabled[cli] = false;
|
|
221
|
+
} else {
|
|
222
|
+
disabled[cli] = false;
|
|
223
|
+
warnings.push(
|
|
224
|
+
`${key} 값은 0 또는 1이어야 함 (현재: ${describeUnsafeValue(normalized)}, source: ${source[cli]}). 허용으로 둔다.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
codexDisabled: disabled.codex,
|
|
231
|
+
antigravityDisabled: disabled.antigravity,
|
|
232
|
+
source: { codex: source.codex, antigravity: source.antigravity },
|
|
233
|
+
profilePath: profile?.exists ? profile.path : null,
|
|
234
|
+
warnings,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* CLI 표기를 정책 키 이름으로 정규화한다. 모르는 이름은 null 이다.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} cli
|
|
242
|
+
* @returns {"codex"|"antigravity"|null}
|
|
243
|
+
*/
|
|
244
|
+
export function normalizeCliName(cli) {
|
|
245
|
+
const key = String(cli ?? "")
|
|
246
|
+
.trim()
|
|
247
|
+
.toLowerCase();
|
|
248
|
+
return CLI_ALIASES[key] || null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 세션 문맥 주입용 한 줄. 형식은 고정이며 소비자가 파싱하지 않고 그대로 보여준다.
|
|
253
|
+
*
|
|
254
|
+
* @param {ReturnType<typeof resolveCliPolicy>} policy
|
|
255
|
+
* @returns {string}
|
|
256
|
+
*/
|
|
257
|
+
export function formatCliPolicyLine(policy) {
|
|
258
|
+
const codex = policy?.codexDisabled ? "off" : "on";
|
|
259
|
+
const antigravity = policy?.antigravityDisabled ? "off" : "on";
|
|
260
|
+
const codexSource = policy?.source?.codex || "default";
|
|
261
|
+
const antigravitySource = policy?.source?.antigravity || "default";
|
|
262
|
+
return `cli-policy: codex=${codex} antigravity=${antigravity} (SSOT: TFX_DISABLE_*; codex<-${codexSource}, antigravity<-${antigravitySource})`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 차단된 CLI 실행 요청에 쓸 오류를 만든다. throw 는 호출자가 한다. 정책상 허용된
|
|
267
|
+
* CLI 이거나 모르는 이름이면 null 을 준다.
|
|
268
|
+
*
|
|
269
|
+
* 메시지에 원인과 해제 방법을 모두 넣는다. tfx-route.sh 의 exit 78 정책과 같은
|
|
270
|
+
* 정신이며, 조용한 강등을 금지한다.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} cli
|
|
273
|
+
* @param {ReturnType<typeof resolveCliPolicy>} policy
|
|
274
|
+
* @returns {Error|null}
|
|
275
|
+
*/
|
|
276
|
+
export function buildDisabledCliError(cli, policy) {
|
|
277
|
+
const name = normalizeCliName(cli);
|
|
278
|
+
if (!name) return null;
|
|
279
|
+
const isDisabled =
|
|
280
|
+
name === "codex" ? policy?.codexDisabled : policy?.antigravityDisabled;
|
|
281
|
+
if (!isDisabled) return null;
|
|
282
|
+
|
|
283
|
+
const key = CLI_DISABLE_KEYS[name];
|
|
284
|
+
const source = policy?.source?.[name] || "profile";
|
|
285
|
+
const error = new Error(
|
|
286
|
+
`${key}=1 (source: ${source}): ${name} 실행이 machine profile 정책으로 차단됨. ` +
|
|
287
|
+
`이번 실행만 허용하려면 ${key}=0, 영구 변경은 node scripts/setup.mjs --machine-profile-only 로 프로파일을 다시 캡처한다. ` +
|
|
288
|
+
"Claude native 로 조용히 강등하지 않는다.",
|
|
289
|
+
);
|
|
290
|
+
error.code = "TFX_CLI_DISABLED";
|
|
291
|
+
error.cli = name;
|
|
292
|
+
return error;
|
|
293
|
+
}
|
|
@@ -345,6 +345,35 @@ function isValidInitResponse(line) {
|
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
function validateHttpInitResponse(msg) {
|
|
349
|
+
if (msg?.jsonrpc !== "2.0") return "http:not-jsonrpc";
|
|
350
|
+
if (msg.id !== 1 && msg.id !== "1") return "http:id-mismatch";
|
|
351
|
+
if (msg.result === undefined && msg.error === undefined) {
|
|
352
|
+
return "http:no-result";
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseSseInitResponse(body) {
|
|
358
|
+
let failureReason = "http:no-jsonrpc-body";
|
|
359
|
+
for (const event of body.split(/\r?\n\r?\n/)) {
|
|
360
|
+
const data = event
|
|
361
|
+
.split(/\r?\n/)
|
|
362
|
+
.filter((line) => line.startsWith("data:"))
|
|
363
|
+
.map((line) => line.slice("data:".length).replace(/^\s/, ""))
|
|
364
|
+
.join("\n");
|
|
365
|
+
if (!data) continue;
|
|
366
|
+
try {
|
|
367
|
+
const reason = validateHttpInitResponse(JSON.parse(data));
|
|
368
|
+
if (!reason) return null;
|
|
369
|
+
failureReason = reason;
|
|
370
|
+
} catch {
|
|
371
|
+
failureReason = "http:invalid-json";
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return failureReason;
|
|
375
|
+
}
|
|
376
|
+
|
|
348
377
|
export function probeStdio(def, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
|
349
378
|
return new Promise((resolve) => {
|
|
350
379
|
const start = Date.now();
|
|
@@ -431,7 +460,10 @@ export async function probeHttp(url, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
|
|
431
460
|
try {
|
|
432
461
|
const res = await fetch(url, {
|
|
433
462
|
method: "POST",
|
|
434
|
-
headers: {
|
|
463
|
+
headers: {
|
|
464
|
+
accept: "application/json, text/event-stream",
|
|
465
|
+
"content-type": "application/json",
|
|
466
|
+
},
|
|
435
467
|
body: makeInitializeRequest().trim(),
|
|
436
468
|
signal: AbortSignal.timeout(timeoutMs),
|
|
437
469
|
});
|
|
@@ -445,26 +477,20 @@ export async function probeHttp(url, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
|
|
445
477
|
} catch {
|
|
446
478
|
return { alive: false, reason: "http:body-read-failed", ms };
|
|
447
479
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.
|
|
451
|
-
.
|
|
452
|
-
|
|
453
|
-
|
|
480
|
+
const contentType = (res.headers.get("content-type") || "")
|
|
481
|
+
.split(";", 1)[0]
|
|
482
|
+
.trim()
|
|
483
|
+
.toLowerCase();
|
|
484
|
+
if (contentType === "text/event-stream") {
|
|
485
|
+
const reason = parseSseInitResponse(body);
|
|
486
|
+
return reason ? { alive: false, reason, ms } : { alive: true, ms };
|
|
487
|
+
}
|
|
488
|
+
if (contentType !== "application/json") {
|
|
454
489
|
return { alive: false, reason: "http:no-jsonrpc-body", ms };
|
|
455
490
|
}
|
|
456
491
|
try {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
return { alive: false, reason: "http:not-jsonrpc", ms };
|
|
460
|
-
}
|
|
461
|
-
if (msg.id !== 1 && msg.id !== "1") {
|
|
462
|
-
return { alive: false, reason: "http:id-mismatch", ms };
|
|
463
|
-
}
|
|
464
|
-
if (msg.result === undefined && msg.error === undefined) {
|
|
465
|
-
return { alive: false, reason: "http:no-result", ms };
|
|
466
|
-
}
|
|
467
|
-
return { alive: true, ms };
|
|
492
|
+
const reason = validateHttpInitResponse(JSON.parse(body));
|
|
493
|
+
return reason ? { alive: false, reason, ms } : { alive: true, ms };
|
|
468
494
|
} catch {
|
|
469
495
|
return { alive: false, reason: "http:invalid-json", ms };
|
|
470
496
|
}
|