@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,493 @@
|
|
|
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
|
+
|
|
27
|
+
/** 代理相关环境变量(大小写兼容后的归一形态) */
|
|
28
|
+
export interface ProxyEnv {
|
|
29
|
+
/** http 目标使用的代理(`HTTP_PROXY` / `http_proxy`) */
|
|
30
|
+
httpProxy: string | null
|
|
31
|
+
/** https 目标使用的代理(`HTTPS_PROXY` / `https_proxy`) */
|
|
32
|
+
httpsProxy: string | null
|
|
33
|
+
/** 直连白名单(`NO_PROXY` / `no_proxy`) */
|
|
34
|
+
noProxy: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 环境变量开关:`DSH_CONFIG_MANAGER_PROXY=off|none|0` 时即使配了代理也强制直连(调试/特殊情况)。 */
|
|
38
|
+
const PROXY_OFF_VALUES = new Set(['off', 'none', '0', 'false', 'disable', 'disabled'])
|
|
39
|
+
|
|
40
|
+
/** 隧道建立与单次请求的默认上限(避免代理不可达时无限挂起)。 */
|
|
41
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000
|
|
42
|
+
|
|
43
|
+
/** 视为可跟随的重定向状态码(与 WebDAV 通道同一套语义) */
|
|
44
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308])
|
|
45
|
+
|
|
46
|
+
/** 不能带响应体的状态码(`new Response(body, {status})` 会抛错) */
|
|
47
|
+
const BODYLESS_STATUSES = new Set([101, 204, 205, 304])
|
|
48
|
+
|
|
49
|
+
/** 取第一个非空值(环境变量大小写与平台差异的归一) */
|
|
50
|
+
function firstNonEmpty(...values: (string | undefined)[]): string | null {
|
|
51
|
+
for (const v of values) {
|
|
52
|
+
if (typeof v === 'string' && v.trim() !== '') return v.trim()
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 读代理环境变量(纯函数,便于测试注入任意 env) */
|
|
58
|
+
export function readProxyEnv(env: Record<string, string | undefined> = process.env): ProxyEnv {
|
|
59
|
+
const off = firstNonEmpty(env['DSH_CONFIG_MANAGER_PROXY'])
|
|
60
|
+
if (off !== null && PROXY_OFF_VALUES.has(off.toLowerCase())) {
|
|
61
|
+
return { httpProxy: null, httpsProxy: null, noProxy: null }
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
httpProxy: firstNonEmpty(env['HTTP_PROXY'], env['http_proxy']),
|
|
65
|
+
httpsProxy: firstNonEmpty(env['HTTPS_PROXY'], env['https_proxy']),
|
|
66
|
+
noProxy: firstNonEmpty(env['NO_PROXY'], env['no_proxy']),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 是否配置了任何代理(用于启动日志与「是否需要走代理链路」的判定) */
|
|
71
|
+
export function isProxyConfigured(env: ProxyEnv = readProxyEnv()): boolean {
|
|
72
|
+
return env.httpProxy !== null || env.httpsProxy !== null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `host[:port]` 切分(兼容 IPv6 方括号形态);无端口 → null */
|
|
76
|
+
function splitHostPort(entry: string): { host: string; port: string | null } {
|
|
77
|
+
if (entry.startsWith('[')) {
|
|
78
|
+
const end = entry.indexOf(']')
|
|
79
|
+
if (end !== -1) {
|
|
80
|
+
const host = entry.slice(0, end + 1)
|
|
81
|
+
const rest = entry.slice(end + 1)
|
|
82
|
+
return rest.startsWith(':') ? { host, port: rest.slice(1) } : { host, port: null }
|
|
83
|
+
}
|
|
84
|
+
return { host: entry, port: null }
|
|
85
|
+
}
|
|
86
|
+
const at = entry.lastIndexOf(':')
|
|
87
|
+
// 仅当冒号后全是数字才视作端口(避免把裸 IPv6 误切)
|
|
88
|
+
if (at !== -1 && /^\d+$/.test(entry.slice(at + 1))) {
|
|
89
|
+
return { host: entry.slice(0, at), port: entry.slice(at + 1) }
|
|
90
|
+
}
|
|
91
|
+
return { host: entry, port: null }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 目标是否命中 `NO_PROXY`。
|
|
96
|
+
* 支持:`*`(全直连)、精确主机、`.example.com` 与 `example.com`(含子域)、可选 `:port` 限定。
|
|
97
|
+
*/
|
|
98
|
+
export function matchesNoProxy(host: string, port: string, noProxy: string | null): boolean {
|
|
99
|
+
if (noProxy === null) return false
|
|
100
|
+
const target = host.toLowerCase()
|
|
101
|
+
for (const raw of noProxy.split(',')) {
|
|
102
|
+
const entry = raw.trim().toLowerCase()
|
|
103
|
+
if (entry === '') continue
|
|
104
|
+
if (entry === '*') return true
|
|
105
|
+
const { host: entryHost, port: entryPort } = splitHostPort(entry)
|
|
106
|
+
if (entryPort !== null && entryPort !== port) continue
|
|
107
|
+
const bare = entryHost.startsWith('.') ? entryHost.slice(1) : entryHost
|
|
108
|
+
if (bare === '') continue
|
|
109
|
+
if (target === bare || target.endsWith(`.${bare}`)) return true
|
|
110
|
+
}
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** URL 的有效端口(缺省按协议补) */
|
|
115
|
+
function effectivePort(url: URL): string {
|
|
116
|
+
if (url.port !== '') return url.port
|
|
117
|
+
return url.protocol === 'https:' ? '443' : '80'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 该目标应使用的代理 URL;无需代理(未配置 / 命中 NO_PROXY / 非法)→ null。
|
|
122
|
+
*
|
|
123
|
+
* 选择规则(对齐 undici `EnvHttpProxyAgent` 的直觉语义):
|
|
124
|
+
* - `https:` 目标 → `HTTPS_PROXY`,缺省回退 `HTTP_PROXY`;
|
|
125
|
+
* - `http:` 目标 → `HTTP_PROXY`,缺省回退 `HTTPS_PROXY`。
|
|
126
|
+
*/
|
|
127
|
+
export function proxyUrlFor(target: string | URL, env: ProxyEnv = readProxyEnv()): string | null {
|
|
128
|
+
let url: URL
|
|
129
|
+
try {
|
|
130
|
+
url = typeof target === 'string' ? new URL(target) : target
|
|
131
|
+
} catch {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
135
|
+
if (matchesNoProxy(url.hostname, effectivePort(url), env.noProxy)) return null
|
|
136
|
+
const candidate = url.protocol === 'https:'
|
|
137
|
+
? (env.httpsProxy ?? env.httpProxy)
|
|
138
|
+
: (env.httpProxy ?? env.httpsProxy)
|
|
139
|
+
if (candidate === null) return null
|
|
140
|
+
try {
|
|
141
|
+
const proxy = new URL(candidate.includes('://') ? candidate : `http://${candidate}`)
|
|
142
|
+
if (proxy.hostname === '') return null
|
|
143
|
+
if (proxy.protocol !== 'http:' && proxy.protocol !== 'https:') return null
|
|
144
|
+
return proxy.toString()
|
|
145
|
+
} catch {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 代理的**脱敏**描述(供日志/报错提示;凭据一律剥离,绝不外泄) */
|
|
151
|
+
export function describeProxy(proxyUrl: string): string {
|
|
152
|
+
try {
|
|
153
|
+
const u = new URL(proxyUrl)
|
|
154
|
+
return `${u.protocol}//${u.hostname}${u.port !== '' ? `:${u.port}` : ''}`
|
|
155
|
+
} catch {
|
|
156
|
+
return '(invalid proxy)'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 一次性 socket 的 agent(隧道已建立;keepAlive 关闭,用完即毁) */
|
|
161
|
+
class TunnelAgent extends https.Agent {
|
|
162
|
+
private readonly tunnel: net.Socket
|
|
163
|
+
constructor(tunnel: net.Socket) {
|
|
164
|
+
super({ keepAlive: false, maxSockets: 1 })
|
|
165
|
+
this.tunnel = tunnel
|
|
166
|
+
}
|
|
167
|
+
override createConnection(): net.Socket {
|
|
168
|
+
return this.tunnel
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 建立到代理的连接(代理本身可能是 https) */
|
|
173
|
+
function connectToProxy(proxy: URL, timeoutMs: number): Promise<net.Socket> {
|
|
174
|
+
const port = Number(proxy.port !== '' ? proxy.port : (proxy.protocol === 'https:' ? 443 : 80))
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const onConnect = (): void => { socket.setTimeout(0); resolve(socket) }
|
|
177
|
+
const socket = proxy.protocol === 'https:'
|
|
178
|
+
? tls.connect({ host: proxy.hostname, port, servername: proxy.hostname }, onConnect)
|
|
179
|
+
: net.connect({ host: proxy.hostname, port }, onConnect)
|
|
180
|
+
socket.setTimeout(timeoutMs, () => {
|
|
181
|
+
socket.destroy()
|
|
182
|
+
reject(new Error(`代理连接超时 / proxy connect timed out (${timeoutMs}ms)`))
|
|
183
|
+
})
|
|
184
|
+
socket.once('error', (err) => { reject(err) })
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 经 HTTP 代理建立到目标的隧道(`CONNECT host:port`)。
|
|
190
|
+
* 返回的 socket 已与目标建立字节通道;https 目标再由调用方叠加 TLS。
|
|
191
|
+
*/
|
|
192
|
+
async function openTunnel(target: URL, proxy: URL, timeoutMs: number): Promise<net.Socket> {
|
|
193
|
+
const socket = await connectToProxy(proxy, timeoutMs)
|
|
194
|
+
const targetPort = effectivePort(target)
|
|
195
|
+
const authority = `${target.hostname}:${targetPort}`
|
|
196
|
+
const lines = [
|
|
197
|
+
`CONNECT ${authority} HTTP/1.1`,
|
|
198
|
+
`Host: ${authority}`,
|
|
199
|
+
// 代理凭据只在此处使用,绝不写日志
|
|
200
|
+
...(proxy.username !== '' || proxy.password !== ''
|
|
201
|
+
? [`Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString('base64')}`]
|
|
202
|
+
: []),
|
|
203
|
+
'Proxy-Connection: keep-alive',
|
|
204
|
+
'',
|
|
205
|
+
'',
|
|
206
|
+
]
|
|
207
|
+
return await new Promise<net.Socket>((resolve, reject) => {
|
|
208
|
+
let buffered = ''
|
|
209
|
+
const fail = (message: string): void => {
|
|
210
|
+
socket.destroy()
|
|
211
|
+
reject(new Error(message))
|
|
212
|
+
}
|
|
213
|
+
const onData = (chunk: Buffer): void => {
|
|
214
|
+
buffered += chunk.toString('latin1')
|
|
215
|
+
const end = buffered.indexOf('\r\n\r\n')
|
|
216
|
+
if (end === -1) {
|
|
217
|
+
if (buffered.length > 8192) {
|
|
218
|
+
socket.off('data', onData)
|
|
219
|
+
fail('代理 CONNECT 响应异常(头部过长)/ malformed CONNECT response')
|
|
220
|
+
}
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
socket.off('data', onData)
|
|
224
|
+
const head = buffered.slice(0, end)
|
|
225
|
+
const status = Number((head.split('\r\n')[0] ?? '').split(' ')[1])
|
|
226
|
+
if (status !== 200) {
|
|
227
|
+
fail(`代理拒绝 CONNECT(HTTP ${status})/ proxy refused CONNECT`)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
// CONNECT 响应头之后的字节已属隧道数据 → 回填,避免 TLS 握手丢包
|
|
231
|
+
const rest = Buffer.from(buffered.slice(end + 4), 'latin1')
|
|
232
|
+
if (rest.length > 0) socket.unshift(rest)
|
|
233
|
+
resolve(socket)
|
|
234
|
+
}
|
|
235
|
+
socket.on('data', onData)
|
|
236
|
+
socket.once('error', (err) => { socket.off('data', onData); reject(err) })
|
|
237
|
+
socket.once('close', () => { socket.off('data', onData); reject(new Error('代理连接在 CONNECT 完成前关闭 / proxy closed during CONNECT')) })
|
|
238
|
+
socket.write(lines.join('\r\n'))
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 单次请求的输入 */
|
|
243
|
+
export interface RawRequestOptions {
|
|
244
|
+
method?: string
|
|
245
|
+
url: string
|
|
246
|
+
headers?: Record<string, string>
|
|
247
|
+
body?: Buffer | string | undefined
|
|
248
|
+
timeoutMs?: number
|
|
249
|
+
signal?: AbortSignal
|
|
250
|
+
env?: ProxyEnv
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 单次请求的响应(始终缓冲完整响应体;插件用到的响应体都是小 JSON/文本) */
|
|
254
|
+
export interface RawResponse {
|
|
255
|
+
status: number
|
|
256
|
+
statusText: string
|
|
257
|
+
headers: Headers
|
|
258
|
+
body: Buffer
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 请求结果归一到 node 的 IncomingMessage(直连与经代理两条路径共用收尾逻辑) */
|
|
262
|
+
function collectResponse(res: http.IncomingMessage, timeoutMs: number): Promise<RawResponse> {
|
|
263
|
+
return new Promise<RawResponse>((resolve, reject) => {
|
|
264
|
+
const chunks: Buffer[] = []
|
|
265
|
+
res.on('data', (chunk: Buffer) => chunks.push(Buffer.from(chunk)))
|
|
266
|
+
res.on('end', () => {
|
|
267
|
+
const headers = new Headers()
|
|
268
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
269
|
+
if (value === undefined) continue
|
|
270
|
+
if (Array.isArray(value)) for (const v of value) headers.append(key, v)
|
|
271
|
+
else headers.set(key, String(value))
|
|
272
|
+
}
|
|
273
|
+
resolve({
|
|
274
|
+
status: res.statusCode ?? 0,
|
|
275
|
+
statusText: res.statusMessage ?? '',
|
|
276
|
+
headers,
|
|
277
|
+
body: Buffer.concat(chunks),
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
res.on('error', reject)
|
|
281
|
+
if (timeoutMs > 0) {
|
|
282
|
+
res.setTimeout(timeoutMs, () => {
|
|
283
|
+
res.destroy()
|
|
284
|
+
reject(new Error(`Request timed out after ${timeoutMs}ms`))
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 发起一次 HTTP/1.1 请求(自动:直连 / 经代理 absolute-form / 经代理 CONNECT 隧道)。
|
|
292
|
+
* 不跟随重定向(跟随策略由调用方决定:WebDAV 保方法语义,fetch 按浏览器语义)。
|
|
293
|
+
*/
|
|
294
|
+
export async function requestOnce(options: RawRequestOptions): Promise<RawResponse> {
|
|
295
|
+
const target = new URL(options.url)
|
|
296
|
+
const env = options.env ?? readProxyEnv()
|
|
297
|
+
const proxyUrl = proxyUrlFor(target, env)
|
|
298
|
+
const method = (options.method ?? 'GET').toUpperCase()
|
|
299
|
+
const timeoutMs = options.timeoutMs ?? 0
|
|
300
|
+
const payload = options.body === undefined
|
|
301
|
+
? undefined
|
|
302
|
+
: Buffer.isBuffer(options.body) ? options.body : Buffer.from(options.body, 'utf8')
|
|
303
|
+
const headers: Record<string, string> = {
|
|
304
|
+
...(payload !== undefined ? { 'content-length': String(payload.length) } : {}),
|
|
305
|
+
...(options.headers ?? {}),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** url 非 null → 以 URL 形态发起(node 自行解析 host/port/path/TLS);否则按显式 options 发起。 */
|
|
309
|
+
const send = (
|
|
310
|
+
lib: typeof http | typeof https,
|
|
311
|
+
url: string | null,
|
|
312
|
+
requestOptions: http.RequestOptions,
|
|
313
|
+
): Promise<RawResponse> =>
|
|
314
|
+
new Promise<RawResponse>((resolve, reject) => {
|
|
315
|
+
const onResponse = (res: http.IncomingMessage): void => {
|
|
316
|
+
collectResponse(res, timeoutMs).then(resolve, reject)
|
|
317
|
+
}
|
|
318
|
+
const req = url === null
|
|
319
|
+
? lib.request(requestOptions, onResponse)
|
|
320
|
+
: lib.request(url, requestOptions, onResponse)
|
|
321
|
+
if (timeoutMs > 0) {
|
|
322
|
+
req.setTimeout(timeoutMs, () => {
|
|
323
|
+
req.destroy()
|
|
324
|
+
reject(new Error(`Request timed out after ${timeoutMs}ms`))
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
req.on('error', reject)
|
|
328
|
+
if (options.signal !== undefined) {
|
|
329
|
+
const onAbort = (): void => { req.destroy(new Error('The operation was aborted.')) }
|
|
330
|
+
if (options.signal.aborted) onAbort()
|
|
331
|
+
else options.signal.addEventListener('abort', onAbort, { once: true })
|
|
332
|
+
}
|
|
333
|
+
if (payload !== undefined) req.write(payload)
|
|
334
|
+
req.end()
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// ---- 未经代理:直连(默认路径,行为与此前一致)----
|
|
338
|
+
if (proxyUrl === null) {
|
|
339
|
+
const lib = target.protocol === 'https:' ? https : http
|
|
340
|
+
return await send(lib, options.url, { method, headers })
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const proxy = new URL(proxyUrl)
|
|
344
|
+
|
|
345
|
+
// ---- http 目标经代理:absolute-form 直接把完整 URL 交给代理 ----
|
|
346
|
+
if (target.protocol === 'http:') {
|
|
347
|
+
const port = Number(proxy.port !== '' ? proxy.port : (proxy.protocol === 'https:' ? 443 : 80))
|
|
348
|
+
const lib = proxy.protocol === 'https:' ? https : http
|
|
349
|
+
return await send(lib, null, {
|
|
350
|
+
method,
|
|
351
|
+
host: proxy.hostname,
|
|
352
|
+
port,
|
|
353
|
+
path: target.toString(),
|
|
354
|
+
headers: {
|
|
355
|
+
...headers,
|
|
356
|
+
host: target.host,
|
|
357
|
+
...(proxy.username !== '' || proxy.password !== ''
|
|
358
|
+
? { 'proxy-authorization': `Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString('base64')}` }
|
|
359
|
+
: {}),
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---- https 目标经代理:CONNECT 隧道 + TLS(SNI 保留目标主机名)----
|
|
365
|
+
const tunnel = await openTunnel(target, proxy, timeoutMs > 0 ? timeoutMs : DEFAULT_CONNECT_TIMEOUT_MS)
|
|
366
|
+
const secure = tls.connect({
|
|
367
|
+
socket: tunnel,
|
|
368
|
+
servername: target.hostname,
|
|
369
|
+
// 与直连一致:默认校验证书链;不做任何降级
|
|
370
|
+
})
|
|
371
|
+
await new Promise<void>((resolve, reject) => {
|
|
372
|
+
secure.once('secureConnect', () => { resolve() })
|
|
373
|
+
secure.once('error', reject)
|
|
374
|
+
})
|
|
375
|
+
return await send(https, null, {
|
|
376
|
+
method,
|
|
377
|
+
headers,
|
|
378
|
+
host: target.hostname,
|
|
379
|
+
port: Number(effectivePort(target)),
|
|
380
|
+
path: `${target.pathname}${target.search}`,
|
|
381
|
+
agent: new TunnelAgent(secure),
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** 把 `HeadersInit` 归一为小写键的普通对象 */
|
|
386
|
+
function normalizeHeaders(init: HeadersInit | undefined): Record<string, string> {
|
|
387
|
+
const out: Record<string, string> = {}
|
|
388
|
+
if (init === undefined) return out
|
|
389
|
+
if (init instanceof Headers) {
|
|
390
|
+
init.forEach((value, key) => { out[key.toLowerCase()] = value })
|
|
391
|
+
return out
|
|
392
|
+
}
|
|
393
|
+
if (Array.isArray(init)) {
|
|
394
|
+
for (const [key, value] of init) out[String(key).toLowerCase()] = String(value)
|
|
395
|
+
return out
|
|
396
|
+
}
|
|
397
|
+
for (const [key, value] of Object.entries(init)) {
|
|
398
|
+
if (value !== undefined) out[key.toLowerCase()] = String(value)
|
|
399
|
+
}
|
|
400
|
+
return out
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** 同源判断(协议 + host,含端口) */
|
|
404
|
+
function sameOrigin(a: string, b: string): boolean {
|
|
405
|
+
try {
|
|
406
|
+
const ua = new URL(a)
|
|
407
|
+
const ub = new URL(b)
|
|
408
|
+
return ua.protocol === ub.protocol && ua.host === ub.host
|
|
409
|
+
} catch {
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** 重定向上限(与 WebDAV 通道一致) */
|
|
415
|
+
const MAX_REDIRECTS = 5
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 代理感知的 fetch 兼容实现(仅插件自身使用;替代 `globalThis.fetch` 的默认值)。
|
|
419
|
+
*
|
|
420
|
+
* 与浏览器 fetch 的关键语义对齐:自动跟随 301/302/303/307/308(上限 5 跳);303 且非 GET/HEAD
|
|
421
|
+
* 降级为 GET 并丢弃请求体;**跨源跳转剥离 `authorization`**(token 不转发给第三方域名);
|
|
422
|
+
* 无响应体状态(204/304 等)不构造 body。
|
|
423
|
+
*/
|
|
424
|
+
export function createProxyAwareFetch(env: ProxyEnv = readProxyEnv()): typeof fetch {
|
|
425
|
+
const impl = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
426
|
+
const url = typeof input === 'string'
|
|
427
|
+
? input
|
|
428
|
+
: input instanceof URL ? input.toString() : (input as Request).url
|
|
429
|
+
let currentUrl = url
|
|
430
|
+
let method = (init?.method ?? 'GET').toUpperCase()
|
|
431
|
+
let headers = normalizeHeaders(init?.headers)
|
|
432
|
+
let body: string | Buffer | undefined = init?.body === undefined || init?.body === null
|
|
433
|
+
? undefined
|
|
434
|
+
: String(init.body)
|
|
435
|
+
|
|
436
|
+
for (let hop = 0; ; hop += 1) {
|
|
437
|
+
const res = await requestOnce({
|
|
438
|
+
method,
|
|
439
|
+
url: currentUrl,
|
|
440
|
+
headers,
|
|
441
|
+
...(body !== undefined ? { body } : {}),
|
|
442
|
+
...(init?.signal != null ? { signal: init.signal } : {}),
|
|
443
|
+
env,
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const location = res.headers.get('location')
|
|
447
|
+
if (!REDIRECT_STATUSES.has(res.status) || location === null) {
|
|
448
|
+
return new Response(
|
|
449
|
+
BODYLESS_STATUSES.has(res.status) ? null : new Uint8Array(res.body),
|
|
450
|
+
{ status: res.status, statusText: res.statusText, headers: res.headers },
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
if (hop >= MAX_REDIRECTS) {
|
|
454
|
+
throw new TypeError(`fetch failed: too many redirects (${MAX_REDIRECTS} max)`)
|
|
455
|
+
}
|
|
456
|
+
let nextUrl: string
|
|
457
|
+
try {
|
|
458
|
+
nextUrl = new URL(location, currentUrl).toString()
|
|
459
|
+
} catch {
|
|
460
|
+
return new Response(
|
|
461
|
+
BODYLESS_STATUSES.has(res.status) ? null : new Uint8Array(res.body),
|
|
462
|
+
{ status: res.status, statusText: res.statusText, headers: res.headers },
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
if (!sameOrigin(currentUrl, nextUrl)) delete headers['authorization']
|
|
466
|
+
if (res.status === 303 && method !== 'GET' && method !== 'HEAD') {
|
|
467
|
+
method = 'GET'
|
|
468
|
+
body = undefined
|
|
469
|
+
delete headers['content-length']
|
|
470
|
+
}
|
|
471
|
+
currentUrl = nextUrl
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return impl as unknown as typeof fetch
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* 默认出站 fetcher:**未配置代理时原样返回全局 `fetch`**(保持既有行为零变化);
|
|
479
|
+
* 配置了代理才切到代理感知实现。
|
|
480
|
+
*/
|
|
481
|
+
export function defaultFetcher(env: ProxyEnv = readProxyEnv()): typeof fetch {
|
|
482
|
+
return isProxyConfigured(env) ? createProxyAwareFetch(env) : globalThis.fetch
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** 启动日志用:当前生效的代理描述(已脱敏),未启用 → null */
|
|
486
|
+
export function activeProxySummary(env: ProxyEnv = readProxyEnv()): { http: string | null; https: string | null; noProxyEntries: number } | null {
|
|
487
|
+
if (!isProxyConfigured(env)) return null
|
|
488
|
+
return {
|
|
489
|
+
http: env.httpProxy !== null ? describeProxy(env.httpProxy) : null,
|
|
490
|
+
https: env.httpsProxy !== null ? describeProxy(env.httpsProxy) : null,
|
|
491
|
+
noProxyEntries: env.noProxy !== null ? env.noProxy.split(',').filter((s) => s.trim() !== '').length : 0,
|
|
492
|
+
}
|
|
493
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue #37 回归:递归遍历必须**跟随**目录 junction / 符号链接(此前静默跳过,备份报成功但缺内容),
|
|
3
|
+
* 并如实报告「跟随了什么 / 跳过了什么」。
|
|
4
|
+
*
|
|
5
|
+
* 用真实临时目录 + 真实链接(Windows 用 junction,无需管理员权限)。
|
|
6
|
+
*/
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import fssync from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { listRecursiveFollowingLinks } from './recursive-walk.ts';
|
|
14
|
+
|
|
15
|
+
function tmpDir(prefix: string): string {
|
|
16
|
+
return fssync.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 建目录链接:win32 用 junction(不需要管理员/开发者模式),其它平台用 dir 符号链接。 */
|
|
20
|
+
async function linkDir(target: string, linkPath: string): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
await fs.symlink(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('issue #37:跟随目录链接收集内容;越界/断链/自引用链接跳过并留痕', async (t) => {
|
|
30
|
+
const home = tmpDir('dshcm-home-');
|
|
31
|
+
const outside = tmpDir('dshcm-outside-');
|
|
32
|
+
t.after(() => {
|
|
33
|
+
fssync.rmSync(home, { recursive: true, force: true });
|
|
34
|
+
fssync.rmSync(outside, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await fs.mkdir(path.join(home, 'skills', 'real'), { recursive: true });
|
|
38
|
+
await fs.writeFile(path.join(home, 'skills', 'a.md'), 'A');
|
|
39
|
+
await fs.writeFile(path.join(home, 'skills', 'real', 'inner.md'), 'INNER');
|
|
40
|
+
await fs.writeFile(path.join(outside, 'secret.md'), 'OUTSIDE');
|
|
41
|
+
|
|
42
|
+
// ① 指向 home 内真实目录的链接(issue #37 的核心场景:技能共享 shared/ scripts/)
|
|
43
|
+
const okLink = await linkDir(path.join(home, 'skills', 'real'), path.join(home, 'skills', 'link-dir'));
|
|
44
|
+
if (!okLink) {
|
|
45
|
+
t.skip('本环境无法创建目录链接(权限受限),跳过 junction 跟随用例');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// ② 自引用链接(指向祖先目录)→ 必须终止并记为 loop
|
|
49
|
+
await linkDir(path.join(home, 'skills'), path.join(home, 'skills', 'self'));
|
|
50
|
+
// ③ 断链
|
|
51
|
+
await fs.symlink(path.join(home, 'skills', 'nope'), path.join(home, 'skills', 'broken'));
|
|
52
|
+
// ④ 指向 home 之外
|
|
53
|
+
await linkDir(outside, path.join(home, 'skills', 'outside'));
|
|
54
|
+
|
|
55
|
+
const listing = await listRecursiveFollowingLinks(path.join(home, 'skills'), home);
|
|
56
|
+
|
|
57
|
+
// 真实内容一个都不能少:真实目录 + 链接目录两条路径都收集(此前 link-dir 整块丢失)
|
|
58
|
+
assert.ok(listing.paths.includes('skills/a.md'), `缺 a.md: ${listing.paths.join(',')}`);
|
|
59
|
+
assert.ok(listing.paths.includes('skills/real/inner.md'), `缺 real/inner.md: ${listing.paths.join(',')}`);
|
|
60
|
+
assert.ok(
|
|
61
|
+
listing.paths.includes('skills/link-dir/inner.md'),
|
|
62
|
+
`链接目录内容必须进备份(issue #37 根因): ${listing.paths.join(',')}`,
|
|
63
|
+
);
|
|
64
|
+
assert.ok(
|
|
65
|
+
!listing.paths.some((p) => p.includes('outside')),
|
|
66
|
+
'home 之外的目标不得进备份(既有边界不变)',
|
|
67
|
+
);
|
|
68
|
+
assert.ok(!listing.paths.some((p) => p.includes('self')), '自引用链接不得递归展开');
|
|
69
|
+
|
|
70
|
+
// 跳过项必须留痕(用户能从报告里知道内容不全)
|
|
71
|
+
const byReason = new Map(listing.skippedLinks.map((s) => [s.path, s.reason]));
|
|
72
|
+
assert.equal(byReason.get('skills/self'), 'loop');
|
|
73
|
+
assert.equal(byReason.get('skills/outside'), 'outside-home');
|
|
74
|
+
assert.equal(byReason.get('skills/broken'), 'broken');
|
|
75
|
+
assert.equal(listing.followedLinks >= 1, true, '至少一个链接被成功跟随');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('issue #37:链接指向同一目标只展开一次(防环 + 防重复)', async (t) => {
|
|
79
|
+
const home = tmpDir('dshcm-home2-');
|
|
80
|
+
t.after(() => fssync.rmSync(home, { recursive: true, force: true }));
|
|
81
|
+
await fs.mkdir(path.join(home, 'skills', 'target'), { recursive: true });
|
|
82
|
+
await fs.writeFile(path.join(home, 'skills', 'target', 'x.md'), 'X');
|
|
83
|
+
const a = await linkDir(path.join(home, 'skills', 'target'), path.join(home, 'skills', 'l1'));
|
|
84
|
+
const b = await linkDir(path.join(home, 'skills', 'target'), path.join(home, 'skills', 'l2'));
|
|
85
|
+
if (!a || !b) {
|
|
86
|
+
t.skip('本环境无法创建目录链接,跳过用例');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const listing = await listRecursiveFollowingLinks(path.join(home, 'skills'), home);
|
|
90
|
+
const hits = listing.paths.filter((p) => p.endsWith('x.md'));
|
|
91
|
+
// target 自身 + 其中一个链接;另一个链接被判为 loop(内容不会重复两遍)
|
|
92
|
+
assert.equal(hits.length, 2, `同一目标只应展开一次(+真实目录): ${listing.paths.join(',')}`);
|
|
93
|
+
assert.equal(listing.skippedLinks.filter((s) => s.reason === 'loop').length, 1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('issue #37:指向 home 之外的**文件**链接不得被读入(目录链接与文件链接同一判据)', async (t) => {
|
|
97
|
+
const home = tmpDir('dshcm-home4-');
|
|
98
|
+
const outside = tmpDir('dshcm-out5-');
|
|
99
|
+
t.after(() => {
|
|
100
|
+
fssync.rmSync(home, { recursive: true, force: true });
|
|
101
|
+
fssync.rmSync(outside, { recursive: true, force: true });
|
|
102
|
+
});
|
|
103
|
+
await fs.mkdir(path.join(home, 'skills'), { recursive: true });
|
|
104
|
+
await fs.writeFile(path.join(home, 'skills', 'ok.md'), 'OK');
|
|
105
|
+
await fs.writeFile(path.join(outside, 'secret.md'), 'SECRET');
|
|
106
|
+
// 文件链接(不是目录链接):目标在 home 外
|
|
107
|
+
try {
|
|
108
|
+
await fs.symlink(path.join(outside, 'secret.md'), path.join(home, 'skills', 'leak.md'), 'file');
|
|
109
|
+
} catch {
|
|
110
|
+
t.skip('本环境无法创建文件符号链接,跳过');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const listing = await listRecursiveFollowingLinks(path.join(home, 'skills'), home);
|
|
114
|
+
assert.deepEqual(listing.paths, ['skills/ok.md'], `home 外的文件链接不得进清单: ${listing.paths.join(',')}`);
|
|
115
|
+
assert.equal(listing.skippedLinks.find((s) => s.path === 'skills/leak.md')?.reason, 'outside-home');
|
|
116
|
+
|
|
117
|
+
// home 内的文件链接 → 跟随(内容属于配置 home,可安全备份)
|
|
118
|
+
await fs.writeFile(path.join(home, 'skills', 'inside.md'), 'INSIDE');
|
|
119
|
+
try {
|
|
120
|
+
await fs.symlink(path.join(home, 'skills', 'inside.md'), path.join(home, 'skills', 'alias.md'), 'file');
|
|
121
|
+
} catch {
|
|
122
|
+
t.skip('本环境无法创建文件符号链接,跳过');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const listing2 = await listRecursiveFollowingLinks(path.join(home, 'skills'), home);
|
|
126
|
+
assert.ok(listing2.paths.includes('skills/alias.md'), `home 内的文件链接应被跟随: ${listing2.paths.join(',')}`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('issue #37:目录不可读不中断整次遍历;base 不在 home 内返回空', async (t) => {
|
|
130
|
+
const home = tmpDir('dshcm-home3-');
|
|
131
|
+
const elsewhere = tmpDir('dshcm-else-');
|
|
132
|
+
t.after(() => {
|
|
133
|
+
fssync.rmSync(home, { recursive: true, force: true });
|
|
134
|
+
fssync.rmSync(elsewhere, { recursive: true, force: true });
|
|
135
|
+
});
|
|
136
|
+
await fs.mkdir(path.join(home, 'skills'), { recursive: true });
|
|
137
|
+
await fs.writeFile(path.join(home, 'skills', 'ok.md'), 'OK');
|
|
138
|
+
await fs.writeFile(path.join(elsewhere, 'x.md'), 'X');
|
|
139
|
+
// 目录不存在 → 空清单(既有语义:目录不存在视为空)
|
|
140
|
+
const missing = await listRecursiveFollowingLinks(path.join(home, 'nope'), home);
|
|
141
|
+
assert.deepEqual(missing.paths, []);
|
|
142
|
+
// base 越界 → 空清单,绝不把 home 之外的东西列出来
|
|
143
|
+
const escaped = await listRecursiveFollowingLinks(elsewhere, home);
|
|
144
|
+
assert.deepEqual(escaped.paths, []);
|
|
145
|
+
// 正常路径仍可遍历
|
|
146
|
+
const ok = await listRecursiveFollowingLinks(path.join(home, 'skills'), home);
|
|
147
|
+
assert.deepEqual(ok.paths, ['skills/ok.md']);
|
|
148
|
+
});
|