talos-code 0.0.1 → 0.2.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/CHANGELOG.md +87 -0
- package/LICENSE +661 -0
- package/README.md +116 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +100 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +244 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +46 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +92 -0
- package/dist/i18n/en/errors.js +267 -0
- package/dist/i18n/en/firstrun.js +101 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +774 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +331 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +265 -0
- package/dist/runtime/talos-composition.js +1011 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +4098 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +560 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +38 -0
- package/dist/tui/components/terminal-shell.js +214 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +148 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +56 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +43 -0
- package/dist/tui/overlays/approval-dialog.js +642 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +77 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +171 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +289 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +791 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +82 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +27 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +64 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-proxy-universale.mjs — M3 (07/09/2026): la CORSIA VELOCE del Browser.
|
|
3
|
+
* Fa vedere dentro TALOS anche le pagine che vietano la cornice (github risponde
|
|
4
|
+
* `X-Frame-Options: DENY` e nella cornice resta un rettangolo grigio), senza
|
|
5
|
+
* dover avviare un browser vero. Il browser vero (la corsia B, CDP) resta per
|
|
6
|
+
* quello che il proxy non può dare: accesso già fatto, pagine che si difendono
|
|
7
|
+
* davvero, JavaScript che parla solo col suo stesso server.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ PERCHÉ UN'ORIGINE SEPARATA, E NON LA NOSTRA — è il punto di tutto il file.
|
|
10
|
+
* Una pagina servita da noi diventa, per il browser, CODICE DELLA NOSTRA
|
|
11
|
+
* ORIGINE: la same-origin policy le apre `localStorage`, `IndexedDB`, i cookie e
|
|
12
|
+
* ogni rotta `/api/*` di TALOS come se fossero roba sua (MDN «Same-origin
|
|
13
|
+
* policy», letto il 07/09/2026: l'origine è schema+host+PORTA, e ciò che
|
|
14
|
+
* condivide l'origine condivide lo storage). Un proxy che serve siti di terzi
|
|
15
|
+
* dalla propria origine mette per giunta tutti i siti proxati nello STESSO
|
|
16
|
+
* barattolo: lo script di un sito legge quello che ci ha lasciato un altro
|
|
17
|
+
* (proxyorb.com, «Same-Origin Policy and Web Proxies», letto il 07/09/2026). E
|
|
18
|
+
* la pagina di terzi è SEMPRE contenuto non affidabile — l'indirect prompt
|
|
19
|
+
* injection su Comet dimostrata da Brave.
|
|
20
|
+
* ⇒ Il proxy vive su una PORTA SUA (origine diversa) e non sa niente di TALOS.
|
|
21
|
+
*
|
|
22
|
+
* ⛔ La porta separa l'origine, NON i cookie. I cookie si scelgono per HOST, la
|
|
23
|
+
* porta non entra nel confronto: una pagina servita da `127.0.0.1:4301` sta nello
|
|
24
|
+
* stesso barattolo di TALOS su `127.0.0.1:4174` — e `talos_token`
|
|
25
|
+
* (`src/http-app.mjs:1193`, HttpOnly SameSite=Strict) resta illeggibile da JS ma
|
|
26
|
+
* VIENE SPEDITO, perché per SameSite due porte dello stesso host sono lo stesso
|
|
27
|
+
* sito. Per questo il proxy si lega di default a un OSPITE diverso, `127.0.0.2`:
|
|
28
|
+
* host diverso ⇒ barattolo diverso ⇒ da lì non parte nessun cookie di TALOS.
|
|
29
|
+
* Dove quell'indirizzo non si può legare (macOS vuole un alias esplicito su
|
|
30
|
+
* `lo0`) si ripiega su `127.0.0.1` e lo si DICHIARA (`cookieCondiviso: true`),
|
|
31
|
+
* invece di fingere un isolamento che non c'è.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ SSRF: il proxy va a prendere pagine per conto di chi chiede, quindi è la
|
|
34
|
+
* classica leva verso l'interno. Indirizzi privati, loopback, link-local (dentro
|
|
35
|
+
* cui sta `169.254.169.254`, le credenziali IAM di una macchina cloud) e
|
|
36
|
+
* multicast si rifiutano, in IPv4 e in IPv6 — comprese le forme mascherate
|
|
37
|
+
* `[::ffff:169.254.169.254]`, che alcune librerie Node lasciano passare
|
|
38
|
+
* (appsecbrief.com «SSRF in 2026» e vulnsy.com «SSRF Cheat Sheet 2026», letti il
|
|
39
|
+
* 07/09/2026). Il controllo si ripete a OGNI salto di reindirizzamento, perché il
|
|
40
|
+
* primo indirizzo può essere pubblico e il secondo no. Resta scoperto il DNS
|
|
41
|
+
* rebinding (un nome pubblico che risolve a 10.x): la cura vera è guardare
|
|
42
|
+
* l'indirizzo del socket, ed è lavoro della corsia B.
|
|
43
|
+
*
|
|
44
|
+
* ⛔ La CSP si RISCRIVE, non si butta. Via `X-Frame-Options` (intero: dice solo
|
|
45
|
+
* chi può incorniciare) e via la SOLA direttiva `frame-ancestors`; `script-src`,
|
|
46
|
+
* `object-src` e le altre restano in piedi. Buttare tutta la CSP toglierebbe a
|
|
47
|
+
* una pagina non affidabile ogni difesa contro i suoi stessi XSS proprio mentre
|
|
48
|
+
* la ospitiamo noi (OWASP «Clickjacking Defense Cheat Sheet» e usamaejaz.com
|
|
49
|
+
* «Securely bypassing X-Frame-Options», letti il 07/09/2026: si modifica la
|
|
50
|
+
* direttiva, non si cancella l'intestazione).
|
|
51
|
+
*
|
|
52
|
+
* ⛔ `integrity` non si tocca. Con `<base>` sull'origine vera una sottorisorsa
|
|
53
|
+
* che prima era same-origin diventa cross-origin: e una risorsa cross-origin con
|
|
54
|
+
* `integrity` DEVE viaggiare in CORS, cioè vuole `crossorigin` nel markup,
|
|
55
|
+
* altrimenti il browser riceve una risposta opaca, non può calcolare l'impronta e
|
|
56
|
+
* blocca lo script (MDN «Subresource Integrity», letto il 07/09/2026). Quindi si
|
|
57
|
+
* AGGIUNGE `crossorigin="anonymous"` dove manca, e non si toglie mai l'impronta:
|
|
58
|
+
* toglierla spegnerebbe in silenzio una difesa sulla catena di fornitura. Per lo
|
|
59
|
+
* stesso motivo il proxy passa SOLO il documento: se riscrivessimo anche le
|
|
60
|
+
* sottorisorse, ogni byte cambiato sarebbe un'impronta che non torna.
|
|
61
|
+
*/
|
|
62
|
+
import http from 'node:http';
|
|
63
|
+
import { randomBytes } from 'node:crypto';
|
|
64
|
+
|
|
65
|
+
import { urlAmmesso, MILLISECONDI_MASSIMI } from './browser-frame.mjs';
|
|
66
|
+
|
|
67
|
+
export const BYTE_MASSIMI = 5 * 1024 * 1024;
|
|
68
|
+
export const SALTI_MASSIMI = 5;
|
|
69
|
+
export const OSPITE_SEPARATO = '127.0.0.2';
|
|
70
|
+
export const OSPITE_RIPIEGO = '127.0.0.1';
|
|
71
|
+
const AGENTE = 'TALOS-Harness-Desktop/0.1 (proxy universale)';
|
|
72
|
+
|
|
73
|
+
/* Mai inoltrate al browser. `set-cookie`: i cookie del sito non entrano nel
|
|
74
|
+
* barattolo del proxy. `strict-transport-security` e `clear-site-data`
|
|
75
|
+
* parlerebbero dell'origine NOSTRA. `cross-origin-resource-policy` e i due
|
|
76
|
+
* `cross-origin-*-policy` bloccherebbero proprio la cornice che stiamo
|
|
77
|
+
* costruendo. `content-encoding`/`content-length`/gli hop-by-hop descrivono un
|
|
78
|
+
* corpo che abbiamo già decodificato e riscritto. `x-frame-options` è il motivo
|
|
79
|
+
* per cui questo file esiste. */
|
|
80
|
+
export const INTESTAZIONI_MAI_INOLTRATE = [
|
|
81
|
+
'set-cookie', 'set-cookie2', 'strict-transport-security', 'clear-site-data', 'public-key-pins',
|
|
82
|
+
'cross-origin-resource-policy', 'cross-origin-opener-policy', 'cross-origin-embedder-policy',
|
|
83
|
+
'content-encoding', 'content-length', 'transfer-encoding', 'connection', 'keep-alive', 'upgrade',
|
|
84
|
+
'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'x-frame-options',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const NOMI_SONDATI = ['content-type', 'content-security-policy', 'content-security-policy-report-only', 'x-frame-options', 'set-cookie', 'cache-control', 'content-length', 'content-encoding', 'strict-transport-security', 'cross-origin-resource-policy', 'cross-origin-opener-policy', 'cross-origin-embedder-policy', 'x-content-type-options', 'location'];
|
|
88
|
+
|
|
89
|
+
/* Lo script che entra nella pagina proxata. Vive sull'origine del PROXY — mai su
|
|
90
|
+
* quella di TALOS: una richiesta verso 4174 partirebbe col cookie di sessione. */
|
|
91
|
+
export const SCRIPT_OVERLAY_PREDEFINITO = `(() => {
|
|
92
|
+
const dove = (document.currentScript && document.currentScript.dataset.talosUrl) || location.href;
|
|
93
|
+
const manda = (tipo, dati) => { try { parent.postMessage(Object.assign({ talos: 'browser-proxy', tipo, url: dove }, dati), '*'); } catch { /* il genitore puo' essere andato via */ } };
|
|
94
|
+
addEventListener('error', (e) => manda('errore', { messaggio: String((e && e.message) || 'errore'), riga: (e && e.lineno) || null }), true);
|
|
95
|
+
addEventListener('unhandledrejection', (e) => manda('errore', { messaggio: String((e && e.reason && e.reason.message) || (e && e.reason) || 'promessa rifiutata') }));
|
|
96
|
+
addEventListener('DOMContentLoaded', () => manda('pronta', { titolo: document.title || null }));
|
|
97
|
+
})();`;
|
|
98
|
+
|
|
99
|
+
/* ─────────────── gli indirizzi che il proxy non tocca ─────────────── */
|
|
100
|
+
|
|
101
|
+
function gruppiIpv6(host) {
|
|
102
|
+
const testo = String(host).replace(/^\[|\]$/g, '');
|
|
103
|
+
if (!testo.includes(':')) return null;
|
|
104
|
+
let corpo = testo;
|
|
105
|
+
const conV4 = /^(.*:)((?:\d{1,3}\.){3}\d{1,3})$/.exec(testo);
|
|
106
|
+
if (conV4) {
|
|
107
|
+
const ottetti = conV4[2].split('.').map(Number);
|
|
108
|
+
if (ottetti.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
109
|
+
corpo = `${conV4[1]}${((ottetti[0] << 8) | ottetti[1]).toString(16)}:${((ottetti[2] << 8) | ottetti[3]).toString(16)}`;
|
|
110
|
+
}
|
|
111
|
+
const parti = corpo.split('::');
|
|
112
|
+
if (parti.length > 2) return null;
|
|
113
|
+
const sinistra = parti[0] ? parti[0].split(':').filter(Boolean) : [];
|
|
114
|
+
const destra = parti.length === 2 ? (parti[1] ? parti[1].split(':').filter(Boolean) : []) : null;
|
|
115
|
+
const buchi = destra === null ? 0 : 8 - sinistra.length - destra.length;
|
|
116
|
+
if (buchi < 0) return null;
|
|
117
|
+
const pezzi = destra === null ? sinistra : [...sinistra, ...Array(buchi).fill('0'), ...destra];
|
|
118
|
+
if (pezzi.length !== 8) return null;
|
|
119
|
+
const gruppi = pezzi.map((p) => (/^[0-9a-f]{1,4}$/i.test(p) ? parseInt(p, 16) : NaN));
|
|
120
|
+
return gruppi.some((n) => Number.isNaN(n)) ? null : gruppi;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ottettiIpv4(host) {
|
|
124
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(String(host));
|
|
125
|
+
if (!m) return null;
|
|
126
|
+
const o = m.slice(1).map(Number);
|
|
127
|
+
return o.some((n) => n > 255) ? null : o;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function motivoIpv4(ottetti) {
|
|
131
|
+
const [a, b, c] = ottetti;
|
|
132
|
+
if (a === 127) return 'Indirizzo di loopback (127.0.0.0/8)';
|
|
133
|
+
if (a === 0) return 'Indirizzo «questa rete» (0.0.0.0/8)';
|
|
134
|
+
if (a === 10) return 'Indirizzo di rete privata (10.0.0.0/8)';
|
|
135
|
+
if (a === 172 && b >= 16 && b <= 31) return 'Indirizzo di rete privata (172.16.0.0/12)';
|
|
136
|
+
if (a === 192 && b === 168) return 'Indirizzo di rete privata (192.168.0.0/16)';
|
|
137
|
+
if (a === 169 && b === 254) return 'Indirizzo link-local (169.254.0.0/16: è lì che risponde il servizio metadati di una macchina cloud)';
|
|
138
|
+
if (a === 100 && b >= 64 && b <= 127) return 'Indirizzo condiviso dell\'operatore (100.64.0.0/10)';
|
|
139
|
+
if (a === 192 && b === 0 && c === 0) return 'Indirizzo riservato IETF (192.0.0.0/24)';
|
|
140
|
+
if (a === 198 && (b === 18 || b === 19)) return 'Indirizzo per prove di rete (198.18.0.0/15)';
|
|
141
|
+
if (a >= 224) return 'Indirizzo multicast o riservato (224.0.0.0/4 e oltre)';
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Dice PERCHÉ un host non si può proxare, o `null` se è un indirizzo pubblico.
|
|
147
|
+
* @param {string} host l'hostname come l'ha normalizzato `new URL` (IPv6 fra parentesi)
|
|
148
|
+
* @returns {string|null}
|
|
149
|
+
*/
|
|
150
|
+
export function indirizzoLocale(host) {
|
|
151
|
+
const nudo = String(host || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
152
|
+
if (!nudo) return 'Indirizzo senza host';
|
|
153
|
+
if (nudo === 'localhost' || nudo.endsWith('.localhost') || nudo.endsWith('.local') || nudo.endsWith('.internal') || nudo.endsWith('.home.arpa')) {
|
|
154
|
+
return 'Nome che punta al tuo computer o alla tua rete di casa';
|
|
155
|
+
}
|
|
156
|
+
const v4 = ottettiIpv4(nudo);
|
|
157
|
+
if (v4) return motivoIpv4(v4);
|
|
158
|
+
const g = gruppiIpv6(nudo);
|
|
159
|
+
if (g) {
|
|
160
|
+
if (g.every((n) => n === 0)) return 'Indirizzo non specificato (::)';
|
|
161
|
+
if (g.slice(0, 7).every((n) => n === 0) && g[7] === 1) return 'Indirizzo di loopback (::1)';
|
|
162
|
+
/* IPv4 travestito da IPv6: `::ffff:169.254.169.254` è lo stesso bersaglio, scritto come molti controlli non lo riconoscono */
|
|
163
|
+
if (g.slice(0, 5).every((n) => n === 0) && g[5] === 0xffff) return motivoIpv4([g[6] >> 8, g[6] & 0xff, g[7] >> 8, g[7] & 0xff]);
|
|
164
|
+
if ((g[0] & 0xfe00) === 0xfc00) return 'Indirizzo IPv6 di rete locale (fc00::/7)';
|
|
165
|
+
if ((g[0] & 0xffc0) === 0xfe80) return 'Indirizzo IPv6 link-local (fe80::/10)';
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Chi il proxy può andare a prendere. Riusa `urlAmmesso` di `browser-frame.mjs`
|
|
172
|
+
* (solo http/https, mai credenziali nell'indirizzo) e ne tiene la FORMA
|
|
173
|
+
* `{ok:true} | {ok:false, motivo}`, aggiungendoci il filtro SSRF.
|
|
174
|
+
* `locale:true` distingue «non lo tocca il proxy» da «non lo tocca nessuno»: un
|
|
175
|
+
* dev server sul computer lo apre benissimo il browser vero, che gira lì.
|
|
176
|
+
* @param {URL} url
|
|
177
|
+
* @returns {{ok:true}|{ok:false, motivo:string, locale?:boolean}}
|
|
178
|
+
*/
|
|
179
|
+
export function bersaglioAmmesso(url) {
|
|
180
|
+
const base = urlAmmesso(url);
|
|
181
|
+
if (!base.ok) return base;
|
|
182
|
+
const motivo = indirizzoLocale(url.hostname);
|
|
183
|
+
if (motivo) return { ok: false, motivo, locale: true };
|
|
184
|
+
return { ok: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ─────────────── quale delle tre vie ─────────────── */
|
|
188
|
+
|
|
189
|
+
const SEGNI_NEL_PERCORSO = /(^|\/)(login|log-in|signin|sign-in|accedi|auth|oauth|oauth2|sso|session|account|accounts)(\/|$)/i;
|
|
190
|
+
const SEGNI_NEL_MOTIVO = /(401|403|login|accesso|autentic|credenzial|sessione|cookie|non autorizzat)/i;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Riconosce una pagina che senza cookie non mostrerebbe niente di utile. Il
|
|
194
|
+
* proxy non inoltra credenziali per costruzione, quindi lì farebbe vedere solo
|
|
195
|
+
* il muro dell'accesso: quella pagina è del browser vero, che ha un profilo suo.
|
|
196
|
+
* @param {{url?:string, motivo?:string|null}} dati
|
|
197
|
+
*/
|
|
198
|
+
export function dietroLogin({ url = '', motivo = null } = {}) {
|
|
199
|
+
if (motivo && SEGNI_NEL_MOTIVO.test(String(motivo))) return true;
|
|
200
|
+
let indirizzo;
|
|
201
|
+
try { indirizzo = new URL(String(url)); } catch { return false; }
|
|
202
|
+
if (SEGNI_NEL_PERCORSO.test(indirizzo.pathname)) return true;
|
|
203
|
+
return /^(accounts?|login|auth|signin|sso|my)\./i.test(indirizzo.hostname);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* La scelta della corsia. Regola d'oro: la via più economica che funziona.
|
|
208
|
+
* @param {{incorniciabile?:boolean, motivo?:string|null, url?:string, proxyDisponibile?:boolean, vivoDisponibile?:boolean}} dati
|
|
209
|
+
* @returns {{via:'cornice'|'proxy'|'vivo', perche:string}}
|
|
210
|
+
*/
|
|
211
|
+
export function decidiVia({ incorniciabile, motivo = null, url = '', proxyDisponibile = true, vivoDisponibile = false } = {}) {
|
|
212
|
+
if (incorniciabile === true) {
|
|
213
|
+
return { via: 'cornice', perche: 'La pagina si lascia incorniciare: è la via più economica — nessun proxy da attraversare, nessun browser da avviare' };
|
|
214
|
+
}
|
|
215
|
+
let bersaglio = null;
|
|
216
|
+
try { bersaglio = new URL(String(url)); } catch { bersaglio = null; }
|
|
217
|
+
const ammesso = bersaglio ? bersaglioAmmesso(bersaglio) : { ok: false, motivo: 'Indirizzo non valido' };
|
|
218
|
+
if (!ammesso.ok) {
|
|
219
|
+
if (ammesso.locale && vivoDisponibile) return { via: 'vivo', perche: `${ammesso.motivo}: il proxy non tocca gli indirizzi privati, ma il browser vero gira sul tuo computer e ci arriva` };
|
|
220
|
+
if (ammesso.locale) return { via: 'cornice', perche: `${ammesso.motivo}: il proxy non lo tocca e il browser vero non è disponibile` };
|
|
221
|
+
return { via: 'cornice', perche: `${ammesso.motivo}: nessuna corsia lo migliora` };
|
|
222
|
+
}
|
|
223
|
+
if (dietroLogin({ url, motivo })) {
|
|
224
|
+
if (vivoDisponibile) return { via: 'vivo', perche: 'Sembra una pagina dietro accesso: il proxy non inoltra i tuoi cookie, quindi mostrerebbe solo il modulo di accesso — il browser vero ha un profilo suo dove puoi entrare' };
|
|
225
|
+
if (proxyDisponibile) return { via: 'proxy', perche: 'Sembra dietro accesso e il browser vero non è disponibile: il proxy la mostrerà come la vede chi l\'accesso non l\'ha fatto' };
|
|
226
|
+
return { via: 'cornice', perche: 'Sembra dietro accesso e non c\'è né il proxy né il browser vero' };
|
|
227
|
+
}
|
|
228
|
+
if (proxyDisponibile) return { via: 'proxy', perche: `${motivo || 'La pagina vieta la cornice'}: la passiamo dal proxy, su un'origine separata dalla nostra` };
|
|
229
|
+
if (vivoDisponibile) return { via: 'vivo', perche: `${motivo || 'La pagina vieta la cornice'}: il proxy non è disponibile, la apre il browser vero` };
|
|
230
|
+
return { via: 'cornice', perche: `${motivo || 'La pagina vieta la cornice'}: senza proxy né browser vero la cornice resterà quasi certamente vuota` };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ─────────────── le intestazioni ─────────────── */
|
|
234
|
+
|
|
235
|
+
function coppieDa(intestazioni) {
|
|
236
|
+
if (!intestazioni) return [];
|
|
237
|
+
if (typeof intestazioni.entries === 'function') return [...intestazioni.entries()];
|
|
238
|
+
if (typeof intestazioni.get === 'function') return NOMI_SONDATI.map((n) => [n, intestazioni.get(n)]).filter(([, v]) => v != null && v !== '');
|
|
239
|
+
return Object.entries(intestazioni).filter(([, v]) => v != null && v !== '').map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Legge una singola intestazione da una qualunque delle tre forme (Headers, `{get}`, oggetto). */
|
|
243
|
+
export function prendiIntestazione(intestazioni, nome) {
|
|
244
|
+
const cercato = String(nome).toLowerCase();
|
|
245
|
+
const trovata = coppieDa(intestazioni).find(([k]) => String(k).toLowerCase() === cercato);
|
|
246
|
+
return trovata ? String(trovata[1]) : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Toglie la SOLA direttiva `frame-ancestors`, lasciando in piedi tutto il resto.
|
|
251
|
+
* Una intestazione CSP può portare più politiche separate da virgola: si
|
|
252
|
+
* trattano una per una, e nel browser continuano a intersecarsi.
|
|
253
|
+
* @returns {{csp:string, tolta:boolean}}
|
|
254
|
+
*/
|
|
255
|
+
export function senzaFrameAncestors(valore) {
|
|
256
|
+
const testo = String(valore || '');
|
|
257
|
+
let tolta = false;
|
|
258
|
+
const csp = testo.split(',').map((politica) => {
|
|
259
|
+
const direttive = politica.split(';').map((d) => d.trim()).filter(Boolean);
|
|
260
|
+
const restano = direttive.filter((d) => {
|
|
261
|
+
const eLei = /^frame-ancestors\b/i.test(d);
|
|
262
|
+
if (eLei) tolta = true;
|
|
263
|
+
return !eLei;
|
|
264
|
+
});
|
|
265
|
+
return restano.join('; ');
|
|
266
|
+
}).filter(Boolean).join(', ');
|
|
267
|
+
return { csp, tolta };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Aggiunge UNA origine esatta alle sorgenti di script, così l'overlay può
|
|
272
|
+
* partire anche dentro una pagina con una CSP severa. È un allargamento minimo e
|
|
273
|
+
* dichiarato — l'opposto di cancellare la politica.
|
|
274
|
+
* ⛔ Con `strict-dynamic` in politica il browser ignora le sorgenti per host: lì
|
|
275
|
+
* l'overlay non parte, e quella pagina è del browser vero.
|
|
276
|
+
*/
|
|
277
|
+
export function permettiScriptDa(csp, origine) {
|
|
278
|
+
const testo = String(csp || '').trim();
|
|
279
|
+
if (!testo) return testo;
|
|
280
|
+
let org = String(origine || '').trim();
|
|
281
|
+
try { org = new URL(org).origin; } catch { /* accettiamo anche un'origine già nuda */ }
|
|
282
|
+
if (!org) return testo;
|
|
283
|
+
return testo.split(',').map((politica) => {
|
|
284
|
+
const direttive = politica.split(';').map((d) => d.trim()).filter(Boolean);
|
|
285
|
+
let toccata = false;
|
|
286
|
+
for (let i = 0; i < direttive.length; i += 1) {
|
|
287
|
+
if (/^script-src(-elem)?\b/i.test(direttive[i])) {
|
|
288
|
+
if (!direttive[i].toLowerCase().includes(org.toLowerCase())) direttive[i] = `${direttive[i]} ${org}`;
|
|
289
|
+
toccata = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!toccata) {
|
|
293
|
+
const predefinita = direttive.find((d) => /^default-src\b/i.test(d));
|
|
294
|
+
/* `script-src` non eredita da `default-src` una volta che esiste: si copia il valore e ci si aggiunge la nostra origine, così nient'altro cambia */
|
|
295
|
+
if (predefinita) direttive.push(`script-src ${predefinita.replace(/^default-src\s*/i, '').trim()} ${org}`.trim());
|
|
296
|
+
}
|
|
297
|
+
return direttive.join('; ');
|
|
298
|
+
}).join(', ');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {Headers|{get:Function}|Record<string,string>} headers
|
|
303
|
+
* @returns {{tolte:string[], tenute:Record<string,string>}}
|
|
304
|
+
*/
|
|
305
|
+
export function intestazioniDaTogliere(headers) {
|
|
306
|
+
const tolte = [];
|
|
307
|
+
const tenute = {};
|
|
308
|
+
for (const [nome0, valore0] of coppieDa(headers)) {
|
|
309
|
+
const nome = String(nome0).toLowerCase();
|
|
310
|
+
const valore = Array.isArray(valore0) ? valore0.join(', ') : String(valore0);
|
|
311
|
+
if (INTESTAZIONI_MAI_INOLTRATE.includes(nome)) { tolte.push(nome); continue; }
|
|
312
|
+
if (nome === 'content-security-policy' || nome === 'content-security-policy-report-only') {
|
|
313
|
+
const { csp, tolta } = senzaFrameAncestors(valore);
|
|
314
|
+
if (tolta) tolte.push(`${nome}: frame-ancestors`);
|
|
315
|
+
if (csp) tenute[nome] = csp;
|
|
316
|
+
else if (tolta) tolte.push(nome); /* c'era solo frame-ancestors: non resta niente da mandare */
|
|
317
|
+
else tenute[nome] = valore;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
tenute[nome] = valore;
|
|
321
|
+
}
|
|
322
|
+
return { tolte, tenute };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* ─────────────── il documento ─────────────── */
|
|
326
|
+
|
|
327
|
+
const virgolette = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"');
|
|
328
|
+
|
|
329
|
+
/* ⛔ `String.replace` con una stringa mangia i `$` (lezione 02/09): qui i sostituti sono SEMPRE funzioni. */
|
|
330
|
+
function riscriviTagConIntegrity(html) {
|
|
331
|
+
return html.replace(/<(script|link)\b[^>]*>/gi, (tag) => {
|
|
332
|
+
if (!/\sintegrity\s*=/i.test(tag)) return tag;
|
|
333
|
+
if (/\scrossorigin(\s*=|[\s/>])/i.test(tag)) return tag;
|
|
334
|
+
return tag.replace(/\s*(\/?)>$/, (_fine, barra) => ` crossorigin="anonymous"${barra ? ' /' : ''}>`);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function riscriviMetaCsp(html, origineScript) {
|
|
339
|
+
return html.replace(/<meta\b[^>]*http-equiv\s*=\s*["']?content-security-policy["']?[^>]*>/gi, (tag) => tag.replace(/(content\s*=\s*)("([^"]*)"|'([^']*)')/i, (_intero, prefisso, _tutto, dop, sing) => {
|
|
340
|
+
const valore = dop !== undefined ? dop : (sing || '');
|
|
341
|
+
const nuovo = permettiScriptDa(senzaFrameAncestors(valore).csp, origineScript);
|
|
342
|
+
return `${prefisso}"${virgolette(nuovo)}"`;
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Prepara il documento per la cornice: `<base>` sull'origine vera (così le
|
|
348
|
+
* sottorisorse continuano a caricarsi dal sito, e nessun byte loro passa da noi
|
|
349
|
+
* a rompere le impronte `integrity`), l'overlay in testa, `frame-ancestors`
|
|
350
|
+
* fuori anche dal `<meta>` — dove per giunta il browser la ignora già (MDN «CSP
|
|
351
|
+
* frame-ancestors», letto il 07/09/2026: in un meta tag la direttiva non ha
|
|
352
|
+
* effetto, la decisione sulla cornice è già stata presa) — e `crossorigin`
|
|
353
|
+
* aggiunto dove c'è `integrity`.
|
|
354
|
+
* @param {string} html
|
|
355
|
+
* @param {{urlPagina:string, origineProxy?:string}} dati
|
|
356
|
+
*/
|
|
357
|
+
export function riscriviHtml(html, { urlPagina, origineProxy = '' } = {}) {
|
|
358
|
+
let s = String(html);
|
|
359
|
+
s = riscriviMetaCsp(s, origineProxy);
|
|
360
|
+
s = riscriviTagConIntegrity(s);
|
|
361
|
+
const base = /<base\b[^>]*href\s*=/i.test(s) ? '' : `<base href="${virgolette(urlPagina)}">`;
|
|
362
|
+
const overlay = origineProxy ? `<script src="${virgolette(String(origineProxy).replace(/\/+$/, ''))}/annota.js" data-talos-url="${virgolette(urlPagina)}"></script>` : '';
|
|
363
|
+
const iniezione = `${base}${overlay}`;
|
|
364
|
+
if (!iniezione) return s;
|
|
365
|
+
if (/<head\b[^>]*>/i.test(s)) return s.replace(/<head\b[^>]*>/i, (apertura) => `${apertura}${iniezione}`);
|
|
366
|
+
if (/<html\b[^>]*>/i.test(s)) return s.replace(/<html\b[^>]*>/i, (apertura) => `${apertura}<head>${iniezione}</head>`);
|
|
367
|
+
return `${iniezione}${s}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** L'indirizzo con cui la cornice chiede una pagina al proxy. Un bersaglio che il proxy non tocca non deve nemmeno avere un link: qui si alza un errore. */
|
|
371
|
+
export function urlProxato(origineProxy, url) {
|
|
372
|
+
const base = String(origineProxy || '').replace(/\/+$/, '');
|
|
373
|
+
if (!/^https?:\/\/[^/]+/i.test(base)) throw new Error('Origine del proxy non valida');
|
|
374
|
+
let bersaglio;
|
|
375
|
+
try { bersaglio = new URL(String(url)); } catch { throw new Error('Indirizzo non valido'); }
|
|
376
|
+
const ammesso = bersaglioAmmesso(bersaglio);
|
|
377
|
+
if (!ammesso.ok) throw new Error(ammesso.motivo);
|
|
378
|
+
return `${base}/vai?u=${encodeURIComponent(bersaglio.href)}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ─────────────── chi va a prendere la pagina ─────────────── */
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Il lettore predefinito. Segue i reindirizzamenti A MANO, un salto per volta,
|
|
385
|
+
* ricontrollando l'indirizzo a ogni salto: con `redirect: 'follow'` vedremmo solo
|
|
386
|
+
* la destinazione finale, e un 302 verso `169.254.169.254` sarebbe già partito.
|
|
387
|
+
* Niente cookie e niente credenziali: `fetch` di Node non ha un barattolo, e lo
|
|
388
|
+
* diciamo comunque con `credentials: 'omit'`.
|
|
389
|
+
*/
|
|
390
|
+
export async function leggiPagina(indirizzo, { fetchFn = globalThis.fetch, millisecondi = MILLISECONDI_MASSIMI, saltiMassimi = SALTI_MASSIMI, ammesso = bersaglioAmmesso } = {}) {
|
|
391
|
+
let url;
|
|
392
|
+
try { url = new URL(String(indirizzo)); } catch { return { ok: false, motivo: 'Indirizzo non valido' }; }
|
|
393
|
+
const controller = new AbortController();
|
|
394
|
+
const timer = setTimeout(() => controller.abort(), millisecondi);
|
|
395
|
+
try {
|
|
396
|
+
for (let salto = 0; salto <= saltiMassimi; salto += 1) {
|
|
397
|
+
const via = ammesso(url);
|
|
398
|
+
if (!via.ok) return { ok: false, motivo: via.motivo, locale: via.locale === true };
|
|
399
|
+
const risposta = await fetchFn(url.href, { method: 'GET', redirect: 'manual', credentials: 'omit', signal: controller.signal, headers: { accept: 'text/html,*/*;q=0.5', 'user-agent': AGENTE } });
|
|
400
|
+
const stato = Number(risposta.status || 0);
|
|
401
|
+
if (stato >= 300 && stato < 400) {
|
|
402
|
+
const dove = prendiIntestazione(risposta.headers, 'location');
|
|
403
|
+
try { await risposta.body?.cancel?.(); } catch { /* niente da liberare */ }
|
|
404
|
+
if (!dove) return { ok: false, motivo: `Reindirizzamento senza destinazione (${stato})` };
|
|
405
|
+
try { url = new URL(dove, url); } catch { return { ok: false, motivo: 'Reindirizzamento verso un indirizzo non valido' }; }
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const tipo = String(prendiIntestazione(risposta.headers, 'content-type') || '');
|
|
409
|
+
if (!/text\/html/i.test(tipo)) {
|
|
410
|
+
try { await risposta.body?.cancel?.(); } catch { /* niente da liberare */ }
|
|
411
|
+
return { ok: true, url: url.href, stato, intestazioni: risposta.headers, corpo: '', tipo };
|
|
412
|
+
}
|
|
413
|
+
const testo = await risposta.text();
|
|
414
|
+
if (testo.length > BYTE_MASSIMI) return { ok: false, motivo: 'La pagina supera i 5 MB' };
|
|
415
|
+
return { ok: true, url: url.href, stato, intestazioni: risposta.headers, corpo: testo, tipo };
|
|
416
|
+
}
|
|
417
|
+
return { ok: false, motivo: `Più di ${saltiMassimi} reindirizzamenti` };
|
|
418
|
+
} catch (errore) {
|
|
419
|
+
return { ok: false, motivo: errore?.name === 'AbortError' ? `Nessuna risposta entro ${Math.round(millisecondi / 1000)} secondi` : 'La pagina non risponde' };
|
|
420
|
+
} finally {
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* ─────────────── il server sulla porta sua ─────────────── */
|
|
426
|
+
|
|
427
|
+
async function ascolta(server, porta, ospite, ripiego) {
|
|
428
|
+
const prova = (indirizzo) => new Promise((risolvi, rifiuta) => {
|
|
429
|
+
const suErrore = (e) => { server.removeListener('listening', suAscolto); rifiuta(e); };
|
|
430
|
+
const suAscolto = () => { server.removeListener('error', suErrore); risolvi(indirizzo); };
|
|
431
|
+
server.once('error', suErrore);
|
|
432
|
+
server.once('listening', suAscolto);
|
|
433
|
+
server.listen(porta, indirizzo);
|
|
434
|
+
});
|
|
435
|
+
try {
|
|
436
|
+
return { ospite: await prova(ospite), cookieCondiviso: false };
|
|
437
|
+
} catch (errore) {
|
|
438
|
+
if (ospite === ripiego) throw errore;
|
|
439
|
+
/* macOS non lega 127.0.0.2 senza un alias su lo0: si ripiega, e lo si DICE a chi chiama */
|
|
440
|
+
return { ospite: await prova(ripiego), cookieCondiviso: true };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Accende il proxy su una porta sua. `leggi` è iniettato: i test non toccano la
|
|
446
|
+
* rete. La chiave nel percorso non cambia l'origine (quella la fa la porta): serve
|
|
447
|
+
* solo perché un altro programma sul computer non possa usarci come proxy aperto.
|
|
448
|
+
* @param {{porta?:number, leggi:Function, ammesso?:Function, ospite?:string, origineOspite?:string|null, scriptOverlay?:string, chiave?:string}} opzioni
|
|
449
|
+
* @returns {Promise<{porta:number, origine:string, ospite:string, cookieCondiviso:boolean, chiave:string, chiudi:()=>Promise<void>}>}
|
|
450
|
+
*/
|
|
451
|
+
export async function creaServerProxy({ porta = 0, leggi, ammesso = bersaglioAmmesso, ospite = OSPITE_SEPARATO, origineOspite = null, scriptOverlay = SCRIPT_OVERLAY_PREDEFINITO, chiave = randomBytes(16).toString('hex') } = {}) {
|
|
452
|
+
if (typeof leggi !== 'function') throw new TypeError('creaServerProxy vuole «leggi»: chi va a prendere la pagina');
|
|
453
|
+
const prefisso = `/s/${chiave}`;
|
|
454
|
+
let origine = '';
|
|
455
|
+
|
|
456
|
+
const server = http.createServer((req, res) => {
|
|
457
|
+
const rispondi = (stato, testo) => {
|
|
458
|
+
res.writeHead(stato, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
459
|
+
res.end(testo);
|
|
460
|
+
};
|
|
461
|
+
(async () => {
|
|
462
|
+
if (req.method !== 'GET') return rispondi(405, 'Il proxy risponde solo a GET');
|
|
463
|
+
let percorso;
|
|
464
|
+
try { percorso = new URL(req.url, 'http://proxy.invalido'); } catch { return rispondi(400, 'Richiesta non valida'); }
|
|
465
|
+
if (!percorso.pathname.startsWith(`${prefisso}/`)) return rispondi(404, 'Qui non c\'è niente');
|
|
466
|
+
const resto = percorso.pathname.slice(prefisso.length);
|
|
467
|
+
if (resto === '/annota.js') {
|
|
468
|
+
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' });
|
|
469
|
+
res.end(scriptOverlay);
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
if (resto !== '/vai') return rispondi(404, 'Qui non c\'è niente');
|
|
473
|
+
const chiesto = percorso.searchParams.get('u');
|
|
474
|
+
if (!chiesto) return rispondi(400, 'Manca l\'indirizzo da mostrare');
|
|
475
|
+
let bersaglio;
|
|
476
|
+
try { bersaglio = new URL(chiesto); } catch { return rispondi(400, 'Indirizzo non valido'); }
|
|
477
|
+
const via = ammesso(bersaglio);
|
|
478
|
+
if (!via.ok) return rispondi(403, via.motivo);
|
|
479
|
+
let pagina;
|
|
480
|
+
try { pagina = await leggi(bersaglio.href); } catch { pagina = { ok: false, motivo: 'La pagina non risponde' }; }
|
|
481
|
+
if (!pagina || pagina.ok === false) return rispondi(502, pagina?.motivo || 'La pagina non risponde');
|
|
482
|
+
const tipo = String(pagina.tipo || prendiIntestazione(pagina.intestazioni, 'content-type') || '');
|
|
483
|
+
if (!/text\/html/i.test(tipo)) return rispondi(415, 'Il proxy passa solo il documento HTML: script, stili e immagini si caricano dal sito vero grazie a <base>');
|
|
484
|
+
const { tolte, tenute } = intestazioniDaTogliere(pagina.intestazioni);
|
|
485
|
+
const html = riscriviHtml(String(pagina.corpo || ''), { urlPagina: pagina.url || bersaglio.href, origineProxy: origine });
|
|
486
|
+
const intestazioni = {
|
|
487
|
+
...tenute,
|
|
488
|
+
'content-type': 'text/html; charset=utf-8',
|
|
489
|
+
'cache-control': 'no-store',
|
|
490
|
+
'referrer-policy': 'no-referrer',
|
|
491
|
+
'x-talos-tolte': tolte.join(', ') || 'nessuna',
|
|
492
|
+
'x-talos-origine': pagina.url || bersaglio.href,
|
|
493
|
+
};
|
|
494
|
+
const delSito = tenute['content-security-policy'] ? permettiScriptDa(tenute['content-security-policy'], origine) : null;
|
|
495
|
+
/* due politiche CSP si INTERSECANO nel browser: quella del sito (senza frame-ancestors) resta in piedi, la nostra dice soltanto chi può incorniciare */
|
|
496
|
+
const nostra = origineOspite ? `frame-ancestors 'self' ${origineOspite}` : null;
|
|
497
|
+
if (delSito && nostra) intestazioni['content-security-policy'] = [delSito, nostra];
|
|
498
|
+
else if (delSito) intestazioni['content-security-policy'] = delSito;
|
|
499
|
+
else if (nostra) intestazioni['content-security-policy'] = nostra;
|
|
500
|
+
res.writeHead(200, intestazioni);
|
|
501
|
+
res.end(html);
|
|
502
|
+
return undefined;
|
|
503
|
+
})().catch(() => {
|
|
504
|
+
try { rispondi(500, 'Errore del proxy'); } catch { /* risposta già chiusa */ }
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const legato = await ascolta(server, porta, ospite, OSPITE_RIPIEGO);
|
|
509
|
+
const portaVera = server.address().port;
|
|
510
|
+
origine = `http://${legato.ospite}:${portaVera}${prefisso}`;
|
|
511
|
+
return {
|
|
512
|
+
porta: portaVera,
|
|
513
|
+
origine,
|
|
514
|
+
ospite: legato.ospite,
|
|
515
|
+
cookieCondiviso: legato.cookieCondiviso,
|
|
516
|
+
chiave,
|
|
517
|
+
chiudi: () => new Promise((risolvi) => { server.close(() => risolvi()); }),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-proxy.mjs — Il proxy locale (06/09) che rende una pagina di un
|
|
3
|
+
* dev server LOCALE «della nostra origine», così dentro la cornice si può iniettare l'overlay di
|
|
4
|
+
* annotazione e leggere il DOM (same-origin policy). Una webview Electron eviterebbe il problema;
|
|
5
|
+
* in un guscio browser la strada è il proxy, come VS Code
|
|
6
|
+
* Live Preview.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ SOLO bersagli locali (localhost, 127.0.0.1, ::1): una pagina passata dal proxy gira nella
|
|
9
|
+
* NOSTRA origine, quindi i suoi script vedono localStorage e cookie di TALOS. Per il dev server
|
|
10
|
+
* dello sviluppatore è il suo stesso codice; per un sito remoto sarebbe consegnargli TALOS.
|
|
11
|
+
* Un sito remoto resta nella cornice normale (altra origine, senza annotazione) o bloccato.
|
|
12
|
+
*
|
|
13
|
+
* Riscrittura (fonti lette il 06/09/2026: gist cprima «PHP proxy for iframe embedding», niutech
|
|
14
|
+
* x-frame-bypass, usamaejaz «bypassing X-Frame-Options»): via le intestazioni X-Frame-Options e
|
|
15
|
+
* Content-Security-Policy della pagina e il `<meta http-equiv="Content-Security-Policy">`;
|
|
16
|
+
* `<base href>` sull'origine vera, così script, stili e immagini relativi si caricano dal dev
|
|
17
|
+
* server; il nostro script in testa al `<head>` (cattura gli errori di console PRIMA degli script
|
|
18
|
+
* della pagina). Al documento proxato NON si applica la CSP di TALOS (bloccherebbe gli script del
|
|
19
|
+
* dev server): resta `frame-ancestors 'self'` — nessun altro può incorniciarlo.
|
|
20
|
+
*/
|
|
21
|
+
import { urlAmmesso, MILLISECONDI_MASSIMI, classificaGuasto } from './browser-frame.mjs'; // 16/09: il guasto si nomina in UN posto solo
|
|
22
|
+
|
|
23
|
+
export const BYTE_MASSIMI = 5 * 1024 * 1024;
|
|
24
|
+
export const SCRIPT_OVERLAY = '/talos/browser-annota.js';
|
|
25
|
+
|
|
26
|
+
/** Solo il computer dello sviluppatore: la pagina proxata gira nella nostra origine. */
|
|
27
|
+
export function bersaglioLocale(url) {
|
|
28
|
+
const host = String(url?.hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
29
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host.endsWith('.localhost');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Riscrive il documento HTML per la cornice: niente CSP in pagina, `<base>` sull'origine vera, il
|
|
34
|
+
* nostro script per primo nel `<head>`.
|
|
35
|
+
* @param {string} html
|
|
36
|
+
* @param {string} urlVero l'indirizzo della pagina (per `<base>` e per il segnale al genitore)
|
|
37
|
+
* @param {string} origineNostra l'origine di TALOS: con `<base>` sul dev server, `/talos/…` andrebbe a finire LÀ — lo script si carica con l'indirizzo assoluto nostro (trovato dal vivo il 06/09: overlay assente)
|
|
38
|
+
*/
|
|
39
|
+
export function riscriviHtml(html, urlVero, origineNostra = '') {
|
|
40
|
+
let s = String(html);
|
|
41
|
+
s = s.replace(/<meta[^>]+http-equiv=["']?content-security-policy["']?[^>]*>/gi, '');
|
|
42
|
+
const haBase = /<base\b[^>]*href=/i.test(s);
|
|
43
|
+
const iniezione = `${haBase ? '' : `<base href="${urlVero.replace(/"/g, '"')}">`}<script src="${origineNostra.replace(/\/$/, '')}${SCRIPT_OVERLAY}" data-talos-url="${urlVero.replace(/"/g, '"')}"></script>`;
|
|
44
|
+
if (/<head[^>]*>/i.test(s)) s = s.replace(/<head[^>]*>/i, (m) => `${m}${iniezione}`);
|
|
45
|
+
else if (/<html[^>]*>/i.test(s)) s = s.replace(/<html[^>]*>/i, (m) => `${m}<head>${iniezione}</head>`);
|
|
46
|
+
else s = `${iniezione}${s}`;
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} indirizzo
|
|
52
|
+
* @param {{fetchFn?:typeof fetch, millisecondi?:number}} deps
|
|
53
|
+
* @returns {Promise<{ok:true, html:string, url:string, stato:number}|{ok:false, codice:string, motivo:string, stato?:number}>}
|
|
54
|
+
*/
|
|
55
|
+
export async function proxyPagina(indirizzo, { fetchFn = globalThis.fetch, millisecondi = MILLISECONDI_MASSIMI, origineNostra = '' } = {}) {
|
|
56
|
+
let url;
|
|
57
|
+
try { url = new URL(String(indirizzo)); } catch { return { ok: false, codice: 'QUERY_INVALID', motivo: 'URL non valido' }; }
|
|
58
|
+
const ammesso = urlAmmesso(url);
|
|
59
|
+
if (!ammesso.ok) return { ok: false, codice: 'QUERY_INVALID', motivo: ammesso.motivo };
|
|
60
|
+
if (!bersaglioLocale(url)) return { ok: false, codice: 'BROWSER_PROXY_SOLO_LOCALE', motivo: 'Il proxy con annotazione vale solo per un dev server sul tuo computer (localhost, 127.0.0.1)' };
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), millisecondi);
|
|
63
|
+
try {
|
|
64
|
+
const risposta = await fetchFn(url.href, { method: 'GET', redirect: 'follow', signal: controller.signal, headers: { accept: 'text/html,*/*;q=0.5', 'user-agent': 'TALOS-Harness-Desktop/0.1 (proxy locale)' } });
|
|
65
|
+
const finale = (() => { try { return new URL(risposta.url || url.href); } catch { return url; } })();
|
|
66
|
+
if (!bersaglioLocale(finale)) { try { await risposta.body?.cancel?.(); } catch { /* niente */ } return { ok: false, codice: 'BROWSER_PROXY_SOLO_LOCALE', motivo: 'La pagina ha reindirizzato fuori dal tuo computer' }; }
|
|
67
|
+
const tipo = String(risposta.headers.get('content-type') || '');
|
|
68
|
+
if (!/text\/html/i.test(tipo)) { try { await risposta.body?.cancel?.(); } catch { /* niente */ } return { ok: false, codice: 'BROWSER_PROXY_NON_HTML', motivo: `Non è una pagina HTML (${tipo.split(';')[0] || 'tipo ignoto'})`, stato: risposta.status }; }
|
|
69
|
+
const lunghezza = Number(risposta.headers.get('content-length') || 0);
|
|
70
|
+
if (lunghezza > BYTE_MASSIMI) { try { await risposta.body?.cancel?.(); } catch { /* niente */ } return { ok: false, codice: 'BROWSER_PROXY_TROPPO_GRANDE', motivo: 'La pagina supera i 5 MB', stato: risposta.status }; }
|
|
71
|
+
const testo = await risposta.text();
|
|
72
|
+
if (testo.length > BYTE_MASSIMI) return { ok: false, codice: 'BROWSER_PROXY_TROPPO_GRANDE', motivo: 'La pagina supera i 5 MB', stato: risposta.status };
|
|
73
|
+
return { ok: true, html: riscriviHtml(testo, finale.href, origineNostra), url: finale.href, stato: risposta.status };
|
|
74
|
+
} catch (errore) {
|
|
75
|
+
/* ⛔ 16/09 — stessa cura di `browser-frame.mjs`, stessa tabella: su un dev server «non l'hai
|
|
76
|
+
acceso» e «ci ho messo troppo» sono due gesti diversi per chi programma, e prima uscivano
|
|
77
|
+
con la stessa frase. La classificazione è importata, non ricopiata. */
|
|
78
|
+
/* ⛔ 16/09, giro di riparazione — passano anche i `dettagli` (i secondi di un timeout): il
|
|
79
|
+
motivo qui è italiano e composto dal server, quindi chi disegna deve poter riscrivere la
|
|
80
|
+
frase nella lingua di chi guarda invece di provare a tradurre una chiave che contiene un
|
|
81
|
+
numero. Stessa cura del percorso della cornice, stesso contratto. */
|
|
82
|
+
const { genere, motivo, dettagli } = classificaGuasto(errore, { millisecondi });
|
|
83
|
+
return { ok: false, codice: 'BROWSER_PROXY_IRRAGGIUNGIBILE', motivo, genere, dettagli };
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|