harnery 0.42.0 → 0.44.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/README.md +3 -3
- package/dist/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +95 -52
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +16 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- 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/commands/workflow.js +24 -30
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +19 -17
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +64 -43
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -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/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- 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/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +120 -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/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/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +9 -1
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +47 -27
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/servers/index.d.ts +24 -13
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +132 -92
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +3 -9
- 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/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +9 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/run-state.d.ts +3 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +31 -27
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +270 -6
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +21 -13
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- 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/gate.js +13 -6
- 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 +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +8 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +151 -74
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +16 -4
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/commands/workflow.ts +30 -30
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +35 -20
- package/src/core/agents/render/session-context.ts +92 -48
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +57 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +139 -22
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -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/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +71 -24
- package/src/core/resources/status.ts +57 -8
- package/src/core/servers/index.ts +177 -91
- package/src/core/servers/tunnels.ts +3 -11
- package/src/core/storage/process-log.ts +17 -2
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +88 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/run-state.ts +31 -29
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +315 -5
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +58 -0
- package/src/core/workflow/workspaces/recovery.ts +25 -13
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
- package/src/lib/zip/index.ts +483 -0
package/src/lib/tunnel/gate.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// available; stdout/stderr is captured into .cache/tunnel/gate.log by the
|
|
13
13
|
// spawner.
|
|
14
14
|
|
|
15
|
+
import { allowlistMatches, compileAllowlist } from "./allowlist.ts";
|
|
15
16
|
import { renderTunnelErrorPage } from "./error-page";
|
|
16
17
|
import { applyUpstreamHeaders } from "./forward-headers";
|
|
17
18
|
import { ALLOW_PATHS_ENV, isPathAllowed, parseAllowPathsEnv } from "./path-scope.ts";
|
|
@@ -27,12 +28,8 @@ function argvFlag(flag: string): string | undefined {
|
|
|
27
28
|
return i !== -1 ? process.argv[i + 1] : undefined;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.split(",")
|
|
33
|
-
.map((s) => s.trim())
|
|
34
|
-
.filter(Boolean),
|
|
35
|
-
);
|
|
31
|
+
// Entries are exact addresses or CIDR ranges (`203.0.113.0/24`, `2601:db8:1:2::/64`).
|
|
32
|
+
const ALLOW = compileAllowlist((process.env.HARNERY_TUNNEL_ALLOW ?? "").split(","));
|
|
36
33
|
const TARGET = process.env.HARNERY_TUNNEL_TARGET ?? "127.0.0.1:8001";
|
|
37
34
|
const VHOST = process.env.HARNERY_TUNNEL_VHOST ?? "localhost";
|
|
38
35
|
const PORT = Number(process.env.HARNERY_TUNNEL_PORT ?? argvFlag("--port") ?? "9001");
|
|
@@ -81,7 +78,7 @@ const server = Bun.serve<WsData, never>({
|
|
|
81
78
|
const url = new URL(req.url);
|
|
82
79
|
if (ACCESS === "cloudflare-allowlist") {
|
|
83
80
|
const ip = req.headers.get("cf-connecting-ip") ?? "";
|
|
84
|
-
if (!ALLOW
|
|
81
|
+
if (!allowlistMatches(ALLOW, ip)) {
|
|
85
82
|
const details = requestDetails(req, url);
|
|
86
83
|
// Log denials so operators can whitelist a phone/laptop that just
|
|
87
84
|
// hit 403 without asking the human to dig up their public IP.
|
|
@@ -213,4 +210,15 @@ const server = Bun.serve<WsData, never>({
|
|
|
213
210
|
console.log(`harn-tunnel-gate :${server.port} -> ${UPSTREAM_HTTP} (Host: ${VHOST})`); // lint-ok-emission: detached worker, see file note above
|
|
214
211
|
console.log(`access: ${ACCESS}`); // lint-ok-emission: detached worker, see file note above
|
|
215
212
|
console.log(`paths: ${ALLOW_PATHS.join(", ") || "(empty, every path refused)"}`); // lint-ok-emission: detached worker, see file note above
|
|
216
|
-
console.log(
|
|
213
|
+
console.log(
|
|
214
|
+
// lint-ok-emission: detached worker, see file note above
|
|
215
|
+
`allow: ${
|
|
216
|
+
process.env.HARNERY_TUNNEL_ALLOW?.split(",")
|
|
217
|
+
.filter((s) => s.trim())
|
|
218
|
+
.join(", ") || "(empty, not used outside allowlist mode)"
|
|
219
|
+
}`,
|
|
220
|
+
);
|
|
221
|
+
if (ALLOW.invalid.length > 0) {
|
|
222
|
+
// lint-ok-emission: detached worker, see file note above
|
|
223
|
+
console.log(`allow: ignoring unreadable entries: ${ALLOW.invalid.join(", ")}`);
|
|
224
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { connect } from "node:net";
|
|
11
|
+
|
|
12
|
+
type Run = (
|
|
13
|
+
command: string,
|
|
14
|
+
args: string[],
|
|
15
|
+
) => { status: number | null; stdout: string; missing: boolean };
|
|
16
|
+
|
|
17
|
+
const runTool: Run = (command, args) => {
|
|
18
|
+
const r = spawnSync(command, args, { encoding: "utf-8", windowsHide: true, timeout: 15_000 });
|
|
19
|
+
const missing = (r.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
20
|
+
return { status: r.status, stdout: typeof r.stdout === "string" ? r.stdout : "", missing };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface ProcessRow {
|
|
24
|
+
pid: number;
|
|
25
|
+
command: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Parse `ps -axo pid=,command=` output (also the shape the PowerShell probe emits). */
|
|
29
|
+
export function parseProcessRows(output: string): ProcessRow[] {
|
|
30
|
+
const rows: ProcessRow[] = [];
|
|
31
|
+
for (const line of output.split(/\r?\n/)) {
|
|
32
|
+
const m = line.match(/^\s*(\d+)\s+(.*\S)\s*$/);
|
|
33
|
+
if (m) rows.push({ pid: Number(m[1]), command: m[2] as string });
|
|
34
|
+
}
|
|
35
|
+
return rows;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const POWERSHELL_PROCESS_LIST =
|
|
39
|
+
"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine } | " +
|
|
40
|
+
'ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }';
|
|
41
|
+
|
|
42
|
+
/** Every process with a command line, or null when the platform gives no way to list them. */
|
|
43
|
+
export function listProcesses(
|
|
44
|
+
platform: NodeJS.Platform = process.platform,
|
|
45
|
+
run: Run = runTool,
|
|
46
|
+
): ProcessRow[] | null {
|
|
47
|
+
if (platform === "win32") {
|
|
48
|
+
for (const shell of ["powershell.exe", "pwsh.exe", "pwsh"]) {
|
|
49
|
+
const r = run(shell, ["-NoProfile", "-NonInteractive", "-Command", POWERSHELL_PROCESS_LIST]);
|
|
50
|
+
if (r.missing) continue;
|
|
51
|
+
if (r.status === 0) return parseProcessRows(r.stdout);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const ps = run("ps", ["-axo", "pid=,command="]);
|
|
56
|
+
return ps.status === 0 ? parseProcessRows(ps.stdout) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* PIDs whose command line matches `pattern`, an extended regular expression
|
|
61
|
+
* (the intersection of `pgrep -f` and JavaScript syntax). Returns [] when
|
|
62
|
+
* nothing matches or no listing tool works.
|
|
63
|
+
*/
|
|
64
|
+
export function findPidsByCommandLine(
|
|
65
|
+
pattern: string,
|
|
66
|
+
platform: NodeJS.Platform = process.platform,
|
|
67
|
+
run: Run = runTool,
|
|
68
|
+
): number[] {
|
|
69
|
+
if (platform !== "win32") {
|
|
70
|
+
// `--` ends options: the provider pattern starts with `--url`, which pgrep
|
|
71
|
+
// would otherwise reject as an unknown flag and report as "no match".
|
|
72
|
+
const r = run("pgrep", ["-f", "--", pattern]);
|
|
73
|
+
if (!r.missing) {
|
|
74
|
+
if (r.status !== 0) return [];
|
|
75
|
+
return r.stdout
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((line) => Number(line.trim()))
|
|
78
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const rows = listProcesses(platform, run);
|
|
82
|
+
if (!rows) return [];
|
|
83
|
+
const re = new RegExp(pattern);
|
|
84
|
+
return rows.filter((row) => re.test(row.command)).map((row) => row.pid);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Ports from `ss -tlnH`: `LISTEN 0 4096 127.0.0.1:9001 0.0.0.0:*`. */
|
|
88
|
+
export function parseSsPorts(output: string): number[] {
|
|
89
|
+
return [...output.matchAll(/:(\d+)\s/g)].map((m) => Number(m[1]));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Ports from `lsof -nP -iTCP -sTCP:LISTEN` rows ending `127.0.0.1:58055 (LISTEN)`. */
|
|
93
|
+
export function parseLsofPorts(output: string): number[] {
|
|
94
|
+
return [...output.matchAll(/:(\d+) \(LISTEN\)\s*$/gm)].map((m) => Number(m[1]));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Ports from `netstat -ano -p TCP` rows: `TCP 0.0.0.0:9001 0.0.0.0:0 LISTENING 4`. */
|
|
98
|
+
export function parseNetstatPorts(output: string): number[] {
|
|
99
|
+
return [...output.matchAll(/^\s*TCP\s+\S*:(\d+)\s+\S+\s+LISTENING\b/gim)].map((m) =>
|
|
100
|
+
Number(m[1]),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Ports with a LISTEN socket, or null when no discovery tool answered.
|
|
106
|
+
*
|
|
107
|
+
* Null is not "no ports": reporting every port as free would let a caller hand
|
|
108
|
+
* out an occupied port, so callers that need a real answer use `portListening`,
|
|
109
|
+
* which falls back to connecting.
|
|
110
|
+
*/
|
|
111
|
+
export function listeningPorts(
|
|
112
|
+
platform: NodeJS.Platform = process.platform,
|
|
113
|
+
run: Run = runTool,
|
|
114
|
+
): Set<number> | null {
|
|
115
|
+
if (platform === "win32") {
|
|
116
|
+
const r = run("netstat", ["-ano", "-p", "TCP"]);
|
|
117
|
+
return !r.missing && r.status === 0 ? new Set(parseNetstatPorts(r.stdout)) : null;
|
|
118
|
+
}
|
|
119
|
+
if (platform === "linux") {
|
|
120
|
+
const ss = run("ss", ["-tlnH"]);
|
|
121
|
+
if (!ss.missing && ss.status === 0) return new Set(parseSsPorts(ss.stdout));
|
|
122
|
+
}
|
|
123
|
+
// macOS and BSD, or Linux without iproute2. lsof reports a failure when any
|
|
124
|
+
// single process is unreadable while stdout still holds usable rows, so
|
|
125
|
+
// judge it on the output.
|
|
126
|
+
const lsof = run("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
|
|
127
|
+
if (!lsof.missing && lsof.stdout.trim()) return new Set(parseLsofPorts(lsof.stdout));
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** True when something accepts a TCP connection on 127.0.0.1:<port>. */
|
|
132
|
+
export function connectProbe(port: number, timeoutMs = 400): Promise<boolean> {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const socket = connect({ port, host: "127.0.0.1" });
|
|
135
|
+
const done = (result: boolean) => {
|
|
136
|
+
socket.destroy();
|
|
137
|
+
resolve(result);
|
|
138
|
+
};
|
|
139
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
140
|
+
socket.once("connect", () => done(true));
|
|
141
|
+
socket.once("error", () => done(false));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Whether `port` has a listener, by the platform tool when there is one, else by connecting. */
|
|
146
|
+
export async function portListening(port: number): Promise<boolean> {
|
|
147
|
+
const ports = listeningPorts();
|
|
148
|
+
return ports ? ports.has(port) : connectProbe(port);
|
|
149
|
+
}
|
package/src/lib/tunnel/state.ts
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
// Tunnel config + state persistence + provider helpers. Commands default to
|
|
2
2
|
// <cwd>/.cache/tunnel/; gitignored, so the allowlist is per-machine.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
5
|
import {
|
|
6
|
+
accessSync,
|
|
7
|
+
chmodSync,
|
|
8
|
+
constants,
|
|
6
9
|
existsSync,
|
|
7
10
|
mkdirSync,
|
|
8
11
|
readdirSync,
|
|
9
12
|
readFileSync,
|
|
13
|
+
statSync,
|
|
10
14
|
unlinkSync,
|
|
11
15
|
writeFileSync,
|
|
12
16
|
} from "node:fs";
|
|
13
|
-
import {
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { delimiter, join, resolve } from "node:path";
|
|
14
19
|
import { isPathAllowed } from "./path-scope.ts";
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
/** Overrides the directory that holds tunnel state and config (a host keeps it out of `.cache/`). */
|
|
22
|
+
export const TUNNEL_DIR_ENV = "HARNERY_TUNNEL_DIR";
|
|
23
|
+
/** Explicit cloudflared binary, for a host that ships a managed copy. */
|
|
24
|
+
export const CLOUDFLARED_ENV = "HARNERY_CLOUDFLARED";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Where tunnel state lives: `$HARNERY_TUNNEL_DIR` when set, else
|
|
28
|
+
* `<root>/.cache/tunnel/`. Root defaults to cwd for the command surface and can
|
|
29
|
+
* be supplied by callers that already resolved a repo.
|
|
30
|
+
*/
|
|
31
|
+
export function tunnelDir(
|
|
32
|
+
root: string = process.cwd(),
|
|
33
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
34
|
+
): string {
|
|
35
|
+
const override = env[TUNNEL_DIR_ENV]?.trim();
|
|
36
|
+
return override ? resolve(override) : resolve(root, ".cache", "tunnel");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cachePath(filename: string, root: string = process.cwd()): string {
|
|
40
|
+
const dir = tunnelDir(root);
|
|
20
41
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
21
42
|
return resolve(dir, filename);
|
|
22
43
|
}
|
|
@@ -63,7 +84,16 @@ const DEFAULT_CONFIG: TunnelConfig = {
|
|
|
63
84
|
};
|
|
64
85
|
|
|
65
86
|
export interface TunnelConfig {
|
|
87
|
+
/** Exact addresses and CIDR ranges the gate admits (Cloudflare provider). */
|
|
66
88
|
allowed_ips: string[];
|
|
89
|
+
/**
|
|
90
|
+
* Entries `allow add --current` added for this machine's own public
|
|
91
|
+
* addresses. They are a subset of `allowed_ips`; the next refresh replaces
|
|
92
|
+
* exactly these and never touches an entry added by hand.
|
|
93
|
+
*/
|
|
94
|
+
auto_allowed?: string[];
|
|
95
|
+
/** Explicit cloudflared binary; `$HARNERY_CLOUDFLARED` wins over this. */
|
|
96
|
+
cloudflared_path?: string;
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
export type TunnelProvider = "cloudflare" | "tailscale";
|
|
@@ -104,27 +134,31 @@ function normalizeState(raw: TunnelState, fallbackName: string): TunnelState {
|
|
|
104
134
|
}
|
|
105
135
|
|
|
106
136
|
export function readConfig(): TunnelConfig {
|
|
107
|
-
const p = cachePath(
|
|
137
|
+
const p = cachePath(CONFIG_FILE);
|
|
108
138
|
if (!existsSync(p)) {
|
|
109
139
|
writeConfig(DEFAULT_CONFIG);
|
|
110
140
|
return { ...DEFAULT_CONFIG, allowed_ips: [...DEFAULT_CONFIG.allowed_ips] };
|
|
111
141
|
}
|
|
112
142
|
try {
|
|
113
|
-
|
|
143
|
+
const parsed = JSON.parse(readFileSync(p, "utf-8")) as Partial<TunnelConfig>;
|
|
144
|
+
return {
|
|
145
|
+
...parsed,
|
|
146
|
+
allowed_ips: Array.isArray(parsed.allowed_ips) ? parsed.allowed_ips : [],
|
|
147
|
+
};
|
|
114
148
|
} catch {
|
|
115
149
|
return { ...DEFAULT_CONFIG, allowed_ips: [...DEFAULT_CONFIG.allowed_ips] };
|
|
116
150
|
}
|
|
117
151
|
}
|
|
118
152
|
|
|
119
153
|
export function writeConfig(cfg: TunnelConfig): void {
|
|
120
|
-
writeFileSync(cachePath(
|
|
154
|
+
writeFileSync(cachePath(CONFIG_FILE), JSON.stringify(cfg, null, 2));
|
|
121
155
|
}
|
|
122
156
|
|
|
123
157
|
export function readState(
|
|
124
158
|
name: string = DEFAULT_INSTANCE,
|
|
125
159
|
root: string = process.cwd(),
|
|
126
160
|
): TunnelState | null {
|
|
127
|
-
const p = cachePath(
|
|
161
|
+
const p = cachePath(stateFile(name), root);
|
|
128
162
|
if (!existsSync(p)) return null;
|
|
129
163
|
try {
|
|
130
164
|
return normalizeState(JSON.parse(readFileSync(p, "utf-8")) as TunnelState, name);
|
|
@@ -134,11 +168,11 @@ export function readState(
|
|
|
134
168
|
}
|
|
135
169
|
|
|
136
170
|
export function writeState(state: TunnelState, root: string = process.cwd()): void {
|
|
137
|
-
writeFileSync(cachePath(
|
|
171
|
+
writeFileSync(cachePath(stateFile(state.name), root), JSON.stringify(state, null, 2));
|
|
138
172
|
}
|
|
139
173
|
|
|
140
174
|
export function clearState(name: string = DEFAULT_INSTANCE, root: string = process.cwd()): void {
|
|
141
|
-
const p = cachePath(
|
|
175
|
+
const p = cachePath(stateFile(name), root);
|
|
142
176
|
if (existsSync(p)) unlinkSync(p);
|
|
143
177
|
}
|
|
144
178
|
|
|
@@ -147,7 +181,7 @@ export function clearState(name: string = DEFAULT_INSTANCE, root: string = proce
|
|
|
147
181
|
* `state*.json` under `.cache/tunnel/`; tolerates missing/corrupt files.
|
|
148
182
|
*/
|
|
149
183
|
export function listStates(root: string = process.cwd()): TunnelState[] {
|
|
150
|
-
const dir =
|
|
184
|
+
const dir = tunnelDir(root);
|
|
151
185
|
if (!existsSync(dir)) return [];
|
|
152
186
|
const out: TunnelState[] = [];
|
|
153
187
|
for (const file of readdirSync(dir)) {
|
|
@@ -226,31 +260,93 @@ export function tunnelServesPaths(state: TunnelState, paths: readonly string[]):
|
|
|
226
260
|
return paths.every((path) => isPathAllowed(path, state.allow_paths));
|
|
227
261
|
}
|
|
228
262
|
|
|
229
|
-
|
|
230
|
-
* Ensure cloudflared is on PATH or installed at ~/.local/bin/cloudflared.
|
|
231
|
-
* Auto-downloads on Linux; throws on other platforms with brew hint.
|
|
232
|
-
*/
|
|
233
|
-
export function ensureCloudflared(): string {
|
|
263
|
+
function isRunnableFile(path: string): boolean {
|
|
234
264
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
265
|
+
if (!statSync(path).isFile()) return false;
|
|
266
|
+
if (process.platform === "win32") return true;
|
|
267
|
+
accessSync(path, constants.X_OK);
|
|
268
|
+
return true;
|
|
237
269
|
} catch {
|
|
238
|
-
|
|
270
|
+
return false;
|
|
239
271
|
}
|
|
240
|
-
|
|
241
|
-
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** First `name` on PATH (honoring `.exe` on Windows), or null. Needs no shell. */
|
|
275
|
+
function findOnPath(name: string, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
276
|
+
const names = process.platform === "win32" ? [`${name}.exe`, name] : [name];
|
|
277
|
+
for (const dir of (env.PATH ?? env.Path ?? "").split(delimiter)) {
|
|
278
|
+
if (!dir) continue;
|
|
279
|
+
for (const candidate of names) {
|
|
280
|
+
const full = join(dir, candidate);
|
|
281
|
+
if (isRunnableFile(full)) return full;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the cloudflared binary:
|
|
289
|
+
* 1. `$HARNERY_CLOUDFLARED`, then `cloudflared_path` in the tunnel config: an
|
|
290
|
+
* explicit choice, so a missing or non-runnable file is an error, never a
|
|
291
|
+
* silent fall-through to some other copy.
|
|
292
|
+
* 2. `cloudflared` on PATH.
|
|
293
|
+
* 3. `~/.local/bin/cloudflared`.
|
|
294
|
+
* 4. Linux x86-64 and arm64: download the latest release to (3).
|
|
295
|
+
* Elsewhere it throws with install guidance.
|
|
296
|
+
*/
|
|
297
|
+
export function ensureCloudflared(
|
|
298
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
299
|
+
configuredPath: string | undefined = readConfig().cloudflared_path,
|
|
300
|
+
): string {
|
|
301
|
+
const explicit = env[CLOUDFLARED_ENV]?.trim();
|
|
302
|
+
const fromConfig = configuredPath?.trim();
|
|
303
|
+
for (const [source, path] of [
|
|
304
|
+
[CLOUDFLARED_ENV, explicit],
|
|
305
|
+
["cloudflared_path in the tunnel config", fromConfig],
|
|
306
|
+
] as const) {
|
|
307
|
+
if (!path) continue;
|
|
308
|
+
if (isRunnableFile(path)) return path;
|
|
309
|
+
throw new Error(
|
|
310
|
+
`cloudflared is set by ${source} to ${path}, but that file is missing or not runnable.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const onPath = findOnPath("cloudflared", env);
|
|
315
|
+
if (onPath) return onPath;
|
|
316
|
+
const exe = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
317
|
+
const local = join(homedir(), ".local", "bin", exe);
|
|
318
|
+
if (isRunnableFile(local)) return local;
|
|
242
319
|
|
|
243
|
-
|
|
320
|
+
const asset =
|
|
321
|
+
process.platform === "linux"
|
|
322
|
+
? (
|
|
323
|
+
{ x64: "cloudflared-linux-amd64", arm64: "cloudflared-linux-arm64" } as Record<
|
|
324
|
+
string,
|
|
325
|
+
string
|
|
326
|
+
>
|
|
327
|
+
)[process.arch]
|
|
328
|
+
: undefined;
|
|
329
|
+
if (!asset) {
|
|
244
330
|
throw new Error(
|
|
245
|
-
"cloudflared not installed.
|
|
331
|
+
"cloudflared not installed. Install it (macOS: `brew install cloudflared`; Windows: " +
|
|
332
|
+
"`winget install Cloudflare.cloudflared`), or point `$HARNERY_CLOUDFLARED` at a binary. " +
|
|
246
333
|
"On Linux it auto-installs to ~/.local/bin/cloudflared.",
|
|
247
334
|
);
|
|
248
335
|
}
|
|
249
336
|
|
|
250
337
|
process.stderr.write("Installing cloudflared to ~/.local/bin/...\n"); // lint-ok-emission: sync setup phase before structured output; pairs with the inherited stdio of the curl below
|
|
251
|
-
|
|
252
|
-
|
|
338
|
+
mkdirSync(join(homedir(), ".local", "bin"), { recursive: true });
|
|
339
|
+
const download = spawnSync(
|
|
340
|
+
"curl",
|
|
341
|
+
[
|
|
342
|
+
"-fsSL",
|
|
343
|
+
`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
|
|
344
|
+
"-o",
|
|
345
|
+
local,
|
|
346
|
+
],
|
|
253
347
|
{ stdio: "inherit" },
|
|
254
348
|
);
|
|
349
|
+
if (download.status !== 0) throw new Error("Downloading cloudflared failed; install it by hand.");
|
|
350
|
+
chmodSync(local, 0o755);
|
|
255
351
|
return local;
|
|
256
352
|
}
|