@logictan/dsh-config-manager 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件私有出站代理(issue #30)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:Node 内置 `fetch()`(undici)**默认不读** `HTTP_PROXY` / `HTTPS_PROXY`,因此「必须经代理
|
|
5
|
+
* 访问 GitHub」的网络下,GitHub 登录(device flow)与市场 API 全部失败(`fetch failed`),
|
|
6
|
+
* 而浏览器与 git 都正常。全局开关(`NODE_USE_ENV_PROXY` / `http.setGlobalProxyFromEnv()`)虽然
|
|
7
|
+
* 有效,但那是**进程级**的:它会连带改变宿主自身(含模型 API)的出站行为 —— 一个备份插件不该
|
|
8
|
+
* 有这种越界副作用(且该 API 需 Node ≥ 24.14,覆盖不全)。
|
|
9
|
+
*
|
|
10
|
+
* 本模块只服务本插件自己的出站请求,**不改动任何全局状态**,且**零第三方依赖**:
|
|
11
|
+
* - `http:` 目标经 HTTP 代理 → 使用 absolute-form 请求(`GET http://host/path HTTP/1.1`);
|
|
12
|
+
* - `https:` 目标经 HTTP 代理 → 自行完成 `CONNECT` 隧道,再在该 socket 上叠加 TLS(SNI 保留);
|
|
13
|
+
* - 命中 `NO_PROXY` 或未配置代理 → 完全直连,行为与未启用前一致。
|
|
14
|
+
*
|
|
15
|
+
* 与全局开关的分工:插件侧默认直连(不改变现状);仅当检测到代理环境变量时才启用代理路由。
|
|
16
|
+
* 如需在已配置代理的环境下刻意直连,设置 `DSH_CONFIG_MANAGER_PROXY=off`。
|
|
17
|
+
*
|
|
18
|
+
* 安全:代理 URL 的 userinfo(账号/密码)只用于 `Proxy-Authorization` 头,绝不进日志;
|
|
19
|
+
* `describeProxy()` 输出的代理描述已剥离凭据;跨源重定向剥离 `authorization`(GitHub token
|
|
20
|
+
* 绝不转发给第三方域名)。
|
|
21
|
+
*/
|
|
22
|
+
import http from 'node:http';
|
|
23
|
+
import https from 'node:https';
|
|
24
|
+
import net from 'node:net';
|
|
25
|
+
import tls from 'node:tls';
|
|
26
|
+
/** 环境变量开关:`DSH_CONFIG_MANAGER_PROXY=off|none|0` 时即使配了代理也强制直连(调试/特殊情况)。 */
|
|
27
|
+
const PROXY_OFF_VALUES = new Set(['off', 'none', '0', 'false', 'disable', 'disabled']);
|
|
28
|
+
/** 隧道建立与单次请求的默认上限(避免代理不可达时无限挂起)。 */
|
|
29
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
|
|
30
|
+
/** 视为可跟随的重定向状态码(与 WebDAV 通道同一套语义) */
|
|
31
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
32
|
+
/** 不能带响应体的状态码(`new Response(body, {status})` 会抛错) */
|
|
33
|
+
const BODYLESS_STATUSES = new Set([101, 204, 205, 304]);
|
|
34
|
+
/** 取第一个非空值(环境变量大小写与平台差异的归一) */
|
|
35
|
+
function firstNonEmpty(...values) {
|
|
36
|
+
for (const v of values) {
|
|
37
|
+
if (typeof v === 'string' && v.trim() !== '')
|
|
38
|
+
return v.trim();
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** 读代理环境变量(纯函数,便于测试注入任意 env) */
|
|
43
|
+
export function readProxyEnv(env = process.env) {
|
|
44
|
+
const off = firstNonEmpty(env['DSH_CONFIG_MANAGER_PROXY']);
|
|
45
|
+
if (off !== null && PROXY_OFF_VALUES.has(off.toLowerCase())) {
|
|
46
|
+
return { httpProxy: null, httpsProxy: null, noProxy: null };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
httpProxy: firstNonEmpty(env['HTTP_PROXY'], env['http_proxy']),
|
|
50
|
+
httpsProxy: firstNonEmpty(env['HTTPS_PROXY'], env['https_proxy']),
|
|
51
|
+
noProxy: firstNonEmpty(env['NO_PROXY'], env['no_proxy']),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** 是否配置了任何代理(用于启动日志与「是否需要走代理链路」的判定) */
|
|
55
|
+
export function isProxyConfigured(env = readProxyEnv()) {
|
|
56
|
+
return env.httpProxy !== null || env.httpsProxy !== null;
|
|
57
|
+
}
|
|
58
|
+
/** `host[:port]` 切分(兼容 IPv6 方括号形态);无端口 → null */
|
|
59
|
+
function splitHostPort(entry) {
|
|
60
|
+
if (entry.startsWith('[')) {
|
|
61
|
+
const end = entry.indexOf(']');
|
|
62
|
+
if (end !== -1) {
|
|
63
|
+
const host = entry.slice(0, end + 1);
|
|
64
|
+
const rest = entry.slice(end + 1);
|
|
65
|
+
return rest.startsWith(':') ? { host, port: rest.slice(1) } : { host, port: null };
|
|
66
|
+
}
|
|
67
|
+
return { host: entry, port: null };
|
|
68
|
+
}
|
|
69
|
+
const at = entry.lastIndexOf(':');
|
|
70
|
+
// 仅当冒号后全是数字才视作端口(避免把裸 IPv6 误切)
|
|
71
|
+
if (at !== -1 && /^\d+$/.test(entry.slice(at + 1))) {
|
|
72
|
+
return { host: entry.slice(0, at), port: entry.slice(at + 1) };
|
|
73
|
+
}
|
|
74
|
+
return { host: entry, port: null };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 目标是否命中 `NO_PROXY`。
|
|
78
|
+
* 支持:`*`(全直连)、精确主机、`.example.com` 与 `example.com`(含子域)、可选 `:port` 限定。
|
|
79
|
+
*/
|
|
80
|
+
export function matchesNoProxy(host, port, noProxy) {
|
|
81
|
+
if (noProxy === null)
|
|
82
|
+
return false;
|
|
83
|
+
const target = host.toLowerCase();
|
|
84
|
+
for (const raw of noProxy.split(',')) {
|
|
85
|
+
const entry = raw.trim().toLowerCase();
|
|
86
|
+
if (entry === '')
|
|
87
|
+
continue;
|
|
88
|
+
if (entry === '*')
|
|
89
|
+
return true;
|
|
90
|
+
const { host: entryHost, port: entryPort } = splitHostPort(entry);
|
|
91
|
+
if (entryPort !== null && entryPort !== port)
|
|
92
|
+
continue;
|
|
93
|
+
const bare = entryHost.startsWith('.') ? entryHost.slice(1) : entryHost;
|
|
94
|
+
if (bare === '')
|
|
95
|
+
continue;
|
|
96
|
+
if (target === bare || target.endsWith(`.${bare}`))
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/** URL 的有效端口(缺省按协议补) */
|
|
102
|
+
function effectivePort(url) {
|
|
103
|
+
if (url.port !== '')
|
|
104
|
+
return url.port;
|
|
105
|
+
return url.protocol === 'https:' ? '443' : '80';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 该目标应使用的代理 URL;无需代理(未配置 / 命中 NO_PROXY / 非法)→ null。
|
|
109
|
+
*
|
|
110
|
+
* 选择规则(对齐 undici `EnvHttpProxyAgent` 的直觉语义):
|
|
111
|
+
* - `https:` 目标 → `HTTPS_PROXY`,缺省回退 `HTTP_PROXY`;
|
|
112
|
+
* - `http:` 目标 → `HTTP_PROXY`,缺省回退 `HTTPS_PROXY`。
|
|
113
|
+
*/
|
|
114
|
+
export function proxyUrlFor(target, env = readProxyEnv()) {
|
|
115
|
+
let url;
|
|
116
|
+
try {
|
|
117
|
+
url = typeof target === 'string' ? new URL(target) : target;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
123
|
+
return null;
|
|
124
|
+
if (matchesNoProxy(url.hostname, effectivePort(url), env.noProxy))
|
|
125
|
+
return null;
|
|
126
|
+
const candidate = url.protocol === 'https:'
|
|
127
|
+
? (env.httpsProxy ?? env.httpProxy)
|
|
128
|
+
: (env.httpProxy ?? env.httpsProxy);
|
|
129
|
+
if (candidate === null)
|
|
130
|
+
return null;
|
|
131
|
+
try {
|
|
132
|
+
const proxy = new URL(candidate.includes('://') ? candidate : `http://${candidate}`);
|
|
133
|
+
if (proxy.hostname === '')
|
|
134
|
+
return null;
|
|
135
|
+
if (proxy.protocol !== 'http:' && proxy.protocol !== 'https:')
|
|
136
|
+
return null;
|
|
137
|
+
return proxy.toString();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** 代理的**脱敏**描述(供日志/报错提示;凭据一律剥离,绝不外泄) */
|
|
144
|
+
export function describeProxy(proxyUrl) {
|
|
145
|
+
try {
|
|
146
|
+
const u = new URL(proxyUrl);
|
|
147
|
+
return `${u.protocol}//${u.hostname}${u.port !== '' ? `:${u.port}` : ''}`;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return '(invalid proxy)';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** 一次性 socket 的 agent(隧道已建立;keepAlive 关闭,用完即毁) */
|
|
154
|
+
class TunnelAgent extends https.Agent {
|
|
155
|
+
tunnel;
|
|
156
|
+
constructor(tunnel) {
|
|
157
|
+
super({ keepAlive: false, maxSockets: 1 });
|
|
158
|
+
this.tunnel = tunnel;
|
|
159
|
+
}
|
|
160
|
+
createConnection() {
|
|
161
|
+
return this.tunnel;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** 建立到代理的连接(代理本身可能是 https) */
|
|
165
|
+
function connectToProxy(proxy, timeoutMs) {
|
|
166
|
+
const port = Number(proxy.port !== '' ? proxy.port : (proxy.protocol === 'https:' ? 443 : 80));
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const onConnect = () => { socket.setTimeout(0); resolve(socket); };
|
|
169
|
+
const socket = proxy.protocol === 'https:'
|
|
170
|
+
? tls.connect({ host: proxy.hostname, port, servername: proxy.hostname }, onConnect)
|
|
171
|
+
: net.connect({ host: proxy.hostname, port }, onConnect);
|
|
172
|
+
socket.setTimeout(timeoutMs, () => {
|
|
173
|
+
socket.destroy();
|
|
174
|
+
reject(new Error(`代理连接超时 / proxy connect timed out (${timeoutMs}ms)`));
|
|
175
|
+
});
|
|
176
|
+
socket.once('error', (err) => { reject(err); });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 经 HTTP 代理建立到目标的隧道(`CONNECT host:port`)。
|
|
181
|
+
* 返回的 socket 已与目标建立字节通道;https 目标再由调用方叠加 TLS。
|
|
182
|
+
*/
|
|
183
|
+
async function openTunnel(target, proxy, timeoutMs) {
|
|
184
|
+
const socket = await connectToProxy(proxy, timeoutMs);
|
|
185
|
+
const targetPort = effectivePort(target);
|
|
186
|
+
const authority = `${target.hostname}:${targetPort}`;
|
|
187
|
+
const lines = [
|
|
188
|
+
`CONNECT ${authority} HTTP/1.1`,
|
|
189
|
+
`Host: ${authority}`,
|
|
190
|
+
// 代理凭据只在此处使用,绝不写日志
|
|
191
|
+
...(proxy.username !== '' || proxy.password !== ''
|
|
192
|
+
? [`Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString('base64')}`]
|
|
193
|
+
: []),
|
|
194
|
+
'Proxy-Connection: keep-alive',
|
|
195
|
+
'',
|
|
196
|
+
'',
|
|
197
|
+
];
|
|
198
|
+
return await new Promise((resolve, reject) => {
|
|
199
|
+
let buffered = '';
|
|
200
|
+
const fail = (message) => {
|
|
201
|
+
socket.destroy();
|
|
202
|
+
reject(new Error(message));
|
|
203
|
+
};
|
|
204
|
+
const onData = (chunk) => {
|
|
205
|
+
buffered += chunk.toString('latin1');
|
|
206
|
+
const end = buffered.indexOf('\r\n\r\n');
|
|
207
|
+
if (end === -1) {
|
|
208
|
+
if (buffered.length > 8192) {
|
|
209
|
+
socket.off('data', onData);
|
|
210
|
+
fail('代理 CONNECT 响应异常(头部过长)/ malformed CONNECT response');
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
socket.off('data', onData);
|
|
215
|
+
const head = buffered.slice(0, end);
|
|
216
|
+
const status = Number((head.split('\r\n')[0] ?? '').split(' ')[1]);
|
|
217
|
+
if (status !== 200) {
|
|
218
|
+
fail(`代理拒绝 CONNECT(HTTP ${status})/ proxy refused CONNECT`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// CONNECT 响应头之后的字节已属隧道数据 → 回填,避免 TLS 握手丢包
|
|
222
|
+
const rest = Buffer.from(buffered.slice(end + 4), 'latin1');
|
|
223
|
+
if (rest.length > 0)
|
|
224
|
+
socket.unshift(rest);
|
|
225
|
+
resolve(socket);
|
|
226
|
+
};
|
|
227
|
+
socket.on('data', onData);
|
|
228
|
+
socket.once('error', (err) => { socket.off('data', onData); reject(err); });
|
|
229
|
+
socket.once('close', () => { socket.off('data', onData); reject(new Error('代理连接在 CONNECT 完成前关闭 / proxy closed during CONNECT')); });
|
|
230
|
+
socket.write(lines.join('\r\n'));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/** 请求结果归一到 node 的 IncomingMessage(直连与经代理两条路径共用收尾逻辑) */
|
|
234
|
+
function collectResponse(res, timeoutMs) {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
const chunks = [];
|
|
237
|
+
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
238
|
+
res.on('end', () => {
|
|
239
|
+
const headers = new Headers();
|
|
240
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
241
|
+
if (value === undefined)
|
|
242
|
+
continue;
|
|
243
|
+
if (Array.isArray(value))
|
|
244
|
+
for (const v of value)
|
|
245
|
+
headers.append(key, v);
|
|
246
|
+
else
|
|
247
|
+
headers.set(key, String(value));
|
|
248
|
+
}
|
|
249
|
+
resolve({
|
|
250
|
+
status: res.statusCode ?? 0,
|
|
251
|
+
statusText: res.statusMessage ?? '',
|
|
252
|
+
headers,
|
|
253
|
+
body: Buffer.concat(chunks),
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
res.on('error', reject);
|
|
257
|
+
if (timeoutMs > 0) {
|
|
258
|
+
res.setTimeout(timeoutMs, () => {
|
|
259
|
+
res.destroy();
|
|
260
|
+
reject(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 发起一次 HTTP/1.1 请求(自动:直连 / 经代理 absolute-form / 经代理 CONNECT 隧道)。
|
|
267
|
+
* 不跟随重定向(跟随策略由调用方决定:WebDAV 保方法语义,fetch 按浏览器语义)。
|
|
268
|
+
*/
|
|
269
|
+
export async function requestOnce(options) {
|
|
270
|
+
const target = new URL(options.url);
|
|
271
|
+
const env = options.env ?? readProxyEnv();
|
|
272
|
+
const proxyUrl = proxyUrlFor(target, env);
|
|
273
|
+
const method = (options.method ?? 'GET').toUpperCase();
|
|
274
|
+
const timeoutMs = options.timeoutMs ?? 0;
|
|
275
|
+
const payload = options.body === undefined
|
|
276
|
+
? undefined
|
|
277
|
+
: Buffer.isBuffer(options.body) ? options.body : Buffer.from(options.body, 'utf8');
|
|
278
|
+
const headers = {
|
|
279
|
+
...(payload !== undefined ? { 'content-length': String(payload.length) } : {}),
|
|
280
|
+
...(options.headers ?? {}),
|
|
281
|
+
};
|
|
282
|
+
/** url 非 null → 以 URL 形态发起(node 自行解析 host/port/path/TLS);否则按显式 options 发起。 */
|
|
283
|
+
const send = (lib, url, requestOptions) => new Promise((resolve, reject) => {
|
|
284
|
+
const onResponse = (res) => {
|
|
285
|
+
collectResponse(res, timeoutMs).then(resolve, reject);
|
|
286
|
+
};
|
|
287
|
+
const req = url === null
|
|
288
|
+
? lib.request(requestOptions, onResponse)
|
|
289
|
+
: lib.request(url, requestOptions, onResponse);
|
|
290
|
+
if (timeoutMs > 0) {
|
|
291
|
+
req.setTimeout(timeoutMs, () => {
|
|
292
|
+
req.destroy();
|
|
293
|
+
reject(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
req.on('error', reject);
|
|
297
|
+
if (options.signal !== undefined) {
|
|
298
|
+
const onAbort = () => { req.destroy(new Error('The operation was aborted.')); };
|
|
299
|
+
if (options.signal.aborted)
|
|
300
|
+
onAbort();
|
|
301
|
+
else
|
|
302
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
303
|
+
}
|
|
304
|
+
if (payload !== undefined)
|
|
305
|
+
req.write(payload);
|
|
306
|
+
req.end();
|
|
307
|
+
});
|
|
308
|
+
// ---- 未经代理:直连(默认路径,行为与此前一致)----
|
|
309
|
+
if (proxyUrl === null) {
|
|
310
|
+
const lib = target.protocol === 'https:' ? https : http;
|
|
311
|
+
return await send(lib, options.url, { method, headers });
|
|
312
|
+
}
|
|
313
|
+
const proxy = new URL(proxyUrl);
|
|
314
|
+
// ---- http 目标经代理:absolute-form 直接把完整 URL 交给代理 ----
|
|
315
|
+
if (target.protocol === 'http:') {
|
|
316
|
+
const port = Number(proxy.port !== '' ? proxy.port : (proxy.protocol === 'https:' ? 443 : 80));
|
|
317
|
+
const lib = proxy.protocol === 'https:' ? https : http;
|
|
318
|
+
return await send(lib, null, {
|
|
319
|
+
method,
|
|
320
|
+
host: proxy.hostname,
|
|
321
|
+
port,
|
|
322
|
+
path: target.toString(),
|
|
323
|
+
headers: {
|
|
324
|
+
...headers,
|
|
325
|
+
host: target.host,
|
|
326
|
+
...(proxy.username !== '' || proxy.password !== ''
|
|
327
|
+
? { 'proxy-authorization': `Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString('base64')}` }
|
|
328
|
+
: {}),
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// ---- https 目标经代理:CONNECT 隧道 + TLS(SNI 保留目标主机名)----
|
|
333
|
+
const tunnel = await openTunnel(target, proxy, timeoutMs > 0 ? timeoutMs : DEFAULT_CONNECT_TIMEOUT_MS);
|
|
334
|
+
const secure = tls.connect({
|
|
335
|
+
socket: tunnel,
|
|
336
|
+
servername: target.hostname,
|
|
337
|
+
// 与直连一致:默认校验证书链;不做任何降级
|
|
338
|
+
});
|
|
339
|
+
await new Promise((resolve, reject) => {
|
|
340
|
+
secure.once('secureConnect', () => { resolve(); });
|
|
341
|
+
secure.once('error', reject);
|
|
342
|
+
});
|
|
343
|
+
return await send(https, null, {
|
|
344
|
+
method,
|
|
345
|
+
headers,
|
|
346
|
+
host: target.hostname,
|
|
347
|
+
port: Number(effectivePort(target)),
|
|
348
|
+
path: `${target.pathname}${target.search}`,
|
|
349
|
+
agent: new TunnelAgent(secure),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/** 把 `HeadersInit` 归一为小写键的普通对象 */
|
|
353
|
+
function normalizeHeaders(init) {
|
|
354
|
+
const out = {};
|
|
355
|
+
if (init === undefined)
|
|
356
|
+
return out;
|
|
357
|
+
if (init instanceof Headers) {
|
|
358
|
+
init.forEach((value, key) => { out[key.toLowerCase()] = value; });
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
if (Array.isArray(init)) {
|
|
362
|
+
for (const [key, value] of init)
|
|
363
|
+
out[String(key).toLowerCase()] = String(value);
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
for (const [key, value] of Object.entries(init)) {
|
|
367
|
+
if (value !== undefined)
|
|
368
|
+
out[key.toLowerCase()] = String(value);
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
/** 同源判断(协议 + host,含端口) */
|
|
373
|
+
function sameOrigin(a, b) {
|
|
374
|
+
try {
|
|
375
|
+
const ua = new URL(a);
|
|
376
|
+
const ub = new URL(b);
|
|
377
|
+
return ua.protocol === ub.protocol && ua.host === ub.host;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** 重定向上限(与 WebDAV 通道一致) */
|
|
384
|
+
const MAX_REDIRECTS = 5;
|
|
385
|
+
/**
|
|
386
|
+
* 代理感知的 fetch 兼容实现(仅插件自身使用;替代 `globalThis.fetch` 的默认值)。
|
|
387
|
+
*
|
|
388
|
+
* 与浏览器 fetch 的关键语义对齐:自动跟随 301/302/303/307/308(上限 5 跳);303 且非 GET/HEAD
|
|
389
|
+
* 降级为 GET 并丢弃请求体;**跨源跳转剥离 `authorization`**(token 不转发给第三方域名);
|
|
390
|
+
* 无响应体状态(204/304 等)不构造 body。
|
|
391
|
+
*/
|
|
392
|
+
export function createProxyAwareFetch(env = readProxyEnv()) {
|
|
393
|
+
const impl = async (input, init) => {
|
|
394
|
+
const url = typeof input === 'string'
|
|
395
|
+
? input
|
|
396
|
+
: input instanceof URL ? input.toString() : input.url;
|
|
397
|
+
let currentUrl = url;
|
|
398
|
+
let method = (init?.method ?? 'GET').toUpperCase();
|
|
399
|
+
let headers = normalizeHeaders(init?.headers);
|
|
400
|
+
let body = init?.body === undefined || init?.body === null
|
|
401
|
+
? undefined
|
|
402
|
+
: String(init.body);
|
|
403
|
+
for (let hop = 0;; hop += 1) {
|
|
404
|
+
const res = await requestOnce({
|
|
405
|
+
method,
|
|
406
|
+
url: currentUrl,
|
|
407
|
+
headers,
|
|
408
|
+
...(body !== undefined ? { body } : {}),
|
|
409
|
+
...(init?.signal != null ? { signal: init.signal } : {}),
|
|
410
|
+
env,
|
|
411
|
+
});
|
|
412
|
+
const location = res.headers.get('location');
|
|
413
|
+
if (!REDIRECT_STATUSES.has(res.status) || location === null) {
|
|
414
|
+
return new Response(BODYLESS_STATUSES.has(res.status) ? null : new Uint8Array(res.body), { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
415
|
+
}
|
|
416
|
+
if (hop >= MAX_REDIRECTS) {
|
|
417
|
+
throw new TypeError(`fetch failed: too many redirects (${MAX_REDIRECTS} max)`);
|
|
418
|
+
}
|
|
419
|
+
let nextUrl;
|
|
420
|
+
try {
|
|
421
|
+
nextUrl = new URL(location, currentUrl).toString();
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return new Response(BODYLESS_STATUSES.has(res.status) ? null : new Uint8Array(res.body), { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
425
|
+
}
|
|
426
|
+
if (!sameOrigin(currentUrl, nextUrl))
|
|
427
|
+
delete headers['authorization'];
|
|
428
|
+
if (res.status === 303 && method !== 'GET' && method !== 'HEAD') {
|
|
429
|
+
method = 'GET';
|
|
430
|
+
body = undefined;
|
|
431
|
+
delete headers['content-length'];
|
|
432
|
+
}
|
|
433
|
+
currentUrl = nextUrl;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
return impl;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* 默认出站 fetcher:**未配置代理时原样返回全局 `fetch`**(保持既有行为零变化);
|
|
440
|
+
* 配置了代理才切到代理感知实现。
|
|
441
|
+
*/
|
|
442
|
+
export function defaultFetcher(env = readProxyEnv()) {
|
|
443
|
+
return isProxyConfigured(env) ? createProxyAwareFetch(env) : globalThis.fetch;
|
|
444
|
+
}
|
|
445
|
+
/** 启动日志用:当前生效的代理描述(已脱敏),未启用 → null */
|
|
446
|
+
export function activeProxySummary(env = readProxyEnv()) {
|
|
447
|
+
if (!isProxyConfigured(env))
|
|
448
|
+
return null;
|
|
449
|
+
return {
|
|
450
|
+
http: env.httpProxy !== null ? describeProxy(env.httpProxy) : null,
|
|
451
|
+
https: env.httpsProxy !== null ? describeProxy(env.httpsProxy) : null,
|
|
452
|
+
noProxyEntries: env.noProxy !== null ? env.noProxy.split(',').filter((s) => s.trim() !== '').length : 0,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** 遍历中跳过的链接(备份必须能说明「哪些内容没进来」) */
|
|
2
|
+
export interface SkippedLink {
|
|
3
|
+
/** homeDir 相对路径(链接本身,不是目标) */
|
|
4
|
+
path: string;
|
|
5
|
+
/** loop=指向已遍历目录(自引用/重复);outside-home=目标越出 homeDir;
|
|
6
|
+
* broken=断链;unreadable=目标既非文件也非目录(设备/FIFO 等);too-deep=超过深度上限 */
|
|
7
|
+
reason: 'loop' | 'outside-home' | 'broken' | 'unreadable' | 'too-deep';
|
|
8
|
+
}
|
|
9
|
+
export interface RecursiveListing {
|
|
10
|
+
/** homeDir 相对、斜杠分隔、已排序的文件路径 */
|
|
11
|
+
paths: string[];
|
|
12
|
+
/** 被跳过的链接(含原因);空 = 本次遍历无内容缺失 */
|
|
13
|
+
skippedLinks: SkippedLink[];
|
|
14
|
+
/** 成功跟随(内容已收集)的链接数;用于「链接结构不会被还原」的提示 */
|
|
15
|
+
followedLinks: number;
|
|
16
|
+
/** 读取失败的目录(ACL / 竞态删除等)——这些目录下的内容同样没进备份,必须留痕 */
|
|
17
|
+
unreadableDirs: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 遍历 baseDir 下的所有文件(跟随目录链接)。
|
|
21
|
+
* @param baseDir 绝对路径;不在 homeDir 内则返回空清单(与既有语义一致)
|
|
22
|
+
* @param homeDir 绝对路径;产出相对它,跟随边界也是它
|
|
23
|
+
*/
|
|
24
|
+
export declare function listRecursiveFollowingLinks(baseDir: string, homeDir: string): Promise<RecursiveListing>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 递归遍历(跟随 junction / 符号链接)—— 文件类分区(skills / agentPresets / sessions / pluginFiles)
|
|
3
|
+
* 的收集内核(issue #37)。
|
|
4
|
+
*
|
|
5
|
+
* 背景:`fs.readdir(withFileTypes)` 对 **目录 junction / 符号链接** 返回的是
|
|
6
|
+
* `isSymbolicLink() === true`、`isDirectory() === false`。此前调用方只处理
|
|
7
|
+
* `isDirectory() / isFile()` 两个分支,于是链接目录及其**全部真实内容**被静默排除,
|
|
8
|
+
* 备份照样报成功(issue #37:Windows 下 8 个链接目录约 12 MB 内容丢失,且无任何提示)。
|
|
9
|
+
*
|
|
10
|
+
* 本模块的语义:
|
|
11
|
+
* - **跟随**目录链接(内容进备份),并用 realpath 去重防环(自引用 / 重复链接 / 深度爆炸);
|
|
12
|
+
* - 链接目标**越出 homeDir** 或**损坏**时跳过,但**记入 skippedLinks** —— 让「没进备份」
|
|
13
|
+
* 变成备份报告里可见的一行,而不是无声缺失;
|
|
14
|
+
* - 产出与 `FileSystemFacade.listRecursive` 一致的 **homeDir 相对** 斜杠路径,且已排序。
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs/promises';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
/** 深度上限:防御病态目录树 / realpath 去重失效时的栈爆炸 */
|
|
19
|
+
const MAX_DEPTH = 64;
|
|
20
|
+
function normalizeSlashes(p) {
|
|
21
|
+
return p.split(path.sep).join('/');
|
|
22
|
+
}
|
|
23
|
+
/** current 是否等于 base 或位于其下(win32 折叠大小写) */
|
|
24
|
+
function isSameOrChild(current, base) {
|
|
25
|
+
const c = path.resolve(current);
|
|
26
|
+
const b = path.resolve(base);
|
|
27
|
+
const cl = process.platform === 'win32' ? c.toLowerCase() : c;
|
|
28
|
+
const bl = process.platform === 'win32' ? b.toLowerCase() : b;
|
|
29
|
+
return cl === bl || cl.startsWith(bl.endsWith(path.sep) ? bl : bl + path.sep);
|
|
30
|
+
}
|
|
31
|
+
async function realpathSafe(p) {
|
|
32
|
+
try {
|
|
33
|
+
return await fs.realpath(p);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 遍历 baseDir 下的所有文件(跟随目录链接)。
|
|
41
|
+
* @param baseDir 绝对路径;不在 homeDir 内则返回空清单(与既有语义一致)
|
|
42
|
+
* @param homeDir 绝对路径;产出相对它,跟随边界也是它
|
|
43
|
+
*/
|
|
44
|
+
export async function listRecursiveFollowingLinks(baseDir, homeDir) {
|
|
45
|
+
const base = path.resolve(baseDir);
|
|
46
|
+
const home = path.resolve(homeDir);
|
|
47
|
+
const paths = [];
|
|
48
|
+
const skippedLinks = [];
|
|
49
|
+
const unreadableDirs = [];
|
|
50
|
+
let followedLinks = 0;
|
|
51
|
+
/** 已进入过的目录 realpath(防环 / 防同一目标被两条链接重复收集) */
|
|
52
|
+
const visited = new Set();
|
|
53
|
+
const rel = (abs) => normalizeSlashes(path.relative(home, abs));
|
|
54
|
+
/** @returns 是否真的进入了该目录(false = 被边界/去重/IO 挡下) */
|
|
55
|
+
const walk = async (dir, depth, viaLink) => {
|
|
56
|
+
if (!isSameOrChild(dir, home))
|
|
57
|
+
return false;
|
|
58
|
+
if (depth > MAX_DEPTH) {
|
|
59
|
+
if (viaLink !== null)
|
|
60
|
+
skippedLinks.push({ path: viaLink, reason: 'too-deep' });
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const real = await realpathSafe(dir);
|
|
64
|
+
// 去重只针对**链接**:链接目标已经进过(自引用 / 两条链接指向同一处)→ 跳过。
|
|
65
|
+
// 普通目录永远遍历(否则「真实目录 + 指向它的链接」会有一边被静默吞掉)。
|
|
66
|
+
if (viaLink !== null && real !== null && visited.has(real)) {
|
|
67
|
+
skippedLinks.push({ path: viaLink, reason: 'loop' });
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (real !== null)
|
|
71
|
+
visited.add(real);
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
// 排序:让「链接 vs 真实目录」的遍历顺序与去重结果可复现(readdir 顺序依 FS 而异)
|
|
75
|
+
entries = (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// 目录不可读(ACL / 竞态删除):不中断整次遍历,但**必须留痕** ——
|
|
79
|
+
// 静默吞掉同样会造成「备份成功但内容缺失」(issue #37 的同类症状)。
|
|
80
|
+
// 根目录不存在(ENOENT)例外:调用方语义是「目录不存在视为空」,不告警。
|
|
81
|
+
if (viaLink !== null)
|
|
82
|
+
skippedLinks.push({ path: viaLink, reason: 'unreadable' });
|
|
83
|
+
else if (err?.code !== 'ENOENT')
|
|
84
|
+
unreadableDirs.push(rel(dir));
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const abs = path.join(dir, entry.name);
|
|
89
|
+
const entryRel = rel(abs);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
await walk(abs, depth + 1, null);
|
|
92
|
+
}
|
|
93
|
+
else if (entry.isFile()) {
|
|
94
|
+
paths.push(entryRel);
|
|
95
|
+
}
|
|
96
|
+
else if (entry.isSymbolicLink()) {
|
|
97
|
+
// 目录 junction / 符号链接:跟随(issue #37 的核心修复)。
|
|
98
|
+
// 注意:Dirent 对链接一律 isDirectory()===false,必须显式 stat 目标才知道类型。
|
|
99
|
+
let st;
|
|
100
|
+
try {
|
|
101
|
+
st = await fs.stat(abs); // 跟随链接
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
skippedLinks.push({ path: entryRel, reason: 'broken' });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// 链接目标必须落在 homeDir 内——**文件链接同样如此**。
|
|
108
|
+
// 只对目录链接做这个检查会漏掉「skills/link.md → ~/外部文件」:
|
|
109
|
+
// 内容会被读进备份(T2-P3 实测就是这种文件链接,属真实越界读取)。
|
|
110
|
+
const target = await realpathSafe(abs);
|
|
111
|
+
if (target === null || !isSameOrChild(target, home)) {
|
|
112
|
+
skippedLinks.push({ path: entryRel, reason: 'outside-home' });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (st.isDirectory()) {
|
|
116
|
+
if (await walk(abs, depth + 1, entryRel))
|
|
117
|
+
followedLinks += 1;
|
|
118
|
+
}
|
|
119
|
+
else if (st.isFile()) {
|
|
120
|
+
paths.push(entryRel);
|
|
121
|
+
followedLinks += 1;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
skippedLinks.push({ path: entryRel, reason: 'unreadable' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 其余类型(socket / FIFO / 设备):与既有实现一致,静默忽略
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
};
|
|
131
|
+
await walk(base, 0, null);
|
|
132
|
+
return { paths: paths.sort(), skippedLinks, followedLinks, unreadableDirs };
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=recursive-walk.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface ZipEntryMeta {
|
|
2
|
+
name: string;
|
|
3
|
+
method: number;
|
|
4
|
+
compressedSize: number;
|
|
5
|
+
uncompressedSize: number;
|
|
6
|
+
crc32: number;
|
|
7
|
+
isDirectory: boolean;
|
|
8
|
+
/** 本地文件头在 ZIP 字节流中的偏移 */
|
|
9
|
+
localOffset: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ZipSafetyLimits {
|
|
12
|
+
maxEntries?: number;
|
|
13
|
+
maxTotalBytes?: number;
|
|
14
|
+
maxCompressedBytes?: number;
|
|
15
|
+
maxSingleBytes?: number;
|
|
16
|
+
maxRatio?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const DEFAULT_ZIP_SAFETY_LIMITS: Required<ZipSafetyLimits>;
|
|
19
|
+
export declare class ZipSafetyError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
export declare function crc32(data: Uint8Array): number;
|
|
23
|
+
export interface ZipWriteEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
data: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
/** 组装 ZIP 字节(全部条目 method=8 deflate,UTF-8 文件名,含中央目录与 EOCD) */
|
|
28
|
+
export declare function zipToBuffer(entries: ZipWriteEntry[]): Uint8Array;
|
|
29
|
+
/** 写出 ZIP 文件(原子写,避免半写 zip) */
|
|
30
|
+
export declare function writeZip(zipPath: string, entries: ZipWriteEntry[]): Promise<void>;
|
|
31
|
+
/** 内存 ZIP 归档:构造时校验全部条目名与上限;读取时逐条解压 + CRC/尺寸/比预算校验 */
|
|
32
|
+
export declare class ZipArchive {
|
|
33
|
+
private readonly buf;
|
|
34
|
+
private readonly metas;
|
|
35
|
+
readonly limits: Required<ZipSafetyLimits>;
|
|
36
|
+
private totalUncompressed;
|
|
37
|
+
constructor(buf: Uint8Array, metas: ZipEntryMeta[], limits: Required<ZipSafetyLimits>);
|
|
38
|
+
names(): string[];
|
|
39
|
+
entries(): ZipEntryMeta[];
|
|
40
|
+
has(name: string): boolean;
|
|
41
|
+
/** 读取并解压单条目(带 CRC32 / 尺寸 / 体积预算 / 压缩比校验) */
|
|
42
|
+
readEntry(name: string): Uint8Array;
|
|
43
|
+
readEntryText(name: string): string;
|
|
44
|
+
/** 读取并深度保护解析 JSON 条目 */
|
|
45
|
+
readEntryJson(name: string): unknown;
|
|
46
|
+
}
|
|
47
|
+
/** 解析 ZIP(含安全校验:条目名 / 条目数 / 压缩体积) */
|
|
48
|
+
export declare function parseZip(buf: Uint8Array, limits?: ZipSafetyLimits): ZipArchive;
|
|
49
|
+
/**
|
|
50
|
+
* 安全解压到受控目录(对齐设计 §13.4 safeExtract 签名)。
|
|
51
|
+
* 返回条目相对路径列表;任何越界/损坏即整体拒绝(不部分落盘)。
|
|
52
|
+
* m4 的 zip-security 可替换/强化本实现。
|
|
53
|
+
*/
|
|
54
|
+
export declare function safeExtract(zipPath: string, destDir: string, limits?: ZipSafetyLimits): Promise<string[]>;
|