harnery 0.40.0 → 0.42.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 +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +34 -20
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +4 -0
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +388 -45
- package/dist/commands/checkpoint.js +8 -12
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/deinit.d.ts +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +8 -4
- 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/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +2 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +58 -3
- package/dist/commands/fetch.d.ts.map +1 -1
- package/dist/commands/fetch.js +10 -0
- 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 +21 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +271 -109
- package/dist/commands/instructions.js +5 -4
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +2 -1
- 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.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/commands/webview.d.ts +4 -0
- package/dist/commands/webview.d.ts.map +1 -0
- package/dist/commands/webview.js +94 -0
- package/dist/core/adapter.d.ts +50 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +59 -1
- package/dist/core/adapters/attestation.d.ts.map +1 -1
- package/dist/core/adapters/attestation.js +3 -2
- package/dist/core/adapters/profiles.d.ts +29 -3
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +53 -1
- package/dist/core/adapters/registry.d.ts.map +1 -1
- package/dist/core/adapters/registry.js +33 -0
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -7
- package/dist/core/agents/cli.js +25 -16
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +7 -2
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +3 -9
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +2 -8
- package/dist/core/agents/rules/stop-hook.d.ts +2 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +1 -1
- package/dist/core/agents/session-events.d.ts +6 -2
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +105 -26
- package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalization-state-v3.js +2 -1
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalizer-v3.js +5 -4
- package/dist/core/agents/session-name-display.d.ts +28 -0
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +43 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +10 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +21 -2
- 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 +61 -0
- package/dist/core/agents/state/live-coordination-writer.js +3 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +2 -10
- package/dist/core/agents/turn-changes.d.ts +17 -0
- package/dist/core/agents/turn-changes.d.ts.map +1 -0
- package/dist/core/agents/turn-changes.js +76 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +11 -3
- 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 +526 -52
- package/dist/core/config.d.ts +73 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +142 -3
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +2 -1
- package/dist/core/conversations/archive.d.ts.map +1 -1
- package/dist/core/conversations/archive.js +4 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +5 -4
- package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/canary.js +5 -4
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/compression.js +2 -1
- package/dist/core/events/v3/adapter-id.d.ts +10 -2
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
- package/dist/core/events/v3/adapter-id.js +16 -0
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +2 -1
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +5 -4
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +17 -0
- package/dist/core/events/v3/contract.d.ts +5 -0
- package/dist/core/events/v3/contract.d.ts.map +1 -1
- package/dist/core/events/v3/contract.js +7 -1
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +3 -2
- package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
- package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
- package/dist/core/events/v3/fingerprint-keys.js +7 -6
- package/dist/core/events/v3/generated.d.ts +1 -1
- package/dist/core/events/v3/generated.js +1 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +5 -4
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +8 -7
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
- package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
- package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +10 -4
- package/dist/core/events/v3/producers/hook.d.ts +2 -0
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +16 -1
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +16 -6
- package/dist/core/events/v3/producers/recorder.d.ts +2 -4
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +125 -101
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/events/v3/recovery.d.ts.map +1 -1
- package/dist/core/events/v3/recovery.js +4 -3
- package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/inventory.js +2 -1
- package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/maintenance.js +4 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
- package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
- package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
- package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
- package/dist/core/events/v3/tool-line-changes.js +135 -0
- package/dist/core/events/v3/writer.d.ts.map +1 -1
- package/dist/core/events/v3/writer.js +8 -7
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +6 -5
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +12 -11
- package/dist/core/governor/state.d.ts.map +1 -1
- package/dist/core/governor/state.js +7 -6
- package/dist/core/guard/evaluator.js +1 -0
- package/dist/core/guard/storage.d.ts.map +1 -1
- package/dist/core/guard/storage.js +3 -2
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/index.js +20 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/types.js +1 -0
- package/dist/core/hooks/adapter/detect.d.ts +1 -1
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +2 -3
- package/dist/core/hooks/adapter/events.d.ts +46 -1
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +36 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
- package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
- package/dist/core/hooks/adapter/output.d.ts +7 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +8 -1
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +52 -8
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +133 -12
- package/dist/core/hooks/cli.js +676 -531
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +2 -1
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
- package/dist/core/hooks/prompt-context/contract.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +1 -1
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +35 -4
- 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/session-name-presence.d.ts +3 -1
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +7 -2
- package/dist/core/hooks/shell-waiter.d.ts +2 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
- package/dist/core/hooks/shell-waiter.js +173 -0
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +5 -4
- package/dist/core/semantic/review.d.ts.map +1 -1
- package/dist/core/semantic/review.js +5 -4
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +15 -14
- package/dist/core/semantic/storage.d.ts.map +1 -1
- package/dist/core/semantic/storage.js +5 -4
- 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/atomic-json.d.ts.map +1 -1
- package/dist/core/storage/atomic-json.js +7 -3
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/storage/durable-history.d.ts.map +1 -1
- package/dist/core/storage/durable-history.js +6 -5
- package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
- package/dist/core/storage/maintenance-providers.js +2 -1
- package/dist/core/storage/maintenance.d.ts.map +1 -1
- package/dist/core/storage/maintenance.js +7 -6
- package/dist/core/storage/metrics.d.ts.map +1 -1
- package/dist/core/storage/metrics.js +5 -1
- package/dist/core/storage/modes.d.ts +17 -0
- package/dist/core/storage/modes.d.ts.map +1 -0
- package/dist/core/storage/modes.js +47 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +3 -2
- package/dist/core/storage/receipts.d.ts.map +1 -1
- package/dist/core/storage/receipts.js +4 -3
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +7 -6
- package/dist/core/supervisor/hook-health.d.ts +2 -1
- package/dist/core/supervisor/hook-health.d.ts.map +1 -1
- package/dist/core/supervisor/hook-health.js +4 -3
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +7 -6
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +8 -7
- package/dist/core/workflow/adapters.d.ts +7 -0
- package/dist/core/workflow/adapters.d.ts.map +1 -1
- package/dist/core/workflow/adapters.js +9 -0
- package/dist/core/workflow/approvals.d.ts.map +1 -1
- package/dist/core/workflow/approvals.js +6 -5
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +33 -4
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +4 -3
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +2 -3
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -2
- 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 +36 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
- package/dist/core/workflow/spawn-opencode.js +135 -0
- package/dist/core/workflow/transcript.d.ts.map +1 -1
- package/dist/core/workflow/transcript.js +3 -2
- 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/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +5 -4
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +3 -2
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/paths.js +2 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +10 -9
- package/dist/lazy-commands.js +8 -1
- package/dist/lib/agent-browser/client.d.ts +9 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +13 -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/client.d.ts +35 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +117 -8
- 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/identity.d.ts +25 -0
- package/dist/lib/browser/identity.d.ts.map +1 -0
- package/dist/lib/browser/identity.js +66 -0
- package/dist/lib/browser/index.d.ts +3 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/launch-args.d.ts +26 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -1
- package/dist/lib/browser/launch-args.js +41 -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/user-agent.d.ts +53 -0
- package/dist/lib/browser/user-agent.d.ts.map +1 -0
- package/dist/lib/browser/user-agent.js +79 -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/docs-metadata-managed.d.ts +1 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +17 -1
- 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/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -9
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +2 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/pace/index.d.ts +2 -0
- package/dist/lib/pace/index.d.ts.map +1 -0
- package/dist/lib/pace/index.js +1 -0
- package/dist/lib/pace/pace.d.ts +95 -0
- package/dist/lib/pace/pace.d.ts.map +1 -0
- package/dist/lib/pace/pace.js +313 -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/forward-headers.d.ts +11 -0
- package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
- package/dist/lib/tunnel/forward-headers.js +17 -0
- package/dist/lib/tunnel/gate.js +34 -10
- 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/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/dist/lib/webview/index.d.ts +2 -0
- package/dist/lib/webview/index.d.ts.map +1 -0
- package/dist/lib/webview/index.js +1 -0
- package/dist/lib/webview/webview.d.ts +116 -0
- package/dist/lib/webview/webview.d.ts.map +1 -0
- package/dist/lib/webview/webview.js +289 -0
- package/opencode-plugin/package.json +19 -0
- package/opencode-plugin/src/index.ts +791 -0
- package/package.json +16 -4
- package/schemas/config.schema.json +104 -0
- package/schemas/event-v3.schema.json +164 -0
- package/src/commander.ts +26 -0
- package/src/commands/agents.ts +47 -18
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse-ai.ts +8 -0
- package/src/commands/browse.ts +570 -48
- package/src/commands/checkpoint.ts +7 -9
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/deinit.ts +9 -4
- package/src/commands/disk.ts +114 -0
- package/src/commands/docs.ts +2 -0
- package/src/commands/doctor.ts +63 -3
- package/src/commands/fetch.ts +14 -0
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +334 -122
- package/src/commands/instructions.ts +8 -4
- package/src/commands/ledger-v3.ts +2 -1
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/commands/webview.ts +159 -0
- package/src/core/adapter.ts +102 -1
- package/src/core/adapters/attestation.ts +3 -2
- package/src/core/adapters/profiles.ts +63 -1
- package/src/core/adapters/registry.ts +37 -0
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/canonical-emit.ts +2 -5
- package/src/core/agents/cli.ts +26 -17
- package/src/core/agents/coord-client.ts +9 -3
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/rules/claim-conflict.ts +3 -8
- package/src/core/agents/rules/commit-conflict.ts +2 -7
- package/src/core/agents/rules/stop-hook.ts +3 -1
- package/src/core/agents/session-events.ts +120 -26
- package/src/core/agents/session-finalization-state-v3.ts +2 -1
- package/src/core/agents/session-finalizer-v3.ts +5 -4
- package/src/core/agents/session-name-display.ts +48 -0
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +24 -2
- package/src/core/agents/state/live-coordination-view.ts +74 -0
- package/src/core/agents/state/live-coordination-writer.ts +3 -0
- package/src/core/agents/state/stale-sweep.ts +2 -9
- package/src/core/agents/turn-changes.ts +97 -0
- package/src/core/artifacts/delivery-card.ts +12 -3
- package/src/core/artifacts/index.ts +701 -47
- package/src/core/config.ts +206 -4
- package/src/core/context/index.ts +2 -1
- package/src/core/conversations/archive.ts +4 -3
- package/src/core/diagnostics/bundle.ts +5 -3
- package/src/core/events/legacy-storage/canary.ts +5 -4
- package/src/core/events/legacy-storage/compression.ts +2 -1
- package/src/core/events/v3/adapter-id.ts +29 -1
- package/src/core/events/v3/authority-outbox.ts +2 -1
- package/src/core/events/v3/bootstrap.ts +5 -4
- package/src/core/events/v3/capabilities.ts +17 -0
- package/src/core/events/v3/contract.ts +9 -1
- package/src/core/events/v3/control-witness.ts +3 -2
- package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
- package/src/core/events/v3/fingerprint-keys.ts +7 -6
- package/src/core/events/v3/generated.ts +1 -1
- package/src/core/events/v3/live-feed.ts +5 -4
- package/src/core/events/v3/producers/command-recorder.ts +8 -6
- package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
- package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
- package/src/core/events/v3/producers/hook-base.ts +11 -4
- package/src/core/events/v3/producers/hook.ts +19 -1
- package/src/core/events/v3/producers/intake.ts +18 -6
- package/src/core/events/v3/producers/recorder.ts +139 -114
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/events/v3/recovery.ts +4 -3
- package/src/core/events/v3/support-storage/inventory.ts +2 -1
- package/src/core/events/v3/support-storage/maintenance.ts +8 -3
- package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
- package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
- package/src/core/events/v3/tool-line-changes.ts +139 -0
- package/src/core/events/v3/writer.ts +8 -7
- package/src/core/governor/planning.ts +6 -5
- package/src/core/governor/service.ts +12 -11
- package/src/core/governor/state.ts +7 -6
- package/src/core/guard/evaluator.ts +1 -0
- package/src/core/guard/storage.ts +3 -2
- package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
- package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
- package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
- package/src/core/hooks/adapter/behaviors/index.ts +31 -0
- package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
- package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
- package/src/core/hooks/adapter/behaviors/types.ts +110 -0
- package/src/core/hooks/adapter/detect.ts +2 -3
- package/src/core/hooks/adapter/events.ts +72 -1
- package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
- package/src/core/hooks/adapter/output.ts +8 -1
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +198 -16
- package/src/core/hooks/cli.ts +776 -539
- package/src/core/hooks/effects/image-capture.ts +2 -1
- package/src/core/hooks/prompt-context/contract.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +42 -5
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/hooks/session-name-presence.ts +10 -3
- package/src/core/hooks/shell-waiter.ts +166 -0
- package/src/core/resources/service.ts +5 -4
- package/src/core/semantic/review.ts +5 -4
- package/src/core/semantic/service.ts +15 -14
- package/src/core/semantic/storage.ts +5 -4
- 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/atomic-json.ts +7 -3
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/storage/durable-history.ts +6 -5
- package/src/core/storage/maintenance-providers.ts +2 -1
- package/src/core/storage/maintenance.ts +7 -6
- package/src/core/storage/metrics.ts +5 -1
- package/src/core/storage/modes.ts +53 -0
- package/src/core/storage/process-log.ts +3 -2
- package/src/core/storage/receipts.ts +4 -3
- package/src/core/storage/segments.ts +7 -6
- package/src/core/supervisor/hook-health.ts +4 -4
- package/src/core/supervisor/service.ts +7 -6
- package/src/core/work/state.ts +8 -7
- package/src/core/workflow/adapters.ts +15 -0
- package/src/core/workflow/approvals.ts +6 -5
- package/src/core/workflow/billing.ts +34 -4
- package/src/core/workflow/durable-record.ts +4 -3
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/live-session-v3.ts +2 -3
- package/src/core/workflow/run-state.ts +3 -2
- 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 +153 -0
- package/src/core/workflow/transcript.ts +3 -2
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/core/workflow/workspaces/leases.ts +5 -4
- package/src/core/workflow/workspaces/local-git.ts +3 -2
- package/src/core/workflow/workspaces/paths.ts +2 -1
- package/src/core/workflow/workspaces/state.ts +10 -9
- package/src/lazy-commands.ts +11 -1
- package/src/lib/agent-browser/client.ts +18 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/client.ts +147 -9
- 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/identity.ts +64 -0
- package/src/lib/browser/index.ts +16 -1
- package/src/lib/browser/launch-args.ts +53 -1
- 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/user-agent.ts +130 -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/docs-metadata-managed.ts +21 -0
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/identities/assume.ts +7 -7
- package/src/lib/instructions/apply.ts +2 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/pace/index.ts +14 -0
- package/src/lib/pace/pace.ts +370 -0
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/forward-headers.ts +16 -0
- package/src/lib/tunnel/gate.ts +39 -10
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
- package/src/lib/webview/index.ts +13 -0
- package/src/lib/webview/webview.ts +457 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Human-pace gate for page loads.
|
|
6
|
+
*
|
|
7
|
+
* Every Harnery surface that loads a web page (`browse`, `browse-ai`, `fetch`,
|
|
8
|
+
* and the headed browse-session verbs) reserves a slot here before the request
|
|
9
|
+
* goes out. Consecutive loads against the same registrable site are spaced by
|
|
10
|
+
* a random gap inside `[minMs, maxMs]`, the way a person reads one page before
|
|
11
|
+
* opening the next. Loads against different sites do not wait on each other.
|
|
12
|
+
* Loopback, private-network, single-label, and reserved-suffix hosts are exempt
|
|
13
|
+
* so local development and page QA keep running at machine speed.
|
|
14
|
+
*
|
|
15
|
+
* The ledger is a small machine-local JSON file (`~/.cache/harnery/pace.json`)
|
|
16
|
+
* shared by every process, so separate `harn fetch` calls in a shell loop are
|
|
17
|
+
* paced the same as one long `harn browse` session. A caller reserves the next
|
|
18
|
+
* slot before it sleeps, so concurrent processes queue behind each other
|
|
19
|
+
* instead of all firing at once.
|
|
20
|
+
*
|
|
21
|
+
* The gate is on by default. `HARNERY_PACE=off` disables it machine-wide and
|
|
22
|
+
* each command carries `--no-pace` for a single run.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_PACE_MIN_MS = 3_000;
|
|
25
|
+
export const DEFAULT_PACE_MAX_MS = 9_000;
|
|
26
|
+
export const DEFAULT_PACE_LEDGER = resolve(homedir(), ".cache", "harnery", "pace.json");
|
|
27
|
+
/** Entries idle this long drop out of the ledger on the next write. */
|
|
28
|
+
const LEDGER_RETENTION_MS = 60 * 60 * 1_000;
|
|
29
|
+
/** A lock older than this belongs to a dead process and is reclaimed. */
|
|
30
|
+
const LOCK_STALE_MS = 5_000;
|
|
31
|
+
/** Total time a reservation waits for the ledger lock before proceeding anyway. */
|
|
32
|
+
const LOCK_WAIT_MS = 500;
|
|
33
|
+
const LOCK_POLL_MS = 5;
|
|
34
|
+
const OFF_VALUES = new Set(["off", "0", "false", "no", "none"]);
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the pace policy from the environment.
|
|
37
|
+
*
|
|
38
|
+
* - `HARNERY_PACE`: `off` (or `0`, `false`, `no`) disables the gate. Unset or
|
|
39
|
+
* `human` keeps the default.
|
|
40
|
+
* - `HARNERY_PACE_MIN_MS` / `HARNERY_PACE_MAX_MS`: gap bounds in ms. Both must
|
|
41
|
+
* be non-negative integers with min <= max.
|
|
42
|
+
* - `HARNERY_PACE_EXEMPT`: comma-separated host suffixes that never wait.
|
|
43
|
+
* - `HARNERY_PACE_LEDGER`: ledger path override.
|
|
44
|
+
*/
|
|
45
|
+
export function pacePolicyFromEnv(env = process.env) {
|
|
46
|
+
const mode = env.HARNERY_PACE?.trim().toLowerCase();
|
|
47
|
+
if (mode && mode !== "human" && !OFF_VALUES.has(mode)) {
|
|
48
|
+
throw new Error(`HARNERY_PACE must be "human" or "off" (got "${env.HARNERY_PACE}").`);
|
|
49
|
+
}
|
|
50
|
+
const minMs = readMs(env, "HARNERY_PACE_MIN_MS", DEFAULT_PACE_MIN_MS);
|
|
51
|
+
const maxMs = readMs(env, "HARNERY_PACE_MAX_MS", DEFAULT_PACE_MAX_MS);
|
|
52
|
+
if (minMs > maxMs) {
|
|
53
|
+
throw new Error(`HARNERY_PACE_MIN_MS (${minMs}) must not exceed HARNERY_PACE_MAX_MS (${maxMs}).`);
|
|
54
|
+
}
|
|
55
|
+
const exemptSuffixes = (env.HARNERY_PACE_EXEMPT ?? "")
|
|
56
|
+
.split(",")
|
|
57
|
+
.map((s) => normalizeHost(s))
|
|
58
|
+
.filter((s) => s !== null);
|
|
59
|
+
return {
|
|
60
|
+
enabled: !(mode && OFF_VALUES.has(mode)),
|
|
61
|
+
minMs,
|
|
62
|
+
maxMs,
|
|
63
|
+
exemptSuffixes,
|
|
64
|
+
ledgerPath: env.HARNERY_PACE_LEDGER?.trim() || DEFAULT_PACE_LEDGER,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function readMs(env, name, fallback) {
|
|
68
|
+
const raw = env[name]?.trim();
|
|
69
|
+
if (!raw)
|
|
70
|
+
return fallback;
|
|
71
|
+
if (!/^\d+$/.test(raw)) {
|
|
72
|
+
throw new Error(`${name} must be a non-negative integer number of milliseconds (got "${raw}").`);
|
|
73
|
+
}
|
|
74
|
+
return Number.parseInt(raw, 10);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Public suffixes that take two labels, so the registrable site is three
|
|
78
|
+
* labels deep (`news.bbc.co.uk` -> `bbc.co.uk`). A short curated list is
|
|
79
|
+
* enough here: the gate only needs sibling hosts of one site to share a key,
|
|
80
|
+
* and a miss degrades to per-hostname pacing, never to no pacing.
|
|
81
|
+
*/
|
|
82
|
+
const TWO_LABEL_SUFFIXES = new Set([
|
|
83
|
+
"co.uk",
|
|
84
|
+
"org.uk",
|
|
85
|
+
"ac.uk",
|
|
86
|
+
"gov.uk",
|
|
87
|
+
"com.au",
|
|
88
|
+
"net.au",
|
|
89
|
+
"org.au",
|
|
90
|
+
"co.nz",
|
|
91
|
+
"co.jp",
|
|
92
|
+
"co.kr",
|
|
93
|
+
"co.in",
|
|
94
|
+
"co.za",
|
|
95
|
+
"com.br",
|
|
96
|
+
"com.mx",
|
|
97
|
+
"com.ar",
|
|
98
|
+
"com.cn",
|
|
99
|
+
"com.tw",
|
|
100
|
+
"com.sg",
|
|
101
|
+
"com.tr",
|
|
102
|
+
]);
|
|
103
|
+
/** Reserved or local-only suffixes that are never paced. */
|
|
104
|
+
const EXEMPT_SUFFIXES = ["localhost", "local", "test", "internal", "example", "invalid"];
|
|
105
|
+
function normalizeHost(raw) {
|
|
106
|
+
const host = raw.trim().toLowerCase().replace(/\.+$/, "");
|
|
107
|
+
return host ? host : null;
|
|
108
|
+
}
|
|
109
|
+
function hasSuffix(host, suffix) {
|
|
110
|
+
return host === suffix || host.endsWith(`.${suffix}`);
|
|
111
|
+
}
|
|
112
|
+
function isExemptIpv4(host) {
|
|
113
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
114
|
+
if (!m)
|
|
115
|
+
return false;
|
|
116
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
117
|
+
return (a === 127 ||
|
|
118
|
+
a === 10 ||
|
|
119
|
+
a === 0 ||
|
|
120
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
121
|
+
(a === 192 && b === 168) ||
|
|
122
|
+
(a === 169 && b === 254));
|
|
123
|
+
}
|
|
124
|
+
function isExemptIpv6(host) {
|
|
125
|
+
const h = host.replace(/^\[|\]$/g, "");
|
|
126
|
+
return h === "::1" || h === "::" || /^f[cd]/.test(h) || /^fe[89ab]/.test(h);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Registrable site a URL belongs to for pacing purposes, or `null` when the
|
|
130
|
+
* URL is exempt: a non-HTTP scheme, a loopback or private address, a
|
|
131
|
+
* single-label or reserved-suffix host, or a configured exemption.
|
|
132
|
+
*/
|
|
133
|
+
export function paceSiteKey(url, exemptSuffixes = []) {
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = new URL(url);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
142
|
+
return null;
|
|
143
|
+
const host = normalizeHost(parsed.hostname);
|
|
144
|
+
if (!host)
|
|
145
|
+
return null;
|
|
146
|
+
if (host.startsWith("[") || host.includes(":"))
|
|
147
|
+
return isExemptIpv6(host) ? null : host;
|
|
148
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host))
|
|
149
|
+
return isExemptIpv4(host) ? null : host;
|
|
150
|
+
for (const suffix of EXEMPT_SUFFIXES)
|
|
151
|
+
if (hasSuffix(host, suffix))
|
|
152
|
+
return null;
|
|
153
|
+
for (const suffix of exemptSuffixes)
|
|
154
|
+
if (hasSuffix(host, suffix))
|
|
155
|
+
return null;
|
|
156
|
+
const labels = host.split(".");
|
|
157
|
+
if (labels.length < 2)
|
|
158
|
+
return null;
|
|
159
|
+
const lastTwo = labels.slice(-2).join(".");
|
|
160
|
+
if (labels.length >= 3 && TWO_LABEL_SUFFIXES.has(lastTwo))
|
|
161
|
+
return labels.slice(-3).join(".");
|
|
162
|
+
return lastTwo;
|
|
163
|
+
}
|
|
164
|
+
/** Block the current thread for `ms`. Used by clients that drive a child process synchronously. */
|
|
165
|
+
export function sleepSync(ms) {
|
|
166
|
+
if (ms <= 0)
|
|
167
|
+
return;
|
|
168
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
169
|
+
}
|
|
170
|
+
export class PaceGate {
|
|
171
|
+
policy;
|
|
172
|
+
now;
|
|
173
|
+
random;
|
|
174
|
+
onWait;
|
|
175
|
+
constructor(policy, deps = {}) {
|
|
176
|
+
this.policy = policy;
|
|
177
|
+
this.now = deps.now ?? Date.now;
|
|
178
|
+
this.random = deps.random ?? Math.random;
|
|
179
|
+
this.onWait = deps.onWait;
|
|
180
|
+
}
|
|
181
|
+
/** A gate that never waits, for callers that opted out. */
|
|
182
|
+
static disabled() {
|
|
183
|
+
return new PaceGate({ ...pacePolicyFromEnv({}), enabled: false });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Reserve the next slot for `url` without sleeping. Returns how long the
|
|
187
|
+
* caller should wait before loading, or `null` when the URL is exempt or the
|
|
188
|
+
* gate is disabled. The reservation is recorded even when the wait is zero,
|
|
189
|
+
* so the next load of the same site owes a gap.
|
|
190
|
+
*/
|
|
191
|
+
reserve(url) {
|
|
192
|
+
if (!this.policy.enabled)
|
|
193
|
+
return null;
|
|
194
|
+
const site = paceSiteKey(url, this.policy.exemptSuffixes);
|
|
195
|
+
if (!site)
|
|
196
|
+
return null;
|
|
197
|
+
const now = this.now();
|
|
198
|
+
const gap = this.policy.minMs + this.random() * (this.policy.maxMs - this.policy.minMs);
|
|
199
|
+
let readyAt = now;
|
|
200
|
+
withLedgerLock(this.policy.ledgerPath, this.now, () => {
|
|
201
|
+
const ledger = readLedger(this.policy.ledgerPath);
|
|
202
|
+
readyAt = Math.max(now, ledger.sites[site]?.nextAllowedAt ?? 0);
|
|
203
|
+
for (const [key, entry] of Object.entries(ledger.sites)) {
|
|
204
|
+
if (entry.nextAllowedAt < now - LEDGER_RETENTION_MS)
|
|
205
|
+
delete ledger.sites[key];
|
|
206
|
+
}
|
|
207
|
+
ledger.sites[site] = { nextAllowedAt: Math.round(readyAt + gap), lastAt: readyAt };
|
|
208
|
+
writeLedger(this.policy.ledgerPath, ledger);
|
|
209
|
+
});
|
|
210
|
+
const wait = { site, waitMs: Math.max(0, Math.round(readyAt - now)) };
|
|
211
|
+
this.onWait?.(wait);
|
|
212
|
+
return wait;
|
|
213
|
+
}
|
|
214
|
+
/** Reserve a slot and wait it out asynchronously. */
|
|
215
|
+
async before(url) {
|
|
216
|
+
const wait = this.reserve(url);
|
|
217
|
+
if (wait && wait.waitMs > 0) {
|
|
218
|
+
await new Promise((done) => setTimeout(done, wait.waitMs));
|
|
219
|
+
}
|
|
220
|
+
return wait;
|
|
221
|
+
}
|
|
222
|
+
/** Reserve a slot and block the thread for it. For `spawnSync`-driven clients. */
|
|
223
|
+
beforeSync(url) {
|
|
224
|
+
const wait = this.reserve(url);
|
|
225
|
+
if (wait && wait.waitMs > 0)
|
|
226
|
+
sleepSync(wait.waitMs);
|
|
227
|
+
return wait;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Gate for a CLI command run. `null` when the run passed `--no-pace`;
|
|
232
|
+
* otherwise the environment policy, with every non-zero wait reported through
|
|
233
|
+
* `log` so the operator can see why a load took longer.
|
|
234
|
+
*/
|
|
235
|
+
export function commandPaceGate(optedIn, log) {
|
|
236
|
+
if (!optedIn)
|
|
237
|
+
return null;
|
|
238
|
+
return new PaceGate(pacePolicyFromEnv(), {
|
|
239
|
+
onWait: (wait) => {
|
|
240
|
+
if (wait.waitMs > 0)
|
|
241
|
+
log(describePaceWait(wait));
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** Describe a wait for a human-facing log line. */
|
|
246
|
+
export function describePaceWait(wait) {
|
|
247
|
+
const seconds = (wait.waitMs / 1_000).toFixed(1);
|
|
248
|
+
return `paced ${seconds}s before ${wait.site} (human pace; --no-pace or HARNERY_PACE=off skips)`;
|
|
249
|
+
}
|
|
250
|
+
function readLedger(path) {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
253
|
+
if (parsed && parsed.version === 1 && parsed.sites && typeof parsed.sites === "object") {
|
|
254
|
+
const sites = {};
|
|
255
|
+
for (const [key, entry] of Object.entries(parsed.sites)) {
|
|
256
|
+
if (entry &&
|
|
257
|
+
typeof entry.nextAllowedAt === "number" &&
|
|
258
|
+
Number.isFinite(entry.nextAllowedAt) &&
|
|
259
|
+
typeof entry.lastAt === "number" &&
|
|
260
|
+
Number.isFinite(entry.lastAt)) {
|
|
261
|
+
sites[key] = { nextAllowedAt: entry.nextAllowedAt, lastAt: entry.lastAt };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { version: 1, sites };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Missing or corrupt ledger: start fresh. A lost ledger costs one unpaced
|
|
269
|
+
// load per site, never a crash.
|
|
270
|
+
}
|
|
271
|
+
return { version: 1, sites: {} };
|
|
272
|
+
}
|
|
273
|
+
function writeLedger(path, ledger) {
|
|
274
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
275
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
276
|
+
writeFileSync(tmp, `${JSON.stringify(ledger)}\n`, { mode: 0o600 });
|
|
277
|
+
renameSync(tmp, path);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Serialize ledger read-modify-write across processes with an exclusive lock
|
|
281
|
+
* file. The lock is best-effort: a stale lock from a dead process is reclaimed,
|
|
282
|
+
* and a caller that cannot get the lock within `LOCK_WAIT_MS` proceeds without
|
|
283
|
+
* it rather than blocking a page load on a bookkeeping file.
|
|
284
|
+
*/
|
|
285
|
+
function withLedgerLock(path, now, fn) {
|
|
286
|
+
const lockPath = `${path}.lock`;
|
|
287
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
288
|
+
const start = now();
|
|
289
|
+
let held = false;
|
|
290
|
+
while (!held && now() - start < LOCK_WAIT_MS) {
|
|
291
|
+
try {
|
|
292
|
+
closeSync(openSync(lockPath, "wx", 0o600));
|
|
293
|
+
held = true;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
try {
|
|
297
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
|
|
298
|
+
rmSync(lockPath, { force: true });
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Lock vanished between the failed open and the stat; retry.
|
|
302
|
+
}
|
|
303
|
+
sleepSync(LOCK_POLL_MS);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
fn();
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
if (held)
|
|
311
|
+
rmSync(lockPath, { force: true });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -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>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headers the tunnel gate sends to the upstream after the Host rewrite.
|
|
3
|
+
*
|
|
4
|
+
* The gate replaces Host so PHP (and other vhost-routed servers) see the
|
|
5
|
+
* configured review host. Callers that compare Origin to Host then fail,
|
|
6
|
+
* because the browser still sends the public tunnel hostname. Copy that
|
|
7
|
+
* original Host into X-Forwarded-Host, overwriting any client-supplied
|
|
8
|
+
* value, so the upstream can recover the hostname the browser used.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyUpstreamHeaders(headers: Headers, vhost: string): void;
|
|
11
|
+
//# sourceMappingURL=forward-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forward-headers.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/forward-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAM1E"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headers the tunnel gate sends to the upstream after the Host rewrite.
|
|
3
|
+
*
|
|
4
|
+
* The gate replaces Host so PHP (and other vhost-routed servers) see the
|
|
5
|
+
* configured review host. Callers that compare Origin to Host then fail,
|
|
6
|
+
* because the browser still sends the public tunnel hostname. Copy that
|
|
7
|
+
* original Host into X-Forwarded-Host, overwriting any client-supplied
|
|
8
|
+
* value, so the upstream can recover the hostname the browser used.
|
|
9
|
+
*/
|
|
10
|
+
export function applyUpstreamHeaders(headers, vhost) {
|
|
11
|
+
const originalHost = headers.get("host") ?? "";
|
|
12
|
+
headers.set("host", vhost);
|
|
13
|
+
if (originalHost)
|
|
14
|
+
headers.set("x-forwarded-host", originalHost);
|
|
15
|
+
headers.set("accept-encoding", "identity");
|
|
16
|
+
headers.delete("connection");
|
|
17
|
+
}
|
package/dist/lib/tunnel/gate.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
// Reverse-proxy worker spawned by `tunnel up`. Listens on 127.0.0.1:<port>
|
|
2
2
|
// and forwards HTTP + WebSocket requests to an upstream with a Host header
|
|
3
|
-
// rewrite
|
|
4
|
-
// body but retains the encoding
|
|
5
|
-
// downstream). Cloudflare provider mode
|
|
6
|
-
// allowlist; trusted local proxy mode lets
|
|
3
|
+
// rewrite, the original Host copied to X-Forwarded-Host, and Content-Encoding
|
|
4
|
+
// stripped (Bun's fetch auto-decompresses the body but retains the encoding
|
|
5
|
+
// header, which breaks browser decoding downstream). Cloudflare provider mode
|
|
6
|
+
// checks CF-Connecting-IP against an allowlist; trusted local proxy mode lets
|
|
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.
|
|
7
10
|
//
|
|
8
11
|
// Runs detached, outside the CLI command framework; no command context is
|
|
9
12
|
// available; stdout/stderr is captured into .cache/tunnel/gate.log by the
|
|
10
13
|
// spawner.
|
|
11
14
|
import { renderTunnelErrorPage } from "./error-page";
|
|
15
|
+
import { applyUpstreamHeaders } from "./forward-headers";
|
|
16
|
+
import { ALLOW_PATHS_ENV, isPathAllowed, parseAllowPathsEnv } from "./path-scope.js";
|
|
12
17
|
// `--port`/`--name` are also passed on argv (not just env) so the gate's port
|
|
13
18
|
// and instance name show up in its process command line. That's what lets
|
|
14
19
|
// `tunnel down` scope its stray-process sweep to a single instance via
|
|
@@ -27,6 +32,7 @@ const TARGET = process.env.HARNERY_TUNNEL_TARGET ?? "127.0.0.1:8001";
|
|
|
27
32
|
const VHOST = process.env.HARNERY_TUNNEL_VHOST ?? "localhost";
|
|
28
33
|
const PORT = Number(process.env.HARNERY_TUNNEL_PORT ?? argvFlag("--port") ?? "9001");
|
|
29
34
|
const ACCESS = process.env.HARNERY_TUNNEL_ACCESS ?? "cloudflare-allowlist";
|
|
35
|
+
const ALLOW_PATHS = parseAllowPathsEnv(process.env[ALLOW_PATHS_ENV]);
|
|
30
36
|
const NAME = argvFlag("--name") ?? "default";
|
|
31
37
|
const UPSTREAM_HTTP = `http://${TARGET}`;
|
|
32
38
|
const UPSTREAM_WS = `ws://${TARGET}`;
|
|
@@ -77,16 +83,30 @@ const server = Bun.serve({
|
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
}
|
|
86
|
+
if (!isPathAllowed(url.pathname, ALLOW_PATHS)) {
|
|
87
|
+
const details = requestDetails(req, url);
|
|
88
|
+
console.log(
|
|
89
|
+
// lint-ok-emission: detached worker, see file note above
|
|
90
|
+
`path-deny: incident=${details.incidentId} ip=${details.clientIp || "(missing-cf-connecting-ip)"} ray=${details.cloudflareRay || "(missing)"} ${req.method} ${details.path}`);
|
|
91
|
+
return new Response(renderTunnelErrorPage({ ...details, kind: "path-denied" }), {
|
|
92
|
+
status: 403,
|
|
93
|
+
headers: {
|
|
94
|
+
"cache-control": "no-store",
|
|
95
|
+
"content-type": "text/html; charset=utf-8",
|
|
96
|
+
"x-harnery-tunnel-incident": details.incidentId,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
80
100
|
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
81
|
-
if (server.upgrade(req, {
|
|
101
|
+
if (server.upgrade(req, {
|
|
102
|
+
data: { path: url.pathname + url.search, originalHost: req.headers.get("host") ?? "" },
|
|
103
|
+
})) {
|
|
82
104
|
return undefined;
|
|
83
105
|
}
|
|
84
106
|
return new Response("WS upgrade failed\n", { status: 500 });
|
|
85
107
|
}
|
|
86
108
|
const headers = new Headers(req.headers);
|
|
87
|
-
headers
|
|
88
|
-
headers.set("accept-encoding", "identity");
|
|
89
|
-
headers.delete("connection");
|
|
109
|
+
applyUpstreamHeaders(headers, VHOST);
|
|
90
110
|
const init = {
|
|
91
111
|
method: req.method,
|
|
92
112
|
headers,
|
|
@@ -135,11 +155,14 @@ const server = Bun.serve({
|
|
|
135
155
|
},
|
|
136
156
|
websocket: {
|
|
137
157
|
open(ws) {
|
|
138
|
-
const { path } = ws.data;
|
|
158
|
+
const { path, originalHost } = ws.data;
|
|
139
159
|
// Bun's WebSocket constructor accepts a `headers` option at runtime
|
|
140
160
|
// even though the standard lib.dom.d.ts WebSocket type doesn't.
|
|
141
161
|
const upstream = new WebSocket(UPSTREAM_WS + path, {
|
|
142
|
-
headers: {
|
|
162
|
+
headers: {
|
|
163
|
+
host: VHOST,
|
|
164
|
+
...(originalHost ? { "x-forwarded-host": originalHost } : {}),
|
|
165
|
+
},
|
|
143
166
|
});
|
|
144
167
|
upstream.binaryType = "arraybuffer";
|
|
145
168
|
ws.upstream = upstream;
|
|
@@ -163,4 +186,5 @@ const server = Bun.serve({
|
|
|
163
186
|
// captured into .cache/tunnel/gate.log by the spawning command.
|
|
164
187
|
console.log(`harn-tunnel-gate :${server.port} -> ${UPSTREAM_HTTP} (Host: ${VHOST})`); // lint-ok-emission: detached worker, see file note above
|
|
165
188
|
console.log(`access: ${ACCESS}`); // lint-ok-emission: detached worker, see file note above
|
|
189
|
+
console.log(`paths: ${ALLOW_PATHS.join(", ") || "(empty, every path refused)"}`); // lint-ok-emission: detached worker, see file note above
|
|
166
190
|
console.log(`allow: ${[...ALLOW].join(", ") || "(empty, not used outside allowlist mode)"}`); // lint-ok-emission: detached worker, see file note above
|
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,8 @@ export interface TunnelState {
|
|
|
21
21
|
target: string;
|
|
22
22
|
vhost: string;
|
|
23
23
|
gate_port: number;
|
|
24
|
+
/** URL path prefixes the gate forwards; every other path is refused. */
|
|
25
|
+
allow_paths: string[];
|
|
24
26
|
tailscale_mode?: TailscaleMode;
|
|
25
27
|
tailscale_path?: string;
|
|
26
28
|
tailscale_https_port?: number;
|
|
@@ -39,6 +41,7 @@ export declare function isProcessAlive(pid: number): boolean;
|
|
|
39
41
|
export type ProcessAliveCheck = (pid: number) => boolean;
|
|
40
42
|
/** True when both the local gate and its provider-side process are live. */
|
|
41
43
|
export declare function isTunnelStateLive(state: TunnelState, processAlive?: ProcessAliveCheck): boolean;
|
|
44
|
+
export declare function tunnelTargetPort(state: TunnelState): number | null;
|
|
42
45
|
/**
|
|
43
46
|
* Newest live tunnel that serves `vhost` on the requested local web port.
|
|
44
47
|
*
|
|
@@ -51,6 +54,8 @@ export declare function isTunnelStateLive(state: TunnelState, processAlive?: Pro
|
|
|
51
54
|
* actually serves the caller's origin.
|
|
52
55
|
*/
|
|
53
56
|
export declare function findLiveTunnelForOrigin(port: number, vhost: string, root?: string, processAlive?: ProcessAliveCheck): TunnelState | null;
|
|
57
|
+
/** True when the tunnel's path scope forwards every one of `paths`. */
|
|
58
|
+
export declare function tunnelServesPaths(state: TunnelState, paths: readonly string[]): boolean;
|
|
54
59
|
/**
|
|
55
60
|
* Ensure cloudflared is on PATH or installed at ~/.local/bin/cloudflared.
|
|
56
61
|
* Auto-downloads on Linux; throws on other platforms with brew hint.
|
|
@@ -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":"AAyBA,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAW1C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,CAG9E;AAgBD,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,WAAW,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,CAAC,EAAE,aAAa,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAgBD,wBAAgB,UAAU,IAAI,YAAY,CAWzC;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAEnD;AAED,wBAAgB,SAAS,CACvB,IAAI,GAAE,MAAyB,EAC/B,IAAI,GAAE,MAAsB,GAC3B,WAAW,GAAG,IAAI,CAQpB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,GAAE,MAAsB,GAAG,IAAI,CAEjF;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAyB,EAAE,IAAI,GAAE,MAAsB,GAAG,IAAI,CAG9F;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAsB,GAAG,WAAW,EAAE,CAWtE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;AAEzD,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,YAAY,GAAE,iBAAkC,GAC/C,OAAO,CAKT;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAWlE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,MAAsB,EAC5B,YAAY,GAAE,iBAAkC,GAC/C,WAAW,GAAG,IAAI,CASpB;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAEvF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAuB1C"}
|
package/dist/lib/tunnel/state.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
+
import { isPathAllowed } from "./path-scope.js";
|
|
6
7
|
// Tunnel state lives under <root>/.cache/tunnel/. Root defaults to cwd for the
|
|
7
8
|
// command surface and can be supplied by callers that already resolved a repo.
|
|
8
9
|
function cachePath(tool, filename, root = process.cwd()) {
|
|
@@ -46,9 +47,18 @@ function nameFromStateFile(file) {
|
|
|
46
47
|
const DEFAULT_CONFIG = {
|
|
47
48
|
allowed_ips: [],
|
|
48
49
|
};
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Normalize a parsed state blob; supply `name` for pre-multi-instance files.
|
|
52
|
+
* A state written before path scopes existed reads as an empty scope, which
|
|
53
|
+
* the gate treats as "refuse every path".
|
|
54
|
+
*/
|
|
50
55
|
function normalizeState(raw, fallbackName) {
|
|
51
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
...raw,
|
|
58
|
+
name: raw.name ?? fallbackName,
|
|
59
|
+
provider: raw.provider ?? "cloudflare",
|
|
60
|
+
allow_paths: Array.isArray(raw.allow_paths) ? raw.allow_paths : [],
|
|
61
|
+
};
|
|
52
62
|
}
|
|
53
63
|
export function readConfig() {
|
|
54
64
|
const p = cachePath("tunnel", CONFIG_FILE);
|
|
@@ -122,7 +132,7 @@ export function isTunnelStateLive(state, processAlive = isProcessAlive) {
|
|
|
122
132
|
const providerPid = state.cloudflared_pid ?? state.provider_pid;
|
|
123
133
|
return typeof providerPid === "number" && processAlive(providerPid);
|
|
124
134
|
}
|
|
125
|
-
function tunnelTargetPort(state) {
|
|
135
|
+
export function tunnelTargetPort(state) {
|
|
126
136
|
const value = /^[a-z][a-z\d+.-]*:\/\//i.test(state.target)
|
|
127
137
|
? state.target
|
|
128
138
|
: `http://${state.target}`;
|
|
@@ -152,6 +162,10 @@ export function findLiveTunnelForOrigin(port, vhost, root = process.cwd(), proce
|
|
|
152
162
|
state.vhost === vhost &&
|
|
153
163
|
isTunnelStateLive(state, processAlive)) ?? null);
|
|
154
164
|
}
|
|
165
|
+
/** True when the tunnel's path scope forwards every one of `paths`. */
|
|
166
|
+
export function tunnelServesPaths(state, paths) {
|
|
167
|
+
return paths.every((path) => isPathAllowed(path, state.allow_paths));
|
|
168
|
+
}
|
|
155
169
|
/**
|
|
156
170
|
* Ensure cloudflared is on PATH or installed at ~/.local/bin/cloudflared.
|
|
157
171
|
* Auto-downloads on Linux; throws on other platforms with brew hint.
|