harnery 0.41.0 → 0.43.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/dist/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +21 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +63 -5
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +372 -103
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +11 -5
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +59 -13
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/agents/state/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +131 -23
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +33 -7
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +33 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +115 -30
- package/package.json +7 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +51 -0
- package/src/commands/agents.ts +111 -11
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +494 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/prompt-context.ts +25 -8
- package/src/core/agents/render/session-context.ts +84 -16
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +152 -22
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/storage/process-log.ts +17 -2
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +38 -9
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +143 -30
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// This machine's public addresses as Cloudflare sees them, and the allowlist
|
|
2
|
+
// bookkeeping around them (`tunnel allow add --current`).
|
|
3
|
+
//
|
|
4
|
+
// The tunnel gate admits a visitor by the `CF-Connecting-IP` header, which is
|
|
5
|
+
// the address Cloudflare saw. Asking Cloudflare's own trace endpoint therefore
|
|
6
|
+
// returns the same address the gate will later compare, with no third party and
|
|
7
|
+
// no guess about NAT. The IPv4 and IPv6 probes use address literals, so each
|
|
8
|
+
// travels over the family it is named for and needs no DNS.
|
|
9
|
+
//
|
|
10
|
+
// Entries the refresh adds are recorded in `auto_allowed`. The next refresh
|
|
11
|
+
// replaces exactly those, so a machine that changes network does not pile up
|
|
12
|
+
// stale addresses, and an entry added by hand is never removed.
|
|
13
|
+
import { ipv4Slash32, ipv6Slash64, normalizeAllowEntry } from "./allowlist.js";
|
|
14
|
+
export const TRACE_URL_V4 = "https://1.1.1.1/cdn-cgi/trace";
|
|
15
|
+
export const TRACE_URL_V6 = "https://[2606:4700:4700::1111]/cdn-cgi/trace";
|
|
16
|
+
/** The `ip=` value of a Cloudflare trace body, or null. */
|
|
17
|
+
export function parseTraceIp(body) {
|
|
18
|
+
const m = body.match(/^ip=(\S+)\s*$/m);
|
|
19
|
+
return m ? m[1] : null;
|
|
20
|
+
}
|
|
21
|
+
async function probe(url, family, opts) {
|
|
22
|
+
let last = "no answer";
|
|
23
|
+
for (let attempt = 0; attempt < opts.attempts; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
const res = await opts.fetch(url, { signal: AbortSignal.timeout(opts.timeoutMs) });
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
throw new Error(`HTTP ${res.status}`);
|
|
28
|
+
const ip = parseTraceIp(await res.text());
|
|
29
|
+
if (!ip)
|
|
30
|
+
throw new Error("the trace had no ip= line");
|
|
31
|
+
const entry = family === 4 ? ipv4Slash32(ip) : ipv6Slash64(ip);
|
|
32
|
+
if (!entry)
|
|
33
|
+
throw new Error(`expected an IPv${family} address, got ${ip}`);
|
|
34
|
+
return ip;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
last = error instanceof Error ? error.message : String(error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(last);
|
|
41
|
+
}
|
|
42
|
+
/** Both families in parallel. A family that fails is reported, not thrown. */
|
|
43
|
+
export async function detectPublicAddresses(options = {}) {
|
|
44
|
+
const opts = {
|
|
45
|
+
fetch: options.fetch ?? fetch,
|
|
46
|
+
timeoutMs: options.timeoutMs ?? 4_000,
|
|
47
|
+
attempts: options.attempts ?? 2,
|
|
48
|
+
};
|
|
49
|
+
const [v4, v6] = await Promise.allSettled([
|
|
50
|
+
probe(TRACE_URL_V4, 4, opts),
|
|
51
|
+
probe(TRACE_URL_V6, 6, opts),
|
|
52
|
+
]);
|
|
53
|
+
const out = { failures: [] };
|
|
54
|
+
if (v4.status === "fulfilled")
|
|
55
|
+
out.v4 = v4.value;
|
|
56
|
+
else
|
|
57
|
+
out.failures.push(`IPv4: ${v4.reason.message}`);
|
|
58
|
+
if (v6.status === "fulfilled")
|
|
59
|
+
out.v6 = v6.value;
|
|
60
|
+
else
|
|
61
|
+
out.failures.push(`IPv6: ${v6.reason.message}`);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
const sameEntry = (a, b) => (normalizeAllowEntry(a) ?? a) === (normalizeAllowEntry(b) ?? b);
|
|
65
|
+
const includesEntry = (list, entry) => list.some((e) => sameEntry(e, entry));
|
|
66
|
+
/**
|
|
67
|
+
* Fold freshly detected addresses into the allowlist.
|
|
68
|
+
*
|
|
69
|
+
* When nothing was detected the config comes back untouched with a warning:
|
|
70
|
+
* a failed lookup must never empty the list. When at least one family was
|
|
71
|
+
* detected, a family that was not is treated as absent from this network, so
|
|
72
|
+
* its old automatic entry goes. The entries for the detected families replace
|
|
73
|
+
* every previous automatic entry; manual entries are kept in place.
|
|
74
|
+
*/
|
|
75
|
+
export function planCurrentAddresses(config, detected) {
|
|
76
|
+
const previousAuto = config.auto_allowed ?? [];
|
|
77
|
+
const wanted = [];
|
|
78
|
+
if (detected.v4) {
|
|
79
|
+
const entry = ipv4Slash32(detected.v4);
|
|
80
|
+
if (entry)
|
|
81
|
+
wanted.push(entry);
|
|
82
|
+
}
|
|
83
|
+
if (detected.v6) {
|
|
84
|
+
const entry = ipv6Slash64(detected.v6);
|
|
85
|
+
if (entry)
|
|
86
|
+
wanted.push(entry);
|
|
87
|
+
}
|
|
88
|
+
if (wanted.length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
config,
|
|
91
|
+
added: [],
|
|
92
|
+
removed: [],
|
|
93
|
+
automatic: previousAuto,
|
|
94
|
+
changed: false,
|
|
95
|
+
warnings: [
|
|
96
|
+
`Could not detect this machine's public address (${detected.failures.join("; ") || "no answer"}). ` +
|
|
97
|
+
`The allowlist is unchanged${previousAuto.length ? `; the previous automatic entries stay: ${previousAuto.join(", ")}` : ""}.`,
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const manual = config.allowed_ips.filter((e) => !includesEntry(previousAuto, e));
|
|
102
|
+
// An address already listed by hand stays manual, so a later refresh keeps it.
|
|
103
|
+
const automatic = wanted.filter((e) => !includesEntry(manual, e));
|
|
104
|
+
const allowed = [...manual, ...automatic];
|
|
105
|
+
const before = config.allowed_ips;
|
|
106
|
+
const added = allowed.filter((e) => !includesEntry(before, e));
|
|
107
|
+
const removed = before.filter((e) => !includesEntry(allowed, e));
|
|
108
|
+
const warnings = [];
|
|
109
|
+
const missing = [detected.v4 ? null : "IPv4", detected.v6 ? null : "IPv6"].filter(Boolean);
|
|
110
|
+
if (missing.length > 0) {
|
|
111
|
+
warnings.push(`${missing.join(" and ")} was not detected, so this network is treated as having none` +
|
|
112
|
+
(detected.failures.length ? ` (${detected.failures.join("; ")})` : "") +
|
|
113
|
+
".");
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
config: { ...config, allowed_ips: allowed, auto_allowed: automatic },
|
|
117
|
+
added,
|
|
118
|
+
removed,
|
|
119
|
+
automatic,
|
|
120
|
+
changed: added.length > 0 || removed.length > 0,
|
|
121
|
+
warnings,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-page.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/error-page.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,GAAG,sBAAsB,CAAC;IAC/
|
|
1
|
+
{"version":3,"file":"error-page.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/error-page.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,GAAG,aAAa,GAAG,sBAAsB,CAAC;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA6BD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAwF7E"}
|
|
@@ -25,14 +25,25 @@ function diagnosticText(options) {
|
|
|
25
25
|
return lines.join("\n");
|
|
26
26
|
}
|
|
27
27
|
export function renderTunnelErrorPage(options) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
const copy = {
|
|
29
|
+
"access-denied": {
|
|
30
|
+
title: "This device is not allowed yet",
|
|
31
|
+
summary: "The tunnel is running, but this device's public IP is not on its access list.",
|
|
32
|
+
nextStep: "Copy the diagnostic below and send it to the person running the tunnel. It includes the public IP that needs to be allowed.",
|
|
33
|
+
},
|
|
34
|
+
"path-denied": {
|
|
35
|
+
title: "This page is not shared",
|
|
36
|
+
summary: "The tunnel is running, but it shares only part of the site, and this address is outside it.",
|
|
37
|
+
nextStep: "If you expected to see this page, copy the diagnostic below and send it to the person running the tunnel.",
|
|
38
|
+
},
|
|
39
|
+
"upstream-unavailable": {
|
|
40
|
+
title: "The preview is temporarily offline",
|
|
41
|
+
summary: "The public tunnel and access check are working, but the local preview server is not responding.",
|
|
42
|
+
nextStep: "Copy the diagnostic below and send it to the person running the tunnel. The incident ID can be matched to the server log.",
|
|
43
|
+
},
|
|
44
|
+
}[options.kind];
|
|
45
|
+
const { title, summary, nextStep } = copy;
|
|
46
|
+
const denied = options.kind !== "upstream-unavailable";
|
|
36
47
|
const diagnostic = diagnosticText(options);
|
|
37
48
|
const safeDiagnosticForScript = JSON.stringify(diagnostic).replaceAll("<", "\\u003c");
|
|
38
49
|
return `<!doctype html>
|
package/dist/lib/tunnel/gate.js
CHANGED
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
// stripped (Bun's fetch auto-decompresses the body but retains the encoding
|
|
5
5
|
// header, which breaks browser decoding downstream). Cloudflare provider mode
|
|
6
6
|
// checks CF-Connecting-IP against an allowlist; trusted local proxy mode lets
|
|
7
|
-
// the local exposer own access.
|
|
7
|
+
// the local exposer own access. In every mode the gate then refuses any path
|
|
8
|
+
// outside the tunnel's path scope (see path-scope.ts); an empty scope refuses
|
|
9
|
+
// everything.
|
|
8
10
|
//
|
|
9
11
|
// Runs detached, outside the CLI command framework; no command context is
|
|
10
12
|
// available; stdout/stderr is captured into .cache/tunnel/gate.log by the
|
|
11
13
|
// spawner.
|
|
14
|
+
import { allowlistMatches, compileAllowlist } from "./allowlist.js";
|
|
12
15
|
import { renderTunnelErrorPage } from "./error-page";
|
|
13
16
|
import { applyUpstreamHeaders } from "./forward-headers";
|
|
17
|
+
import { ALLOW_PATHS_ENV, isPathAllowed, parseAllowPathsEnv } from "./path-scope.js";
|
|
14
18
|
// `--port`/`--name` are also passed on argv (not just env) so the gate's port
|
|
15
19
|
// and instance name show up in its process command line. That's what lets
|
|
16
20
|
// `tunnel down` scope its stray-process sweep to a single instance via
|
|
@@ -21,14 +25,13 @@ function argvFlag(flag) {
|
|
|
21
25
|
const i = process.argv.indexOf(flag);
|
|
22
26
|
return i !== -1 ? process.argv[i + 1] : undefined;
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.map((s) => s.trim())
|
|
27
|
-
.filter(Boolean));
|
|
28
|
+
// Entries are exact addresses or CIDR ranges (`203.0.113.0/24`, `2601:db8:1:2::/64`).
|
|
29
|
+
const ALLOW = compileAllowlist((process.env.HARNERY_TUNNEL_ALLOW ?? "").split(","));
|
|
28
30
|
const TARGET = process.env.HARNERY_TUNNEL_TARGET ?? "127.0.0.1:8001";
|
|
29
31
|
const VHOST = process.env.HARNERY_TUNNEL_VHOST ?? "localhost";
|
|
30
32
|
const PORT = Number(process.env.HARNERY_TUNNEL_PORT ?? argvFlag("--port") ?? "9001");
|
|
31
33
|
const ACCESS = process.env.HARNERY_TUNNEL_ACCESS ?? "cloudflare-allowlist";
|
|
34
|
+
const ALLOW_PATHS = parseAllowPathsEnv(process.env[ALLOW_PATHS_ENV]);
|
|
32
35
|
const NAME = argvFlag("--name") ?? "default";
|
|
33
36
|
const UPSTREAM_HTTP = `http://${TARGET}`;
|
|
34
37
|
const UPSTREAM_WS = `ws://${TARGET}`;
|
|
@@ -62,7 +65,7 @@ const server = Bun.serve({
|
|
|
62
65
|
const url = new URL(req.url);
|
|
63
66
|
if (ACCESS === "cloudflare-allowlist") {
|
|
64
67
|
const ip = req.headers.get("cf-connecting-ip") ?? "";
|
|
65
|
-
if (!ALLOW
|
|
68
|
+
if (!allowlistMatches(ALLOW, ip)) {
|
|
66
69
|
const details = requestDetails(req, url);
|
|
67
70
|
// Log denials so operators can whitelist a phone/laptop that just
|
|
68
71
|
// hit 403 without asking the human to dig up their public IP.
|
|
@@ -79,6 +82,20 @@ const server = Bun.serve({
|
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
}
|
|
85
|
+
if (!isPathAllowed(url.pathname, ALLOW_PATHS)) {
|
|
86
|
+
const details = requestDetails(req, url);
|
|
87
|
+
console.log(
|
|
88
|
+
// lint-ok-emission: detached worker, see file note above
|
|
89
|
+
`path-deny: incident=${details.incidentId} ip=${details.clientIp || "(missing-cf-connecting-ip)"} ray=${details.cloudflareRay || "(missing)"} ${req.method} ${details.path}`);
|
|
90
|
+
return new Response(renderTunnelErrorPage({ ...details, kind: "path-denied" }), {
|
|
91
|
+
status: 403,
|
|
92
|
+
headers: {
|
|
93
|
+
"cache-control": "no-store",
|
|
94
|
+
"content-type": "text/html; charset=utf-8",
|
|
95
|
+
"x-harnery-tunnel-incident": details.incidentId,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
82
99
|
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
83
100
|
if (server.upgrade(req, {
|
|
84
101
|
data: { path: url.pathname + url.search, originalHost: req.headers.get("host") ?? "" },
|
|
@@ -168,4 +185,13 @@ const server = Bun.serve({
|
|
|
168
185
|
// captured into .cache/tunnel/gate.log by the spawning command.
|
|
169
186
|
console.log(`harn-tunnel-gate :${server.port} -> ${UPSTREAM_HTTP} (Host: ${VHOST})`); // lint-ok-emission: detached worker, see file note above
|
|
170
187
|
console.log(`access: ${ACCESS}`); // lint-ok-emission: detached worker, see file note above
|
|
171
|
-
console.log(`
|
|
188
|
+
console.log(`paths: ${ALLOW_PATHS.join(", ") || "(empty, every path refused)"}`); // lint-ok-emission: detached worker, see file note above
|
|
189
|
+
console.log(
|
|
190
|
+
// lint-ok-emission: detached worker, see file note above
|
|
191
|
+
`allow: ${process.env.HARNERY_TUNNEL_ALLOW?.split(",")
|
|
192
|
+
.filter((s) => s.trim())
|
|
193
|
+
.join(", ") || "(empty, not used outside allowlist mode)"}`);
|
|
194
|
+
if (ALLOW.invalid.length > 0) {
|
|
195
|
+
// lint-ok-emission: detached worker, see file note above
|
|
196
|
+
console.log(`allow: ignoring unreadable entries: ${ALLOW.invalid.join(", ")}`);
|
|
197
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Env var that carries the scope from `tunnel up` to the detached gate. */
|
|
2
|
+
export declare const ALLOW_PATHS_ENV = "HARNERY_TUNNEL_ALLOW_PATHS";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize one `--allow-path` value to a canonical prefix: a leading slash,
|
|
5
|
+
* no trailing slash (except `/` itself), no query or fragment, and no dot,
|
|
6
|
+
* empty, or percent-encoded segment. Throws on anything the gate could not
|
|
7
|
+
* match unambiguously, so a typo fails at `tunnel up` instead of publishing
|
|
8
|
+
* more or less than intended.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeAllowPath(raw: string): string;
|
|
11
|
+
/** Normalize and de-duplicate a list of `--allow-path` values. */
|
|
12
|
+
export declare function normalizeAllowPaths(raw: readonly string[]): string[];
|
|
13
|
+
/** Parse the gate's env value (comma-separated, already normalized by `up`). */
|
|
14
|
+
export declare function parseAllowPathsEnv(value: string | undefined): string[];
|
|
15
|
+
/**
|
|
16
|
+
* True when `pathname` (a WHATWG-parsed URL pathname, so `.` and `..` segments
|
|
17
|
+
* are already resolved) falls inside one of the allowed prefixes. A prefix
|
|
18
|
+
* matches itself and anything below it on a segment boundary: `/share` allows
|
|
19
|
+
* `/share` and `/share/x`, never `/shared`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isPathAllowed(pathname: string, allowPaths: readonly string[]): boolean;
|
|
22
|
+
//# sourceMappingURL=path-scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-scope.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/path-scope.ts"],"names":[],"mappings":"AAQA,4EAA4E;AAC5E,eAAO,MAAM,eAAe,+BAA+B,CAAC;AAE5D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAmBtD;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpE;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAOtE;AAQD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMtF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Path scope for the tunnel gate: the URL path prefixes a tunnel publishes.
|
|
2
|
+
//
|
|
3
|
+
// A tunnel forwards to a whole local app, but the person starting it usually
|
|
4
|
+
// means to share one surface of that app. The gate therefore refuses every
|
|
5
|
+
// request whose path is outside the scope the operator named at `tunnel up`,
|
|
6
|
+
// and an empty scope refuses everything. `/` is the explicit way to publish
|
|
7
|
+
// the whole upstream.
|
|
8
|
+
/** Env var that carries the scope from `tunnel up` to the detached gate. */
|
|
9
|
+
export const ALLOW_PATHS_ENV = "HARNERY_TUNNEL_ALLOW_PATHS";
|
|
10
|
+
/**
|
|
11
|
+
* Normalize one `--allow-path` value to a canonical prefix: a leading slash,
|
|
12
|
+
* no trailing slash (except `/` itself), no query or fragment, and no dot,
|
|
13
|
+
* empty, or percent-encoded segment. Throws on anything the gate could not
|
|
14
|
+
* match unambiguously, so a typo fails at `tunnel up` instead of publishing
|
|
15
|
+
* more or less than intended.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeAllowPath(raw) {
|
|
18
|
+
const value = raw.trim();
|
|
19
|
+
if (!value.startsWith("/")) {
|
|
20
|
+
throw new Error(`allow-path must start with "/": ${JSON.stringify(raw)}`);
|
|
21
|
+
}
|
|
22
|
+
if (/[?#%\\]/.test(value)) {
|
|
23
|
+
throw new Error(`allow-path must be a plain path prefix without ?, #, %, or \\: ${JSON.stringify(raw)}`);
|
|
24
|
+
}
|
|
25
|
+
if (value === "/")
|
|
26
|
+
return "/";
|
|
27
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
28
|
+
const segments = trimmed.slice(1).split("/");
|
|
29
|
+
if (segments.some((s) => s === "" || s === "." || s === "..")) {
|
|
30
|
+
throw new Error(`allow-path must not contain empty, "." or ".." segments: ${JSON.stringify(raw)}`);
|
|
31
|
+
}
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
/** Normalize and de-duplicate a list of `--allow-path` values. */
|
|
35
|
+
export function normalizeAllowPaths(raw) {
|
|
36
|
+
return [...new Set(raw.map(normalizeAllowPath))];
|
|
37
|
+
}
|
|
38
|
+
/** Parse the gate's env value (comma-separated, already normalized by `up`). */
|
|
39
|
+
export function parseAllowPathsEnv(value) {
|
|
40
|
+
return normalizeAllowPaths((value ?? "")
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((s) => s.trim())
|
|
43
|
+
.filter(Boolean));
|
|
44
|
+
}
|
|
45
|
+
// Encoded slash, backslash, dot, or percent. A request path carrying one of
|
|
46
|
+
// these can be decoded differently by the gate and the upstream router (for
|
|
47
|
+
// example `/share%2F..%2Fprivate`), so the gate refuses it outright rather than
|
|
48
|
+
// guess which reading the upstream will use.
|
|
49
|
+
const AMBIGUOUS_PATH = /%(2f|5c|2e|25)|\\/i;
|
|
50
|
+
/**
|
|
51
|
+
* True when `pathname` (a WHATWG-parsed URL pathname, so `.` and `..` segments
|
|
52
|
+
* are already resolved) falls inside one of the allowed prefixes. A prefix
|
|
53
|
+
* matches itself and anything below it on a segment boundary: `/share` allows
|
|
54
|
+
* `/share` and `/share/x`, never `/shared`.
|
|
55
|
+
*/
|
|
56
|
+
export function isPathAllowed(pathname, allowPaths) {
|
|
57
|
+
if (allowPaths.length === 0)
|
|
58
|
+
return false;
|
|
59
|
+
if (!pathname.startsWith("/") || AMBIGUOUS_PATH.test(pathname))
|
|
60
|
+
return false;
|
|
61
|
+
return allowPaths.some((prefix) => prefix === "/" || pathname === prefix || pathname.startsWith(`${prefix}/`));
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type Run = (command: string, args: string[]) => {
|
|
2
|
+
status: number | null;
|
|
3
|
+
stdout: string;
|
|
4
|
+
missing: boolean;
|
|
5
|
+
};
|
|
6
|
+
export interface ProcessRow {
|
|
7
|
+
pid: number;
|
|
8
|
+
command: string;
|
|
9
|
+
}
|
|
10
|
+
/** Parse `ps -axo pid=,command=` output (also the shape the PowerShell probe emits). */
|
|
11
|
+
export declare function parseProcessRows(output: string): ProcessRow[];
|
|
12
|
+
/** Every process with a command line, or null when the platform gives no way to list them. */
|
|
13
|
+
export declare function listProcesses(platform?: NodeJS.Platform, run?: Run): ProcessRow[] | null;
|
|
14
|
+
/**
|
|
15
|
+
* PIDs whose command line matches `pattern`, an extended regular expression
|
|
16
|
+
* (the intersection of `pgrep -f` and JavaScript syntax). Returns [] when
|
|
17
|
+
* nothing matches or no listing tool works.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findPidsByCommandLine(pattern: string, platform?: NodeJS.Platform, run?: Run): number[];
|
|
20
|
+
/** Ports from `ss -tlnH`: `LISTEN 0 4096 127.0.0.1:9001 0.0.0.0:*`. */
|
|
21
|
+
export declare function parseSsPorts(output: string): number[];
|
|
22
|
+
/** Ports from `lsof -nP -iTCP -sTCP:LISTEN` rows ending `127.0.0.1:58055 (LISTEN)`. */
|
|
23
|
+
export declare function parseLsofPorts(output: string): number[];
|
|
24
|
+
/** Ports from `netstat -ano -p TCP` rows: `TCP 0.0.0.0:9001 0.0.0.0:0 LISTENING 4`. */
|
|
25
|
+
export declare function parseNetstatPorts(output: string): number[];
|
|
26
|
+
/**
|
|
27
|
+
* Ports with a LISTEN socket, or null when no discovery tool answered.
|
|
28
|
+
*
|
|
29
|
+
* Null is not "no ports": reporting every port as free would let a caller hand
|
|
30
|
+
* out an occupied port, so callers that need a real answer use `portListening`,
|
|
31
|
+
* which falls back to connecting.
|
|
32
|
+
*/
|
|
33
|
+
export declare function listeningPorts(platform?: NodeJS.Platform, run?: Run): Set<number> | null;
|
|
34
|
+
/** True when something accepts a TCP connection on 127.0.0.1:<port>. */
|
|
35
|
+
export declare function connectProbe(port: number, timeoutMs?: number): Promise<boolean>;
|
|
36
|
+
/** Whether `port` has a listener, by the platform tool when there is one, else by connecting. */
|
|
37
|
+
export declare function portListening(port: number): Promise<boolean>;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=processes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processes.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/processes.ts"],"names":[],"mappings":"AAWA,KAAK,GAAG,GAAG,CACT,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAQjE,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wFAAwF;AACxF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAO7D;AAMD,8FAA8F;AAC9F,wBAAgB,aAAa,CAC3B,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,GAAa,GACjB,UAAU,EAAE,GAAG,IAAI,CAWrB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,GAAa,GACjB,MAAM,EAAE,CAiBV;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAErD;AAED,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAI1D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,GAAa,GACjB,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAepB;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAW5E;AAED,iGAAiG;AACjG,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGlE"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Process and port discovery for the tunnel commands, per platform.
|
|
2
|
+
//
|
|
3
|
+
// Linux and macOS have `pgrep`/`ps`; Linux has `ss`; both have `lsof`. Windows
|
|
4
|
+
// has none of these, so it uses PowerShell for command lines and `netstat` for
|
|
5
|
+
// listening sockets. Every probe is best effort: a missing tool yields "no
|
|
6
|
+
// answer" (null or an empty list), never a thrown error, and the callers fall
|
|
7
|
+
// back to checks that need no tool (a TCP connect probe, a PID check).
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { connect } from "node:net";
|
|
10
|
+
const runTool = (command, args) => {
|
|
11
|
+
const r = spawnSync(command, args, { encoding: "utf-8", windowsHide: true, timeout: 15_000 });
|
|
12
|
+
const missing = r.error?.code === "ENOENT";
|
|
13
|
+
return { status: r.status, stdout: typeof r.stdout === "string" ? r.stdout : "", missing };
|
|
14
|
+
};
|
|
15
|
+
/** Parse `ps -axo pid=,command=` output (also the shape the PowerShell probe emits). */
|
|
16
|
+
export function parseProcessRows(output) {
|
|
17
|
+
const rows = [];
|
|
18
|
+
for (const line of output.split(/\r?\n/)) {
|
|
19
|
+
const m = line.match(/^\s*(\d+)\s+(.*\S)\s*$/);
|
|
20
|
+
if (m)
|
|
21
|
+
rows.push({ pid: Number(m[1]), command: m[2] });
|
|
22
|
+
}
|
|
23
|
+
return rows;
|
|
24
|
+
}
|
|
25
|
+
const POWERSHELL_PROCESS_LIST = "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine } | " +
|
|
26
|
+
'ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }';
|
|
27
|
+
/** Every process with a command line, or null when the platform gives no way to list them. */
|
|
28
|
+
export function listProcesses(platform = process.platform, run = runTool) {
|
|
29
|
+
if (platform === "win32") {
|
|
30
|
+
for (const shell of ["powershell.exe", "pwsh.exe", "pwsh"]) {
|
|
31
|
+
const r = run(shell, ["-NoProfile", "-NonInteractive", "-Command", POWERSHELL_PROCESS_LIST]);
|
|
32
|
+
if (r.missing)
|
|
33
|
+
continue;
|
|
34
|
+
if (r.status === 0)
|
|
35
|
+
return parseProcessRows(r.stdout);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const ps = run("ps", ["-axo", "pid=,command="]);
|
|
40
|
+
return ps.status === 0 ? parseProcessRows(ps.stdout) : null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* PIDs whose command line matches `pattern`, an extended regular expression
|
|
44
|
+
* (the intersection of `pgrep -f` and JavaScript syntax). Returns [] when
|
|
45
|
+
* nothing matches or no listing tool works.
|
|
46
|
+
*/
|
|
47
|
+
export function findPidsByCommandLine(pattern, platform = process.platform, run = runTool) {
|
|
48
|
+
if (platform !== "win32") {
|
|
49
|
+
// `--` ends options: the provider pattern starts with `--url`, which pgrep
|
|
50
|
+
// would otherwise reject as an unknown flag and report as "no match".
|
|
51
|
+
const r = run("pgrep", ["-f", "--", pattern]);
|
|
52
|
+
if (!r.missing) {
|
|
53
|
+
if (r.status !== 0)
|
|
54
|
+
return [];
|
|
55
|
+
return r.stdout
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((line) => Number(line.trim()))
|
|
58
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const rows = listProcesses(platform, run);
|
|
62
|
+
if (!rows)
|
|
63
|
+
return [];
|
|
64
|
+
const re = new RegExp(pattern);
|
|
65
|
+
return rows.filter((row) => re.test(row.command)).map((row) => row.pid);
|
|
66
|
+
}
|
|
67
|
+
/** Ports from `ss -tlnH`: `LISTEN 0 4096 127.0.0.1:9001 0.0.0.0:*`. */
|
|
68
|
+
export function parseSsPorts(output) {
|
|
69
|
+
return [...output.matchAll(/:(\d+)\s/g)].map((m) => Number(m[1]));
|
|
70
|
+
}
|
|
71
|
+
/** Ports from `lsof -nP -iTCP -sTCP:LISTEN` rows ending `127.0.0.1:58055 (LISTEN)`. */
|
|
72
|
+
export function parseLsofPorts(output) {
|
|
73
|
+
return [...output.matchAll(/:(\d+) \(LISTEN\)\s*$/gm)].map((m) => Number(m[1]));
|
|
74
|
+
}
|
|
75
|
+
/** Ports from `netstat -ano -p TCP` rows: `TCP 0.0.0.0:9001 0.0.0.0:0 LISTENING 4`. */
|
|
76
|
+
export function parseNetstatPorts(output) {
|
|
77
|
+
return [...output.matchAll(/^\s*TCP\s+\S*:(\d+)\s+\S+\s+LISTENING\b/gim)].map((m) => Number(m[1]));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Ports with a LISTEN socket, or null when no discovery tool answered.
|
|
81
|
+
*
|
|
82
|
+
* Null is not "no ports": reporting every port as free would let a caller hand
|
|
83
|
+
* out an occupied port, so callers that need a real answer use `portListening`,
|
|
84
|
+
* which falls back to connecting.
|
|
85
|
+
*/
|
|
86
|
+
export function listeningPorts(platform = process.platform, run = runTool) {
|
|
87
|
+
if (platform === "win32") {
|
|
88
|
+
const r = run("netstat", ["-ano", "-p", "TCP"]);
|
|
89
|
+
return !r.missing && r.status === 0 ? new Set(parseNetstatPorts(r.stdout)) : null;
|
|
90
|
+
}
|
|
91
|
+
if (platform === "linux") {
|
|
92
|
+
const ss = run("ss", ["-tlnH"]);
|
|
93
|
+
if (!ss.missing && ss.status === 0)
|
|
94
|
+
return new Set(parseSsPorts(ss.stdout));
|
|
95
|
+
}
|
|
96
|
+
// macOS and BSD, or Linux without iproute2. lsof reports a failure when any
|
|
97
|
+
// single process is unreadable while stdout still holds usable rows, so
|
|
98
|
+
// judge it on the output.
|
|
99
|
+
const lsof = run("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
|
|
100
|
+
if (!lsof.missing && lsof.stdout.trim())
|
|
101
|
+
return new Set(parseLsofPorts(lsof.stdout));
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/** True when something accepts a TCP connection on 127.0.0.1:<port>. */
|
|
105
|
+
export function connectProbe(port, timeoutMs = 400) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const socket = connect({ port, host: "127.0.0.1" });
|
|
108
|
+
const done = (result) => {
|
|
109
|
+
socket.destroy();
|
|
110
|
+
resolve(result);
|
|
111
|
+
};
|
|
112
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
113
|
+
socket.once("connect", () => done(true));
|
|
114
|
+
socket.once("error", () => done(false));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Whether `port` has a listener, by the platform tool when there is one, else by connecting. */
|
|
118
|
+
export async function portListening(port) {
|
|
119
|
+
const ports = listeningPorts();
|
|
120
|
+
return ports ? ports.has(port) : connectProbe(port);
|
|
121
|
+
}
|
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
/** Overrides the directory that holds tunnel state and config (a host keeps it out of `.cache/`). */
|
|
2
|
+
export declare const TUNNEL_DIR_ENV = "HARNERY_TUNNEL_DIR";
|
|
3
|
+
/** Explicit cloudflared binary, for a host that ships a managed copy. */
|
|
4
|
+
export declare const CLOUDFLARED_ENV = "HARNERY_CLOUDFLARED";
|
|
5
|
+
/**
|
|
6
|
+
* Where tunnel state lives: `$HARNERY_TUNNEL_DIR` when set, else
|
|
7
|
+
* `<root>/.cache/tunnel/`. Root defaults to cwd for the command surface and can
|
|
8
|
+
* be supplied by callers that already resolved a repo.
|
|
9
|
+
*/
|
|
10
|
+
export declare function tunnelDir(root?: string, env?: NodeJS.ProcessEnv): string;
|
|
1
11
|
/** The default instance name when `--name` is omitted. */
|
|
2
12
|
export declare const DEFAULT_INSTANCE = "default";
|
|
3
13
|
export declare function gateLogFile(name: string): string;
|
|
4
14
|
export declare function cfdLogFile(name: string): string;
|
|
5
15
|
export declare function providerLogFile(name: string, provider: TunnelProvider): string;
|
|
6
16
|
export interface TunnelConfig {
|
|
17
|
+
/** Exact addresses and CIDR ranges the gate admits (Cloudflare provider). */
|
|
7
18
|
allowed_ips: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Entries `allow add --current` added for this machine's own public
|
|
21
|
+
* addresses. They are a subset of `allowed_ips`; the next refresh replaces
|
|
22
|
+
* exactly these and never touches an entry added by hand.
|
|
23
|
+
*/
|
|
24
|
+
auto_allowed?: string[];
|
|
25
|
+
/** Explicit cloudflared binary; `$HARNERY_CLOUDFLARED` wins over this. */
|
|
26
|
+
cloudflared_path?: string;
|
|
8
27
|
}
|
|
9
28
|
export type TunnelProvider = "cloudflare" | "tailscale";
|
|
10
29
|
export type TailscaleMode = "serve" | "funnel";
|
|
@@ -21,6 +40,8 @@ export interface TunnelState {
|
|
|
21
40
|
target: string;
|
|
22
41
|
vhost: string;
|
|
23
42
|
gate_port: number;
|
|
43
|
+
/** URL path prefixes the gate forwards; every other path is refused. */
|
|
44
|
+
allow_paths: string[];
|
|
24
45
|
tailscale_mode?: TailscaleMode;
|
|
25
46
|
tailscale_path?: string;
|
|
26
47
|
tailscale_https_port?: number;
|
|
@@ -39,6 +60,7 @@ export declare function isProcessAlive(pid: number): boolean;
|
|
|
39
60
|
export type ProcessAliveCheck = (pid: number) => boolean;
|
|
40
61
|
/** True when both the local gate and its provider-side process are live. */
|
|
41
62
|
export declare function isTunnelStateLive(state: TunnelState, processAlive?: ProcessAliveCheck): boolean;
|
|
63
|
+
export declare function tunnelTargetPort(state: TunnelState): number | null;
|
|
42
64
|
/**
|
|
43
65
|
* Newest live tunnel that serves `vhost` on the requested local web port.
|
|
44
66
|
*
|
|
@@ -51,9 +73,17 @@ export declare function isTunnelStateLive(state: TunnelState, processAlive?: Pro
|
|
|
51
73
|
* actually serves the caller's origin.
|
|
52
74
|
*/
|
|
53
75
|
export declare function findLiveTunnelForOrigin(port: number, vhost: string, root?: string, processAlive?: ProcessAliveCheck): TunnelState | null;
|
|
76
|
+
/** True when the tunnel's path scope forwards every one of `paths`. */
|
|
77
|
+
export declare function tunnelServesPaths(state: TunnelState, paths: readonly string[]): boolean;
|
|
54
78
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
79
|
+
* Resolve the cloudflared binary:
|
|
80
|
+
* 1. `$HARNERY_CLOUDFLARED`, then `cloudflared_path` in the tunnel config: an
|
|
81
|
+
* explicit choice, so a missing or non-runnable file is an error, never a
|
|
82
|
+
* silent fall-through to some other copy.
|
|
83
|
+
* 2. `cloudflared` on PATH.
|
|
84
|
+
* 3. `~/.local/bin/cloudflared`.
|
|
85
|
+
* 4. Linux x86-64 and arm64: download the latest release to (3).
|
|
86
|
+
* Elsewhere it throws with install guidance.
|
|
57
87
|
*/
|
|
58
|
-
export declare function ensureCloudflared(): string;
|
|
88
|
+
export declare function ensureCloudflared(env?: NodeJS.ProcessEnv, configuredPath?: string | undefined): string;
|
|
59
89
|
//# sourceMappingURL=state.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"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"}
|