@synap-core/cli 1.12.2 → 1.13.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/LICENSE +21 -0
- package/README.md +25 -8
- package/dist/build-info.json +2 -2
- package/dist/commands/agents.d.ts +0 -1
- package/dist/commands/agents.js +1 -1
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/app.js +2 -1
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/automation.js +46 -41
- package/dist/commands/automation.js.map +1 -1
- package/dist/commands/box.d.ts +42 -0
- package/dist/commands/box.js +89 -0
- package/dist/commands/box.js.map +1 -0
- package/dist/commands/bridge-setup.js +3 -1
- package/dist/commands/bridge-setup.js.map +1 -1
- package/dist/commands/capability.d.ts +1 -1
- package/dist/commands/capability.js +57 -34
- package/dist/commands/capability.js.map +1 -1
- package/dist/commands/close-questions.d.ts +27 -0
- package/dist/commands/close-questions.js +49 -0
- package/dist/commands/close-questions.js.map +1 -0
- package/dist/commands/close-questions.test.d.ts +1 -0
- package/dist/commands/close-questions.test.js +52 -0
- package/dist/commands/close-questions.test.js.map +1 -0
- package/dist/commands/connect.d.ts +1 -3
- package/dist/commands/connect.js +2 -2
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/context.js +72 -43
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/data-extra.js +12 -4
- package/dist/commands/data-extra.js.map +1 -1
- package/dist/commands/data.js +2 -1
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.js +108 -13
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hooks.d.ts +19 -0
- package/dist/commands/hooks.js +70 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/import.d.ts +25 -0
- package/dist/commands/import.js +98 -12
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +59 -19
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/knowledge.js +4 -2
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/lens.js +2 -1
- package/dist/commands/lens.js.map +1 -1
- package/dist/commands/market-authoring.js +1 -1
- package/dist/commands/market-authoring.js.map +1 -1
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/observe.category.test.d.ts +1 -0
- package/dist/commands/observe.category.test.js +59 -0
- package/dist/commands/observe.category.test.js.map +1 -0
- package/dist/commands/observe.d.ts +3 -0
- package/dist/commands/observe.js +7 -5
- package/dist/commands/observe.js.map +1 -1
- package/dist/commands/observe.title.test.d.ts +1 -0
- package/dist/commands/observe.title.test.js +37 -0
- package/dist/commands/observe.title.test.js.map +1 -0
- package/dist/commands/playbook.js +9 -0
- package/dist/commands/playbook.js.map +1 -1
- package/dist/commands/pods.d.ts +6 -1
- package/dist/commands/pods.js +32 -15
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proton-setup.js +3 -1
- package/dist/commands/proton-setup.js.map +1 -1
- package/dist/commands/session-feed-wait.d.ts +48 -0
- package/dist/commands/session-feed-wait.js +261 -0
- package/dist/commands/session-feed-wait.js.map +1 -0
- package/dist/commands/session-post.d.ts +40 -0
- package/dist/commands/session-post.js +96 -0
- package/dist/commands/session-post.js.map +1 -0
- package/dist/commands/session-wait.d.ts +9 -4
- package/dist/commands/session-wait.js +20 -5
- package/dist/commands/session-wait.js.map +1 -1
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +23 -0
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/skills-commands.d.ts +2 -0
- package/dist/commands/skills-commands.js +9 -1
- package/dist/commands/skills-commands.js.map +1 -1
- package/dist/commands/skills-commands.test.d.ts +1 -0
- package/dist/commands/skills-commands.test.js +57 -0
- package/dist/commands/skills-commands.test.js.map +1 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/template.d.ts +42 -0
- package/dist/commands/template.js +191 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/tracks.d.ts +67 -6
- package/dist/commands/tracks.js +268 -16
- package/dist/commands/tracks.js.map +1 -1
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/worker-accounts.d.ts +149 -0
- package/dist/commands/worker-accounts.js +339 -0
- package/dist/commands/worker-accounts.js.map +1 -0
- package/dist/commands/worker-accounts.test.d.ts +1 -0
- package/dist/commands/worker-accounts.test.js +224 -0
- package/dist/commands/worker-accounts.test.js.map +1 -0
- package/dist/commands/worker.d.ts +219 -10
- package/dist/commands/worker.js +749 -117
- package/dist/commands/worker.js.map +1 -1
- package/dist/commands/worker.test.js +321 -10
- package/dist/commands/worker.test.js.map +1 -1
- package/dist/index.js +164 -27
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-door.d.ts +1 -3
- package/dist/lib/agent-door.js +10 -7
- package/dist/lib/agent-door.js.map +1 -1
- package/dist/lib/auth.d.ts +3 -2
- package/dist/lib/auth.js +3 -3
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/box/egress.d.ts +82 -0
- package/dist/lib/box/egress.js +264 -0
- package/dist/lib/box/egress.js.map +1 -0
- package/dist/lib/box/evidence.d.ts +86 -0
- package/dist/lib/box/evidence.js +121 -0
- package/dist/lib/box/evidence.js.map +1 -0
- package/dist/lib/box/job-server.d.ts +72 -0
- package/dist/lib/box/job-server.js +246 -0
- package/dist/lib/box/job-server.js.map +1 -0
- package/dist/lib/box/provisioner.d.ts +83 -0
- package/dist/lib/box/provisioner.js +312 -0
- package/dist/lib/box/provisioner.js.map +1 -0
- package/dist/lib/box/proxmox.d.ts +112 -0
- package/dist/lib/box/proxmox.js +211 -0
- package/dist/lib/box/proxmox.js.map +1 -0
- package/dist/lib/box/wire.d.ts +98 -0
- package/dist/lib/box/wire.js +70 -0
- package/dist/lib/box/wire.js.map +1 -0
- package/dist/lib/browser-auth.d.ts +35 -0
- package/dist/lib/browser-auth.js +170 -0
- package/dist/lib/browser-auth.js.map +1 -0
- package/dist/lib/client-header.test.d.ts +1 -0
- package/dist/lib/client-header.test.js +85 -0
- package/dist/lib/client-header.test.js.map +1 -0
- package/dist/lib/conversation-headers.d.ts +24 -9
- package/dist/lib/conversation-headers.js +40 -3
- package/dist/lib/conversation-headers.js.map +1 -1
- package/dist/lib/credential-class.d.ts +28 -1
- package/dist/lib/credential-class.js +28 -6
- package/dist/lib/credential-class.js.map +1 -1
- package/dist/lib/exporter-coverage.d.ts +1 -1
- package/dist/lib/exporter-coverage.js +1 -1
- package/dist/lib/goal-words.d.ts +38 -0
- package/dist/lib/goal-words.js +112 -0
- package/dist/lib/goal-words.js.map +1 -0
- package/dist/lib/hub-client.d.ts +28 -7
- package/dist/lib/hub-client.js +75 -34
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/init-handoff.d.ts +8 -0
- package/dist/lib/init-handoff.js +9 -2
- package/dist/lib/init-handoff.js.map +1 -1
- package/dist/lib/key-source.d.ts +3 -1
- package/dist/lib/key-source.js +7 -4
- package/dist/lib/key-source.js.map +1 -1
- package/dist/lib/knowledge-title.d.ts +3 -2
- package/dist/lib/knowledge-title.js +3 -2
- package/dist/lib/knowledge-title.js.map +1 -1
- package/dist/lib/mcp-bridge.d.ts +7 -0
- package/dist/lib/mcp-bridge.js +34 -1
- package/dist/lib/mcp-bridge.js.map +1 -1
- package/dist/lib/mcp-bridge.test.js +31 -3
- package/dist/lib/mcp-bridge.test.js.map +1 -1
- package/dist/lib/openclaw.d.ts +32 -0
- package/dist/lib/openclaw.js +165 -0
- package/dist/lib/openclaw.js.map +1 -0
- package/dist/lib/pod-moved.d.ts +70 -0
- package/dist/lib/pod-moved.js +146 -0
- package/dist/lib/pod-moved.js.map +1 -0
- package/dist/lib/pod-reach.d.ts +39 -0
- package/dist/lib/pod-reach.js +117 -0
- package/dist/lib/pod-reach.js.map +1 -0
- package/dist/lib/pod-reach.test.d.ts +1 -0
- package/dist/lib/pod-reach.test.js +67 -0
- package/dist/lib/pod-reach.test.js.map +1 -0
- package/dist/lib/pod.d.ts +8 -0
- package/dist/lib/pod.js +12 -3
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/register-claude-code-mcp.test.d.ts +1 -0
- package/dist/lib/register-claude-code-mcp.test.js +78 -0
- package/dist/lib/register-claude-code-mcp.test.js.map +1 -0
- package/dist/lib/review-credential.js +3 -1
- package/dist/lib/review-credential.js.map +1 -1
- package/dist/lib/seed.d.ts +13 -0
- package/dist/lib/seed.js +135 -0
- package/dist/lib/seed.js.map +1 -0
- package/dist/lib/session-feed.d.ts +185 -0
- package/dist/lib/session-feed.js +257 -0
- package/dist/lib/session-feed.js.map +1 -0
- package/dist/lib/targets.d.ts +21 -3
- package/dist/lib/targets.js +52 -107
- package/dist/lib/targets.js.map +1 -1
- package/dist/lib/wake-hook-settings.d.ts +57 -0
- package/dist/lib/wake-hook-settings.js +153 -0
- package/dist/lib/wake-hook-settings.js.map +1 -0
- package/dist/lib/wake-state.d.ts +65 -0
- package/dist/lib/wake-state.js +122 -0
- package/dist/lib/wake-state.js.map +1 -0
- package/package.json +5 -2
- package/skills/README.md +1 -1
- package/skills/agent-os/SKILL.md +8 -8
- package/skills/synap/SKILL.md +141 -84
- package/skills/synap/code-skills.md +70 -0
- package/skills/synap/concepts.md +28 -20
- package/skills/synap/creative-loop.md +1 -1
- package/skills/synap/document-embeds.md +3 -1
- package/skills/synap/focus-sessions.md +16 -7
- package/skills/synap/from-intent.md +32 -16
- package/skills/synap/governance-rules.md +1 -1
- package/skills/synap/lenses.md +4 -4
- package/skills/synap/linking.md +2 -2
- package/skills/synap/mental-model.md +1 -1
- package/skills/synap/operating-mode-rules.md +1 -1
- package/skills/synap/quick-reference.md +2 -1
- package/skills/synap/reflexes.md +6 -6
- package/skills/synap/viewframe-cells.md +1 -1
- package/skills/synap/work-model.md +24 -10
- package/skills/synap/workspace-design.md +5 -5
- package/skills/synap/writes.md +16 -9
- package/skills/synap-app/SKILL.md +148 -66
- package/skills/synap-app/creating-the-base-app.md +12 -7
- package/skills/synap-app/governance.md +27 -16
- package/skills/synap-app/install-commands.md +18 -9
- package/skills/synap-app/overview.md +16 -9
- package/skills/synap-app/using-the-sdk.md +75 -25
- package/skills/synap-schema/SKILL.md +21 -21
- package/skills/synap-schema/intent-vocabulary-ssot.md +21 -21
- package/templates/box/runner.py +206 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw detection and configuration utilities.
|
|
3
|
+
* Reads/writes OpenClaw config without modifying unrelated settings.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
6
|
+
import { writeSecretFile } from "./atomic-file.js";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { execSync } from "child_process";
|
|
10
|
+
const OPENCLAW_DIR = join(homedir(), ".openclaw");
|
|
11
|
+
const OPENCLAW_CONFIG = join(OPENCLAW_DIR, "openclaw.json");
|
|
12
|
+
const OPENCLAW_SKILLS = join(OPENCLAW_DIR, "skills");
|
|
13
|
+
export function detectOpenClaw() {
|
|
14
|
+
// Detection priority:
|
|
15
|
+
// 1. Docker container (most reliable — actively running state)
|
|
16
|
+
// 2. Local install (openclaw binary in PATH + ~/.openclaw dir)
|
|
17
|
+
//
|
|
18
|
+
// Why Docker first: the presence of ~/.openclaw on the host doesn't prove
|
|
19
|
+
// OpenClaw is installed locally — it may be a stray directory, or the
|
|
20
|
+
// Synap deploy dir may live in the same home. A running Docker container
|
|
21
|
+
// is a much stronger signal.
|
|
22
|
+
// ── Path 1: Docker container ───────────────────────────────────────────
|
|
23
|
+
const dockerInfo = detectOpenClawDocker();
|
|
24
|
+
if (dockerInfo)
|
|
25
|
+
return dockerInfo;
|
|
26
|
+
// ── Path 2: Local install (npm -g or npx) ─────────────────────────────
|
|
27
|
+
// Require BOTH ~/.openclaw AND an `openclaw` binary in PATH.
|
|
28
|
+
// ~/.openclaw alone is not enough (could be a leftover dir).
|
|
29
|
+
let localBinaryVersion;
|
|
30
|
+
try {
|
|
31
|
+
const out = execSync("openclaw --version 2>/dev/null", {
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
}).trim();
|
|
35
|
+
const match = out.match(/\d+\.\d+\.\d+/);
|
|
36
|
+
if (match)
|
|
37
|
+
localBinaryVersion = match[0];
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// binary not in PATH — not a local install
|
|
41
|
+
}
|
|
42
|
+
if (existsSync(OPENCLAW_DIR) && localBinaryVersion) {
|
|
43
|
+
const info = {
|
|
44
|
+
found: true,
|
|
45
|
+
runtime: "local",
|
|
46
|
+
configPath: OPENCLAW_CONFIG,
|
|
47
|
+
skillsDir: OPENCLAW_SKILLS,
|
|
48
|
+
version: localBinaryVersion,
|
|
49
|
+
};
|
|
50
|
+
// Config
|
|
51
|
+
if (existsSync(OPENCLAW_CONFIG)) {
|
|
52
|
+
try {
|
|
53
|
+
info.config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
catch { /* corrupted */ }
|
|
56
|
+
}
|
|
57
|
+
// Gateway
|
|
58
|
+
const port = info.config?.gateway?.port ?? 18789;
|
|
59
|
+
info.gatewayPort = port;
|
|
60
|
+
try {
|
|
61
|
+
execSync(`curl -sf http://127.0.0.1:${port}/health >/dev/null 2>&1`, { timeout: 3000 });
|
|
62
|
+
info.gatewayRunning = true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
info.gatewayRunning = false;
|
|
66
|
+
}
|
|
67
|
+
return info;
|
|
68
|
+
}
|
|
69
|
+
return { found: false };
|
|
70
|
+
}
|
|
71
|
+
function detectOpenClawDocker() {
|
|
72
|
+
// (a) Check for a running container with "openclaw" in the name/image
|
|
73
|
+
let containerName = null;
|
|
74
|
+
let version;
|
|
75
|
+
try {
|
|
76
|
+
const psOut = execSync("docker ps --format '{{.Names}}\\t{{.Image}}' 2>/dev/null", { encoding: "utf-8", timeout: 4000 });
|
|
77
|
+
for (const line of psOut.split("\n")) {
|
|
78
|
+
if (/openclaw/i.test(line)) {
|
|
79
|
+
containerName = line.split("\t")[0]?.trim() ?? null;
|
|
80
|
+
// Extract version from image tag e.g. openclaw:2026.3.31
|
|
81
|
+
const verMatch = line.match(/:(\d{4}\.\d+\.\d+)/);
|
|
82
|
+
if (verMatch)
|
|
83
|
+
version = verMatch[1];
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { /* docker not available */ }
|
|
89
|
+
// (b) Gateway probe — works even if container name doesn't match pattern
|
|
90
|
+
let gatewayRunning = false;
|
|
91
|
+
try {
|
|
92
|
+
execSync("curl -sf http://127.0.0.1:18789/health >/dev/null 2>&1", { timeout: 3000 });
|
|
93
|
+
gatewayRunning = true;
|
|
94
|
+
}
|
|
95
|
+
catch { /* not responding */ }
|
|
96
|
+
// Need at least one signal to claim found
|
|
97
|
+
if (!containerName && !gatewayRunning)
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
found: true,
|
|
101
|
+
runtime: "docker",
|
|
102
|
+
containerName: containerName ?? "openclaw",
|
|
103
|
+
version,
|
|
104
|
+
gatewayRunning,
|
|
105
|
+
gatewayPort: 18789,
|
|
106
|
+
// No local config/skillsDir — they live inside the container volume
|
|
107
|
+
configPath: undefined,
|
|
108
|
+
skillsDir: undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function readOpenClawConfig() {
|
|
112
|
+
if (!existsSync(OPENCLAW_CONFIG))
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf-8"));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function writeOpenClawConfig(config) {
|
|
122
|
+
writeSecretFile(OPENCLAW_CONFIG, JSON.stringify(config, null, 2) + "\n");
|
|
123
|
+
}
|
|
124
|
+
export function getConfigPermissions() {
|
|
125
|
+
try {
|
|
126
|
+
const stat = statSync(OPENCLAW_DIR);
|
|
127
|
+
const mode = (stat.mode & 0o777).toString(8);
|
|
128
|
+
return { mode, safe: (stat.mode & 0o077) === 0 }; // safe = no group/world perms
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// ENOENT = dir doesn't exist (Docker runtime — config lives in container volume)
|
|
132
|
+
const code = err?.code;
|
|
133
|
+
if (code === "ENOENT")
|
|
134
|
+
return { mode: "N/A", safe: true };
|
|
135
|
+
return { mode: "???", safe: false };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get a nested config value safely.
|
|
140
|
+
*/
|
|
141
|
+
export function getConfigValue(config, path) {
|
|
142
|
+
const parts = path.split(".");
|
|
143
|
+
let current = config;
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
if (current == null || typeof current !== "object")
|
|
146
|
+
return undefined;
|
|
147
|
+
current = current[part];
|
|
148
|
+
}
|
|
149
|
+
return current;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Set a nested config value.
|
|
153
|
+
*/
|
|
154
|
+
export function setConfigValue(config, path, value) {
|
|
155
|
+
const parts = path.split(".");
|
|
156
|
+
let current = config;
|
|
157
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
158
|
+
if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
|
|
159
|
+
current[parts[i]] = {};
|
|
160
|
+
}
|
|
161
|
+
current = current[parts[i]];
|
|
162
|
+
}
|
|
163
|
+
current[parts[parts.length - 1]] = value;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=openclaw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openclaw.js","sourceRoot":"","sources":["../../src/lib/openclaw.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pod that moved to a new address.
|
|
3
|
+
*
|
|
4
|
+
* Hosted pods move from `{name}.synap.live` to `{name}.pod.synap.live`
|
|
5
|
+
* (Control Plane "tenant domains v2"). For 90 days the old name answers every
|
|
6
|
+
* request with a redirect to the new one. The CLI NEVER follows a redirect to
|
|
7
|
+
* another host for an API call: the request carries the pod key, and a
|
|
8
|
+
* redirect is something any server on the old name can send. Instead:
|
|
9
|
+
*
|
|
10
|
+
* 1. every pod call goes through `podFetch`, which refuses a cross-host
|
|
11
|
+
* redirect with `PodMovedError` (one line: what happened, what to run);
|
|
12
|
+
* 2. `synap doctor` asks the Control Plane where the pod lives now —
|
|
13
|
+
* `POST {cp}/pods/handshake {targetUrl: <old url>}` answers
|
|
14
|
+
* `410 {code: "POD_MOVED", podId, podUrl, consoleUrl}` for a moved pod
|
|
15
|
+
* (CP `src/routes/pods.ts`, `src/lib/pod-handshake-problem.ts`) —
|
|
16
|
+
* and `synap doctor --fix` rewrites the saved profile through the one
|
|
17
|
+
* door for a URL change (`synap pods update`).
|
|
18
|
+
*
|
|
19
|
+
* The new address comes ONLY from the Control Plane, never from the redirect.
|
|
20
|
+
*/
|
|
21
|
+
/** What to run when a pod has moved. */
|
|
22
|
+
export declare const POD_MOVED_HINT = "Run: synap doctor --fix (it finds the new address and updates your settings)";
|
|
23
|
+
/** The one line a command prints when its pod has moved. */
|
|
24
|
+
export declare function podMovedMessage(podUrl: string): string;
|
|
25
|
+
export declare class PodMovedError extends Error {
|
|
26
|
+
/** The address the CLI holds (origin). */
|
|
27
|
+
readonly podUrl: string;
|
|
28
|
+
/** Where the old address redirected — informational only, never used. */
|
|
29
|
+
readonly redirectedTo: string;
|
|
30
|
+
constructor(podUrl: string, redirectedTo: string);
|
|
31
|
+
}
|
|
32
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
33
|
+
/**
|
|
34
|
+
* `fetch` for calls to a pod: redirects within the same host are followed (at
|
|
35
|
+
* most 5), a redirect to another host throws `PodMovedError`. Network errors
|
|
36
|
+
* propagate unchanged, so callers keep their own transport classification.
|
|
37
|
+
*/
|
|
38
|
+
export declare function podFetch(url: string, init?: RequestInit, fetchImpl?: FetchLike): Promise<Response>;
|
|
39
|
+
/** What the Control Plane says about a pod address. */
|
|
40
|
+
export type PodMoveAnswer = {
|
|
41
|
+
kind: "moved";
|
|
42
|
+
podUrl: string;
|
|
43
|
+
consoleUrl?: string;
|
|
44
|
+
podId?: string;
|
|
45
|
+
}
|
|
46
|
+
/** The CP knows this address as the pod's own (or knows nothing to move). */
|
|
47
|
+
| {
|
|
48
|
+
kind: "not-moved";
|
|
49
|
+
code?: string;
|
|
50
|
+
}
|
|
51
|
+
/** No Control Plane sign-in on this computer, or it expired. */
|
|
52
|
+
| {
|
|
53
|
+
kind: "signed-out";
|
|
54
|
+
}
|
|
55
|
+
/** The CP could not be asked, or answered something unreadable. */
|
|
56
|
+
| {
|
|
57
|
+
kind: "unknown";
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Ask the Control Plane where a pod lives now. Needs the CP sign-in from
|
|
62
|
+
* `synap login` (a bearer token); without one the answer is `signed-out`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function askControlPlaneWherePodIs(params: {
|
|
65
|
+
podUrl: string;
|
|
66
|
+
cpUrl: string;
|
|
67
|
+
cpToken: string | undefined;
|
|
68
|
+
fetchImpl?: FetchLike;
|
|
69
|
+
}): Promise<PodMoveAnswer>;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pod that moved to a new address.
|
|
3
|
+
*
|
|
4
|
+
* Hosted pods move from `{name}.synap.live` to `{name}.pod.synap.live`
|
|
5
|
+
* (Control Plane "tenant domains v2"). For 90 days the old name answers every
|
|
6
|
+
* request with a redirect to the new one. The CLI NEVER follows a redirect to
|
|
7
|
+
* another host for an API call: the request carries the pod key, and a
|
|
8
|
+
* redirect is something any server on the old name can send. Instead:
|
|
9
|
+
*
|
|
10
|
+
* 1. every pod call goes through `podFetch`, which refuses a cross-host
|
|
11
|
+
* redirect with `PodMovedError` (one line: what happened, what to run);
|
|
12
|
+
* 2. `synap doctor` asks the Control Plane where the pod lives now —
|
|
13
|
+
* `POST {cp}/pods/handshake {targetUrl: <old url>}` answers
|
|
14
|
+
* `410 {code: "POD_MOVED", podId, podUrl, consoleUrl}` for a moved pod
|
|
15
|
+
* (CP `src/routes/pods.ts`, `src/lib/pod-handshake-problem.ts`) —
|
|
16
|
+
* and `synap doctor --fix` rewrites the saved profile through the one
|
|
17
|
+
* door for a URL change (`synap pods update`).
|
|
18
|
+
*
|
|
19
|
+
* The new address comes ONLY from the Control Plane, never from the redirect.
|
|
20
|
+
*/
|
|
21
|
+
/** What to run when a pod has moved. */
|
|
22
|
+
export const POD_MOVED_HINT = "Run: synap doctor --fix (it finds the new address and updates your settings)";
|
|
23
|
+
/** The one line a command prints when its pod has moved. */
|
|
24
|
+
export function podMovedMessage(podUrl) {
|
|
25
|
+
return `The pod at ${podUrl} has a new address. Run: synap doctor --fix`;
|
|
26
|
+
}
|
|
27
|
+
export class PodMovedError extends Error {
|
|
28
|
+
/** The address the CLI holds (origin). */
|
|
29
|
+
podUrl;
|
|
30
|
+
/** Where the old address redirected — informational only, never used. */
|
|
31
|
+
redirectedTo;
|
|
32
|
+
constructor(podUrl, redirectedTo) {
|
|
33
|
+
super(podMovedMessage(podUrl));
|
|
34
|
+
this.name = "PodMovedError";
|
|
35
|
+
this.podUrl = podUrl;
|
|
36
|
+
this.redirectedTo = redirectedTo;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const MAX_SAME_HOST_HOPS = 5;
|
|
40
|
+
function isRedirect(status) {
|
|
41
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A redirect the CLI may follow: the same origin, or the same host upgraded
|
|
45
|
+
* from http to https (default ports). Anything else is another host.
|
|
46
|
+
*/
|
|
47
|
+
function isSameHost(from, to) {
|
|
48
|
+
if (from.origin === to.origin)
|
|
49
|
+
return true;
|
|
50
|
+
return (from.protocol === "http:" &&
|
|
51
|
+
to.protocol === "https:" &&
|
|
52
|
+
from.hostname === to.hostname &&
|
|
53
|
+
from.port === "" &&
|
|
54
|
+
to.port === "");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `fetch` for calls to a pod: redirects within the same host are followed (at
|
|
58
|
+
* most 5), a redirect to another host throws `PodMovedError`. Network errors
|
|
59
|
+
* propagate unchanged, so callers keep their own transport classification.
|
|
60
|
+
*/
|
|
61
|
+
export async function podFetch(url, init = {}, fetchImpl = fetch) {
|
|
62
|
+
const start = new URL(url);
|
|
63
|
+
let current = start;
|
|
64
|
+
let currentInit = init;
|
|
65
|
+
for (let hop = 0;; hop++) {
|
|
66
|
+
const res = await fetchImpl(current.toString(), { ...currentInit, redirect: "manual" });
|
|
67
|
+
if (!isRedirect(res.status))
|
|
68
|
+
return res;
|
|
69
|
+
const location = res.headers.get("location");
|
|
70
|
+
if (!location)
|
|
71
|
+
return res;
|
|
72
|
+
let next;
|
|
73
|
+
try {
|
|
74
|
+
next = new URL(location, current);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return res;
|
|
78
|
+
}
|
|
79
|
+
if (!isSameHost(start, next)) {
|
|
80
|
+
throw new PodMovedError(start.origin, next.origin);
|
|
81
|
+
}
|
|
82
|
+
if (hop >= MAX_SAME_HOST_HOPS)
|
|
83
|
+
return res;
|
|
84
|
+
if (res.status === 303 && (currentInit.method ?? "GET").toUpperCase() !== "GET") {
|
|
85
|
+
const { body: _body, ...rest } = currentInit;
|
|
86
|
+
currentInit = { ...rest, method: "GET" };
|
|
87
|
+
}
|
|
88
|
+
current = next;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Ask the Control Plane where a pod lives now. Needs the CP sign-in from
|
|
93
|
+
* `synap login` (a bearer token); without one the answer is `signed-out`.
|
|
94
|
+
*/
|
|
95
|
+
export async function askControlPlaneWherePodIs(params) {
|
|
96
|
+
if (!params.cpToken)
|
|
97
|
+
return { kind: "signed-out" };
|
|
98
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
99
|
+
let res;
|
|
100
|
+
try {
|
|
101
|
+
res = await fetchImpl(`${params.cpUrl.replace(/\/+$/, "")}/pods/handshake`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${params.cpToken}`,
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({ targetUrl: params.podUrl }),
|
|
108
|
+
redirect: "manual",
|
|
109
|
+
signal: AbortSignal.timeout(10_000),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return { kind: "unknown", reason: err instanceof Error ? err.message : String(err) };
|
|
114
|
+
}
|
|
115
|
+
if (res.status === 401)
|
|
116
|
+
return { kind: "signed-out" };
|
|
117
|
+
const body = (await res.json().catch(() => null));
|
|
118
|
+
if (res.status === 410 && body?.code === "POD_MOVED") {
|
|
119
|
+
const podUrl = typeof body.podUrl === "string" ? safeHttpsOrigin(body.podUrl) : null;
|
|
120
|
+
if (!podUrl)
|
|
121
|
+
return { kind: "unknown", reason: "the Control Plane named no usable new address" };
|
|
122
|
+
return {
|
|
123
|
+
kind: "moved",
|
|
124
|
+
podUrl,
|
|
125
|
+
...(typeof body.consoleUrl === "string" ? { consoleUrl: body.consoleUrl } : {}),
|
|
126
|
+
...(typeof body.podId === "string" ? { podId: body.podId } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (res.ok || res.status === 403 || res.status === 409) {
|
|
130
|
+
return { kind: "not-moved", ...(typeof body?.code === "string" ? { code: body.code } : {}) };
|
|
131
|
+
}
|
|
132
|
+
return { kind: "unknown", reason: `the Control Plane answered ${res.status}` };
|
|
133
|
+
}
|
|
134
|
+
/** An https origin, or null. A new pod address is never plain http. */
|
|
135
|
+
function safeHttpsOrigin(value) {
|
|
136
|
+
try {
|
|
137
|
+
const url = new URL(value);
|
|
138
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
139
|
+
return null;
|
|
140
|
+
return url.origin;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=pod-moved.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pod-moved.js","sourceRoot":"","sources":["../../src/lib/pod-moved.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a pod probe failed, told apart: the network between this machine and the
|
|
3
|
+
* pod (egress proxy, DNS, firewall, TLS) versus the pod itself answering badly.
|
|
4
|
+
*
|
|
5
|
+
* `checkPodHealth` used to fold every failure into `healthy: false`, so `synap
|
|
6
|
+
* init` behind an egress proxy that answers 403 to CONNECT printed a pod-side
|
|
7
|
+
* checklist ("is the container running?") for a pod that was never contacted.
|
|
8
|
+
* This is the one place that taxonomy lives for the probe.
|
|
9
|
+
*/
|
|
10
|
+
export type ReachKind =
|
|
11
|
+
/** An egress proxy refused the CONNECT tunnel — the pod was never contacted. */
|
|
12
|
+
"proxy"
|
|
13
|
+
/** DNS, refused, reset, unroutable, timeout, TLS — no HTTP answer from anything. */
|
|
14
|
+
| "network"
|
|
15
|
+
/** The pod host answered, but not with a healthy /health. */
|
|
16
|
+
| "pod";
|
|
17
|
+
export interface ReachFailure {
|
|
18
|
+
kind: ReachKind;
|
|
19
|
+
/** The underlying error, one line: status / error code / message. */
|
|
20
|
+
detail: string;
|
|
21
|
+
/** Env variable whose proxy this process used, with credentials stripped. */
|
|
22
|
+
proxy?: {
|
|
23
|
+
env: string;
|
|
24
|
+
url: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The env proxy that applies to `podUrl`, credentials removed. Honors NO_PROXY. */
|
|
28
|
+
export declare function proxyInUse(podUrl: string, env?: NodeJS.ProcessEnv): {
|
|
29
|
+
env: string;
|
|
30
|
+
url: string;
|
|
31
|
+
} | undefined;
|
|
32
|
+
/** Classify a fetch that threw (no HTTP answer from the pod). */
|
|
33
|
+
export declare function classifyFetchError(err: unknown, podUrl: string, env?: NodeJS.ProcessEnv): ReachFailure;
|
|
34
|
+
/** Classify a /health that answered with a non-2xx status. */
|
|
35
|
+
export declare function classifyHttpStatus(status: number, podUrl: string, env?: NodeJS.ProcessEnv): ReachFailure;
|
|
36
|
+
/** The lines `synap init` / `synap doctor` print under "not reachable", by cause. */
|
|
37
|
+
export declare function reachChecklist(f: ReachFailure | undefined): string[];
|
|
38
|
+
/** The one-line headline: names the underlying error, not just "not reachable". */
|
|
39
|
+
export declare function unreachableMessage(podUrl: string, failure?: ReachFailure): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a pod probe failed, told apart: the network between this machine and the
|
|
3
|
+
* pod (egress proxy, DNS, firewall, TLS) versus the pod itself answering badly.
|
|
4
|
+
*
|
|
5
|
+
* `checkPodHealth` used to fold every failure into `healthy: false`, so `synap
|
|
6
|
+
* init` behind an egress proxy that answers 403 to CONNECT printed a pod-side
|
|
7
|
+
* checklist ("is the container running?") for a pod that was never contacted.
|
|
8
|
+
* This is the one place that taxonomy lives for the probe.
|
|
9
|
+
*/
|
|
10
|
+
const PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"];
|
|
11
|
+
/** The env proxy that applies to `podUrl`, credentials removed. Honors NO_PROXY. */
|
|
12
|
+
export function proxyInUse(podUrl, env = process.env) {
|
|
13
|
+
let target;
|
|
14
|
+
try {
|
|
15
|
+
target = new URL(podUrl);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const noProxy = (env.NO_PROXY ?? env.no_proxy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
21
|
+
const bypass = noProxy.some((p) => p === "*" ||
|
|
22
|
+
target.hostname === p.replace(/^\./, "") ||
|
|
23
|
+
target.hostname.endsWith(p.startsWith(".") ? p : `.${p}`));
|
|
24
|
+
if (bypass)
|
|
25
|
+
return undefined;
|
|
26
|
+
const order = target.protocol === "https:" ? PROXY_ENV : [...PROXY_ENV.slice(2, 4), ...PROXY_ENV.slice(0, 2), ...PROXY_ENV.slice(4)];
|
|
27
|
+
for (const name of order) {
|
|
28
|
+
const raw = env[name];
|
|
29
|
+
if (!raw)
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(raw);
|
|
33
|
+
u.username = "";
|
|
34
|
+
u.password = "";
|
|
35
|
+
return { env: name, url: u.origin };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { env: name, url: "(unparseable)" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function codeOf(err) {
|
|
44
|
+
const cause = err?.cause;
|
|
45
|
+
for (const e of [cause, err]) {
|
|
46
|
+
const c = e?.code;
|
|
47
|
+
if (typeof c === "string")
|
|
48
|
+
return c;
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
/** Classify a fetch that threw (no HTTP answer from the pod). */
|
|
53
|
+
export function classifyFetchError(err, podUrl, env = process.env) {
|
|
54
|
+
const proxy = proxyInUse(podUrl, env);
|
|
55
|
+
const cause = err?.cause;
|
|
56
|
+
const message = [cause?.message, err instanceof Error ? err.message : String(err)].filter(Boolean)[0] ?? "unknown error";
|
|
57
|
+
const code = codeOf(err);
|
|
58
|
+
// undici's ProxyAgent: "Proxy response (403) !== 200 when HTTP Tunneling";
|
|
59
|
+
// Node's env-proxy support surfaces "Tunnel ... 403" / ERR_PROXY_TUNNEL.
|
|
60
|
+
const tunnel = /proxy response \((\d{3})\)|tunnel.*?(\d{3})|ERR_PROXY_TUNNEL/i.exec(`${code} ${message}`);
|
|
61
|
+
if (tunnel || code === "UND_ERR_PRX_TLS" || code === "ERR_PROXY_TUNNEL") {
|
|
62
|
+
const status = tunnel?.[1] ?? tunnel?.[2];
|
|
63
|
+
return {
|
|
64
|
+
kind: "proxy",
|
|
65
|
+
detail: status ? `the egress proxy refused the tunnel (HTTP ${status} to CONNECT)` : `the egress proxy refused the tunnel (${message})`,
|
|
66
|
+
proxy,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const name = err instanceof Error ? err.name : "";
|
|
70
|
+
const label = code || (name === "TimeoutError" || name === "AbortError" ? "TIMEOUT" : message);
|
|
71
|
+
return {
|
|
72
|
+
kind: "network",
|
|
73
|
+
detail: `${label}${label === message ? "" : `: ${message}`}`,
|
|
74
|
+
proxy,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Classify a /health that answered with a non-2xx status. */
|
|
78
|
+
export function classifyHttpStatus(status, podUrl, env = process.env) {
|
|
79
|
+
return { kind: "pod", detail: `/health answered HTTP ${status}`, proxy: proxyInUse(podUrl, env) };
|
|
80
|
+
}
|
|
81
|
+
/** The lines `synap init` / `synap doctor` print under "not reachable", by cause. */
|
|
82
|
+
export function reachChecklist(f) {
|
|
83
|
+
const proxyLine = f?.proxy ? [`Proxy in use: ${f.proxy.url} (from ${f.proxy.env})`] : [];
|
|
84
|
+
if (f?.kind === "proxy") {
|
|
85
|
+
return [
|
|
86
|
+
...proxyLine,
|
|
87
|
+
"Your network's egress proxy blocked the connection. The pod was not contacted.",
|
|
88
|
+
"- Ask the proxy's owner to allow CONNECT to the pod host (port 443).",
|
|
89
|
+
"- Or bypass the proxy for it: export NO_PROXY=<pod-host>",
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
if (f?.kind === "network") {
|
|
93
|
+
return [
|
|
94
|
+
...proxyLine,
|
|
95
|
+
"This machine could not reach the pod host. The pod itself may be fine.",
|
|
96
|
+
"- Is the pod URL correct? (scheme + host + port)",
|
|
97
|
+
"- Are you online, on the right network/VPN, behind a firewall or proxy?",
|
|
98
|
+
"- Can you open the URL in a browser from this machine?",
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
return [
|
|
102
|
+
...proxyLine,
|
|
103
|
+
"- Is the pod URL correct? (scheme + host + port)",
|
|
104
|
+
"- Is the pod container running? On the server: ./synap health",
|
|
105
|
+
"- Is there a firewall or reverse proxy in the way?",
|
|
106
|
+
"- For localhost setups, did you start the stack? ./synap start",
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
/** The one-line headline: names the underlying error, not just "not reachable". */
|
|
110
|
+
export function unreachableMessage(podUrl, failure) {
|
|
111
|
+
if (!failure)
|
|
112
|
+
return `Pod at ${podUrl} is not reachable.`;
|
|
113
|
+
return failure.kind === "pod"
|
|
114
|
+
? `Pod at ${podUrl} is not healthy: ${failure.detail}.`
|
|
115
|
+
: `Could not reach ${podUrl}: ${failure.detail}.`;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=pod-reach.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pod-reach.js","sourceRoot":"","sources":["../../src/lib/pod-reach.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it, vi, afterEach } from "vitest";
|
|
2
|
+
import { checkPodHealth } from "./pod.js";
|
|
3
|
+
import { classifyFetchError, proxyInUse, reachChecklist, unreachableMessage } from "./pod-reach.js";
|
|
4
|
+
const POD = "https://pod.example.com";
|
|
5
|
+
/** What undici throws when the proxy answers CONNECT with 403. */
|
|
6
|
+
function proxy403() {
|
|
7
|
+
const err = new TypeError("fetch failed");
|
|
8
|
+
err.cause = Object.assign(new Error("Proxy response (403) !== 200 when HTTP Tunneling"), {
|
|
9
|
+
code: "UND_ERR_PRX_TUNNEL",
|
|
10
|
+
});
|
|
11
|
+
return err;
|
|
12
|
+
}
|
|
13
|
+
function refused() {
|
|
14
|
+
const err = new TypeError("fetch failed");
|
|
15
|
+
err.cause = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:443"), { code: "ECONNREFUSED" });
|
|
16
|
+
return err;
|
|
17
|
+
}
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
vi.unstubAllEnvs();
|
|
21
|
+
});
|
|
22
|
+
describe("classifyFetchError", () => {
|
|
23
|
+
it("a proxy 403 on CONNECT is a proxy failure naming the status and the env proxy used", () => {
|
|
24
|
+
const f = classifyFetchError(proxy403(), POD, { HTTPS_PROXY: "http://user:secret@egress.corp:3128" });
|
|
25
|
+
expect(f.kind).toBe("proxy");
|
|
26
|
+
expect(f.detail).toContain("403");
|
|
27
|
+
expect(f.proxy).toEqual({ env: "HTTPS_PROXY", url: "http://egress.corp:3128" });
|
|
28
|
+
expect(JSON.stringify(f)).not.toContain("secret");
|
|
29
|
+
});
|
|
30
|
+
it("ECONNREFUSED is a network failure that names the code", () => {
|
|
31
|
+
const f = classifyFetchError(refused(), POD, {});
|
|
32
|
+
expect(f.kind).toBe("network");
|
|
33
|
+
expect(f.detail).toContain("ECONNREFUSED");
|
|
34
|
+
expect(f.proxy).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
it("NO_PROXY bypass means no proxy is reported", () => {
|
|
37
|
+
expect(proxyInUse(POD, { HTTPS_PROXY: "http://egress:3128", NO_PROXY: ".example.com" })).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe("checkPodHealth carries the cause", () => {
|
|
41
|
+
it("proxy 403: not healthy, failure.kind proxy", async () => {
|
|
42
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(proxy403()));
|
|
43
|
+
const h = await checkPodHealth(POD);
|
|
44
|
+
expect(h.healthy).toBe(false);
|
|
45
|
+
expect(h.failure?.kind).toBe("proxy");
|
|
46
|
+
});
|
|
47
|
+
it("pod answering 503: failure.kind pod", async () => {
|
|
48
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("down", { status: 503 })));
|
|
49
|
+
const h = await checkPodHealth(POD);
|
|
50
|
+
expect(h.failure).toMatchObject({ kind: "pod", detail: "/health answered HTTP 503" });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("init message and checklist branch on the cause", () => {
|
|
54
|
+
it("proxy failure: headline has the status; checklist is not the pod-container list", () => {
|
|
55
|
+
const f = classifyFetchError(proxy403(), POD, { HTTPS_PROXY: "http://egress.corp:3128" });
|
|
56
|
+
expect(unreachableMessage(POD, f)).toContain("403");
|
|
57
|
+
const list = reachChecklist(f).join("\n");
|
|
58
|
+
expect(list).toContain("HTTPS_PROXY");
|
|
59
|
+
expect(list).toContain("NO_PROXY");
|
|
60
|
+
expect(list).not.toContain("container running");
|
|
61
|
+
});
|
|
62
|
+
it("pod failure keeps the pod-side checklist", () => {
|
|
63
|
+
const list = reachChecklist({ kind: "pod", detail: "/health answered HTTP 503" }).join("\n");
|
|
64
|
+
expect(list).toContain("container running");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=pod-reach.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pod-reach.test.js","sourceRoot":"","sources":["../../src/lib/pod-reach.test.ts"],"names":[],"mappings":"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"}
|