dsh-config-manager 0.1.71 → 0.1.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -96
- package/README.zh-CN.md +207 -86
- package/lib/adapters/agent-instructions.js +1 -1
- package/lib/adapters/file-collection.js +1 -1
- package/lib/adapters/index.d.ts +1 -1
- package/lib/adapters/index.js +1 -1
- package/lib/adapters/link-report.d.ts +1 -0
- package/lib/adapters/link-report.js +27 -3
- package/lib/adapters/messages.d.ts +18 -0
- package/lib/adapters/messages.js +50 -0
- package/lib/adapters/plugin-files.js +3 -2
- package/lib/adapters/plugins.js +4 -3
- package/lib/adapters/prompts.d.ts +1 -0
- package/lib/adapters/prompts.js +7 -1
- package/lib/adapters/self.d.ts +7 -1
- package/lib/adapters/self.js +1 -1
- package/lib/adapters/sessions.d.ts +1 -0
- package/lib/adapters/sessions.js +4 -2
- package/lib/adapters/ui.d.ts +2 -2
- package/lib/adapters/ui.js +25 -25
- package/lib/adapters/workspaces.d.ts +1 -0
- package/lib/adapters/workspaces.js +7 -3
- package/lib/cli/actions.d.ts +9 -1
- package/lib/cli/actions.js +5 -2
- package/lib/cli/help.js +23 -10
- package/lib/cli/index.d.ts +23 -0
- package/lib/cli/index.js +90 -32
- package/lib/cli/sessions-inspect.js +3 -1
- package/lib/cli/sessions-repair.js +25 -6
- package/lib/cli/web/page.d.ts +12 -0
- package/lib/cli/web/page.js +21 -12
- package/lib/cli/web/routes.js +2 -2
- package/lib/cli/web/server.js +4 -4
- package/lib/client.d.ts +1349 -490
- package/lib/client.js +43047 -36738
- package/lib/core/backup-plan.js +30 -17
- package/lib/core/backup-verify.js +24 -23
- package/lib/core/backup.d.ts +5 -0
- package/lib/core/backup.js +51 -25
- package/lib/core/boot-rescue.js +4 -3
- package/lib/core/boot-safety.js +2 -1
- package/lib/core/command-rollback.d.ts +178 -0
- package/lib/core/command-rollback.js +289 -0
- package/lib/core/consult-source.js +7 -6
- package/lib/core/disk-usage.d.ts +74 -10
- package/lib/core/disk-usage.js +67 -4
- package/lib/core/engine-locale.d.ts +87 -0
- package/lib/core/engine-locale.js +125 -0
- package/lib/core/exporter.js +3 -3
- package/lib/core/fork-reconcile.d.ts +128 -0
- package/lib/core/fork-reconcile.js +150 -0
- package/lib/core/journal.js +6 -5
- package/lib/core/local-plugin-host.js +3 -2
- package/lib/core/local-plugin-pack.js +11 -6
- package/lib/core/messages-core.d.ts +292 -0
- package/lib/core/messages-core.js +615 -0
- package/lib/core/messages.d.ts +60 -2
- package/lib/core/messages.js +116 -13
- package/lib/core/migration-consult.js +21 -20
- package/lib/core/migration-history.js +19 -5
- package/lib/core/model-tools.d.ts +0 -22
- package/lib/core/model-tools.js +50 -27
- package/lib/core/phase3-host.js +30 -5
- package/lib/core/plugin-cli.js +18 -19
- package/lib/core/reconcile.d.ts +2 -14
- package/lib/core/reconcile.js +87 -17
- package/lib/core/recovery-orchestrator.js +19 -18
- package/lib/core/reinstall.d.ts +4 -0
- package/lib/core/reinstall.js +26 -21
- package/lib/core/restore.js +20 -17
- package/lib/core/rollback-ignore-rules.d.ts +31 -0
- package/lib/core/rollback-ignore-rules.js +140 -0
- package/lib/core/rollback-point.d.ts +1208 -0
- package/lib/core/rollback-point.js +2993 -0
- package/lib/core/run-registry.js +6 -5
- package/lib/core/session-health.d.ts +15 -0
- package/lib/core/session-meta.d.ts +42 -2
- package/lib/core/session-meta.js +140 -1
- package/lib/core/session-title.d.ts +28 -0
- package/lib/core/session-title.js +185 -0
- package/lib/core/startup-barrier.d.ts +24 -0
- package/lib/core/startup-barrier.js +22 -0
- package/lib/core/transaction-coordinator.js +4 -3
- package/lib/core/types.d.ts +18 -1
- package/lib/core/validator.js +5 -4
- package/lib/core/verify-recovery.js +31 -30
- package/lib/foreign/bundle.js +3 -2
- package/lib/foreign/continue.js +2 -1
- package/lib/foreign/crush.js +2 -1
- package/lib/foreign/gemini.js +2 -1
- package/lib/foreign/goose.js +2 -1
- package/lib/foreign/kilocode.js +2 -1
- package/lib/foreign/kimi.js +2 -1
- package/lib/foreign/messages.d.ts +15 -0
- package/lib/foreign/messages.js +40 -0
- package/lib/foreign/mimocode.js +2 -1
- package/lib/foreign/openclaw.js +2 -1
- package/lib/foreign/opencode.js +2 -1
- package/lib/foreign/pi.js +2 -1
- package/lib/foreign/qoder.js +2 -1
- package/lib/foreign/qwen.js +2 -1
- package/lib/foreign/registry.js +7 -6
- package/lib/foreign/teleagent.js +2 -1
- package/lib/foreign/workbuddy.js +2 -1
- package/lib/foreign/zcode.js +2 -1
- package/lib/foreign/zed.js +2 -1
- package/lib/index.d.ts +223 -7
- package/lib/index.js +729 -54
- package/lib/market/git-file-writer.js +20 -19
- package/lib/market/github-repos.js +35 -19
- package/lib/market/index-parser.js +31 -30
- package/lib/market/messages-market.d.ts +138 -0
- package/lib/market/messages-market.js +305 -0
- package/lib/market/my-repo.d.ts +2 -2
- package/lib/market/my-repo.js +32 -29
- package/lib/market/prepare.js +18 -15
- package/lib/market/security.js +20 -19
- package/lib/market/types.js +2 -1
- package/lib/market/url.js +2 -1
- package/lib/migrations/index.js +6 -5
- package/lib/migrations/messages-migrations.d.ts +32 -0
- package/lib/migrations/messages-migrations.js +58 -0
- package/lib/profiles/dsh-profile-launcher.js +2 -1
- package/lib/profiles/dsh-profile-runtime.js +2 -1
- package/lib/profiles/messages.d.ts +7 -0
- package/lib/profiles/messages.js +27 -0
- package/lib/routes/backup.js +18 -2
- package/lib/routes/import.js +3 -3
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +3 -2
- package/lib/routes/kit.js +6 -1
- package/lib/routes/market.js +1 -1
- package/lib/routes/messages.d.ts +42 -0
- package/lib/routes/messages.js +100 -0
- package/lib/routes/profiles.js +12 -6
- package/lib/routes/recovery.js +378 -114
- package/lib/routes/snapshots.js +51 -4
- package/lib/routes/sync.js +68 -16
- package/lib/schema/config.d.ts +11 -1
- package/lib/schema/config.js +52 -20
- package/lib/schema/index.d.ts +8 -6
- package/lib/schema/index.js +8 -6
- package/lib/schema/manifest.js +19 -18
- package/lib/schema/messages.d.ts +47 -0
- package/lib/schema/messages.js +110 -0
- package/lib/schema/section-registry.d.ts +41 -14
- package/lib/schema/section-registry.js +35 -29
- package/lib/schema/versions.js +6 -5
- package/lib/security/encryption.js +16 -15
- package/lib/security/integrity.js +8 -7
- package/lib/security/messages.d.ts +30 -0
- package/lib/security/messages.js +77 -0
- package/lib/security/redaction.js +12 -0
- package/lib/security/secret-scanner.js +16 -13
- package/lib/security/vault.js +2 -1
- package/lib/security/zip-security.js +4 -3
- package/lib/sync/ancestor.js +2 -1
- package/lib/sync/autosync-config.d.ts +11 -0
- package/lib/sync/autosync-config.js +12 -0
- package/lib/sync/autosync-scheduler.d.ts +11 -1
- package/lib/sync/autosync-scheduler.js +115 -76
- package/lib/sync/backup-files.js +3 -2
- package/lib/sync/backup-scheduler.js +6 -5
- package/lib/sync/blob-store.js +2 -1
- package/lib/sync/git/git-transport.d.ts +55 -0
- package/lib/sync/git/git-transport.js +152 -2
- package/lib/sync/github-auth.js +10 -9
- package/lib/sync/layout.js +13 -12
- package/lib/sync/messages.d.ts +63 -0
- package/lib/sync/messages.js +149 -0
- package/lib/sync/review-queue.js +6 -5
- package/lib/sync/s3/messages.d.ts +7 -0
- package/lib/sync/s3/messages.js +16 -0
- package/lib/sync/s3/s3-providers.d.ts +4 -1
- package/lib/sync/s3/s3-providers.js +5 -5
- package/lib/sync/s3/s3-transport.js +1 -1
- package/lib/sync/s3/sigv4.js +2 -1
- package/lib/sync/snapshot-crypto.js +6 -5
- package/lib/sync/snapshot-json.js +3 -2
- package/lib/sync/sync-engine.d.ts +37 -15
- package/lib/sync/sync-engine.js +67 -37
- package/lib/sync/sync-history.js +7 -6
- package/lib/sync/sync-selection.d.ts +15 -4
- package/lib/sync/sync-selection.js +17 -5
- package/lib/sync/webdav/webdav-transport.d.ts +1 -0
- package/lib/sync/webdav/webdav-transport.js +4 -1
- package/lib/ui/artifact-view.js +4 -3
- package/lib/ui/commands.d.ts +1 -1
- package/lib/ui/commands.js +10 -0
- package/lib/ui/disk-usage-view.d.ts +33 -4
- package/lib/ui/disk-usage-view.js +29 -1
- package/lib/ui/errors.d.ts +1 -1
- package/lib/ui/errors.js +4 -3
- package/lib/ui/export-flow.js +24 -20
- package/lib/ui/foreign-view.d.ts +5 -1
- package/lib/ui/foreign-view.js +26 -2
- package/lib/ui/i18n.d.ts +65 -0
- package/lib/ui/i18n.js +114 -0
- package/lib/ui/import-wizard.d.ts +7 -0
- package/lib/ui/import-wizard.js +13 -10
- package/lib/ui/progress.d.ts +2 -2
- package/lib/ui/progress.js +1 -2
- package/lib/ui/report.d.ts +5 -5
- package/lib/ui/report.js +6 -6
- package/lib/ui/rollback-view.d.ts +756 -0
- package/lib/ui/rollback-view.js +724 -0
- package/lib/ui/selection-model.d.ts +41 -0
- package/lib/ui/selection-model.js +30 -0
- package/lib/ui/session-inventory-view.d.ts +97 -3
- package/lib/ui/session-inventory-view.js +76 -2
- package/lib/ui/session-issue-labels.d.ts +43 -0
- package/lib/ui/session-issue-labels.js +48 -0
- package/lib/ui/sync-settings-view.d.ts +33 -0
- package/lib/ui/sync-settings-view.js +40 -0
- package/lib/ui/types.d.ts +8 -3
- package/lib/ui/types.js +1 -1
- package/lib/utils/atomic-write.js +6 -5
- package/lib/utils/bundle-scan.d.ts +11 -1
- package/lib/utils/bundle-scan.js +12 -1
- package/lib/utils/env-lock.d.ts +10 -0
- package/lib/utils/env-lock.js +73 -46
- package/lib/utils/json.js +6 -5
- package/lib/utils/messages-utils.d.ts +96 -0
- package/lib/utils/messages-utils.js +216 -0
- package/lib/utils/proxy.js +18 -5
- package/lib/utils/recursive-walk.d.ts +22 -0
- package/lib/utils/recursive-walk.js +75 -1
- package/lib/utils/session-log-repair.js +10 -9
- package/lib/utils/zip.js +27 -26
- package/package.json +12 -2
- package/src/adapters/agent-instructions.ts +1 -1
- package/src/adapters/file-collection.test.ts +1 -0
- package/src/adapters/file-collection.ts +1 -1
- package/src/adapters/files.test.ts +27 -0
- package/src/adapters/index.ts +1 -1
- package/src/adapters/link-report.test.ts +22 -2
- package/src/adapters/link-report.ts +29 -4
- package/src/adapters/messages.ts +54 -0
- package/src/adapters/plugin-files.ts +3 -2
- package/src/adapters/plugins.ts +4 -3
- package/src/adapters/prompts.ts +7 -1
- package/src/adapters/self.test.ts +26 -0
- package/src/adapters/self.ts +8 -2
- package/src/adapters/sessions.ts +4 -2
- package/src/adapters/settings.test.ts +38 -1
- package/src/adapters/ui.test.ts +2 -2
- package/src/adapters/ui.ts +27 -26
- package/src/adapters/workspaces.ts +7 -3
- package/src/cli/actions.test.ts +28 -0
- package/src/cli/actions.ts +11 -2
- package/src/cli/help.ts +23 -10
- package/src/cli/index-guards.test.ts +193 -0
- package/src/cli/index.ts +106 -31
- package/src/cli/sessions-inspect.test.ts +15 -1
- package/src/cli/sessions-inspect.ts +3 -1
- package/src/cli/sessions-repair.test.ts +84 -0
- package/src/cli/sessions-repair.ts +33 -7
- package/src/cli/web/console-route-parity.test.ts +115 -0
- package/src/cli/web/page.ts +23 -12
- package/src/cli/web/routes.ts +2 -1
- package/src/cli/web/server.ts +4 -4
- package/src/cli/web/web.test.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +30 -2
- package/src/client/about/AboutPanel.tsx +2 -2
- package/src/client/about/PluginUpdateDialog.tsx +1 -1
- package/src/client/about/ReleaseNotesDialog.tsx +2 -2
- package/src/client/about/about-view.test.ts +13 -3
- package/src/client/about/about-view.ts +10 -6
- package/src/client/about/release-notes-view.test.ts +14 -4
- package/src/client/about/release-notes-view.ts +4 -2
- package/src/client/api.ts +18 -6
- package/src/client/chat/ChatEntryView.tsx +391 -0
- package/src/client/chat/RollbackDiffTab.tsx +61 -0
- package/src/client/chat/RollbackFileIcon.tsx +96 -0
- package/src/client/chat/RollbackReviewDialog.tsx +591 -0
- package/src/client/chat/chat-entry-view.test.ts +1174 -0
- package/src/client/chat/chat-entry-view.ts +758 -0
- package/src/client/chat/chat-locales.ts +152 -0
- package/src/client/chat/chat-node-registration.ts +64 -0
- package/src/client/chat/chat-point-window.ts +173 -0
- package/src/client/chat/chat-slots.ts +107 -0
- package/src/client/chat/rollback-diff-tab.test.ts +105 -0
- package/src/client/chat/rollback-diff-tab.ts +164 -0
- package/src/client/chat/rollback-entry-view.test.ts +1469 -0
- package/src/client/chat/rollback-entry-view.ts +941 -0
- package/src/client/chat/sidebar-slots.ts +37 -0
- package/src/client/common/ConfirmDialog.tsx +7 -3
- package/src/client/common/ContentPicker.tsx +48 -10
- package/src/client/common/DiffTable.tsx +97 -0
- package/src/client/common/ErrorBanner.tsx +4 -4
- package/src/client/common/Icon.tsx +2 -0
- package/src/client/common/Modal.tsx +8 -1
- package/src/client/common/ProgressBar.tsx +8 -2
- package/src/client/common/ReportView.tsx +4 -4
- package/src/client/common/RunsCenter.tsx +1 -0
- package/src/client/common/client-route-parity.test.ts +78 -0
- package/src/client/common/info-hint-guard.test.ts +46 -13
- package/src/client/common/picker-lazy-units.test.ts +42 -0
- package/src/client/common/plan-text-redaction.test.ts +17 -6
- package/src/client/common/progress-view-edge.test.ts +8 -7
- package/src/client/common/progress-view.test.ts +9 -9
- package/src/client/common/progress-view.ts +2 -2
- package/src/client/common/routes.ts +28 -0
- package/src/client/common/ui.tsx +73 -10
- package/src/client/config-manager.module.css +996 -21
- package/src/client/css-scale-guard.test.ts +30 -0
- package/src/client/environment/EnvironmentPanel.tsx +11 -4
- package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
- package/src/client/export/ExportView.tsx +13 -8
- package/src/client/history/HistoryPanel.tsx +4 -3
- package/src/client/history/history-api.ts +2 -2
- package/src/client/history/history-locales.ts +5 -1
- package/src/client/home/HomePanel.tsx +29 -8
- package/src/client/import/ForeignImportView.tsx +4 -5
- package/src/client/import/ImportWizardView.tsx +5 -3
- package/src/client/import/import-wizard-split.test.ts +3 -1
- package/src/client/import/import-wizard-steps.tsx +9 -9
- package/src/client/index.ts +186 -3
- package/src/client/library/LibraryActions.tsx +32 -7
- package/src/client/library/LibraryPanel.tsx +23 -3
- package/src/client/library/RestorePlanView.tsx +9 -66
- package/src/client/locales.ts +54 -5
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +13 -2
- package/src/client/market/MyConfigsView.tsx +5 -4
- package/src/client/market/MyConfigsWizard.tsx +10 -8
- package/src/client/market/market-api.test.ts +9 -8
- package/src/client/market/market-api.ts +2 -2
- package/src/client/market/market-locales.ts +3 -0
- package/src/client/market/market-view.test.ts +6 -6
- package/src/client/market/market-view.ts +3 -3
- package/src/client/market/my-configs-api.ts +2 -2
- package/src/client/market/my-configs-view.test.ts +23 -22
- package/src/client/market/my-configs-view.ts +8 -8
- package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
- package/src/client/recovery/RecoveryPanel.tsx +442 -283
- package/src/client/recovery/RollbackCard.tsx +665 -0
- package/src/client/recovery/RollbackDiffPane.tsx +58 -0
- package/src/client/recovery/RollbackFileChanges.tsx +163 -0
- package/src/client/recovery/incident-api.ts +2 -2
- package/src/client/recovery/recovery-api.ts +116 -6
- package/src/client/recovery/recovery-locales.test.ts +299 -0
- package/src/client/recovery/recovery-locales.ts +488 -149
- package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
- package/src/client/recovery/rollback-file-changes.test.ts +73 -0
- package/src/client/recovery/session-dialog-mount.test.ts +54 -0
- package/src/client/run-store.ts +84 -21
- package/src/client/sync/AutosyncCard.tsx +25 -1
- package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
- package/src/client/sync/ChannelConfigDialog.tsx +10 -10
- package/src/client/sync/DecryptPasswordCard.tsx +2 -2
- package/src/client/sync/SecurityOptionsCard.tsx +27 -17
- package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
- package/src/client/sync/SyncConfirmView.tsx +46 -8
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +31 -23
- package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
- package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
- package/src/client/sync/SyncSettingsView.test.ts +20 -1
- package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
- package/src/client/sync/WorkDirCard.tsx +112 -0
- package/src/client/sync/history-model.test.ts +42 -9
- package/src/client/sync/history-model.ts +30 -15
- package/src/client/sync/sync-api.test.ts +57 -33
- package/src/client/sync/sync-api.ts +80 -6
- package/src/client/sync/sync-locales.ts +104 -3
- package/src/client/sync/sync-view-v2.test.ts +65 -13
- package/src/client/sync/sync-view.test.ts +107 -52
- package/src/client/sync/sync-view.ts +124 -41
- package/src/core/backup-plan.test.ts +28 -0
- package/src/core/backup-plan.ts +29 -17
- package/src/core/backup-verify.ts +24 -23
- package/src/core/backup.test.ts +76 -0
- package/src/core/backup.ts +55 -25
- package/src/core/boot-rescue.ts +4 -3
- package/src/core/boot-safety.ts +2 -1
- package/src/core/command-rollback.test.ts +536 -0
- package/src/core/command-rollback.ts +402 -0
- package/src/core/consult-source.ts +7 -6
- package/src/core/disk-usage.test.ts +257 -1
- package/src/core/disk-usage.ts +134 -14
- package/src/core/engine-locale.test.ts +593 -0
- package/src/core/engine-locale.ts +175 -0
- package/src/core/exporter.ts +3 -3
- package/src/core/fork-reconcile.test.ts +279 -0
- package/src/core/fork-reconcile.ts +234 -0
- package/src/core/journal.ts +6 -5
- package/src/core/local-plugin-host.ts +3 -2
- package/src/core/local-plugin-pack.ts +11 -6
- package/src/core/messages-core.ts +657 -0
- package/src/core/messages.ts +123 -17
- package/src/core/migration-consult.ts +21 -20
- package/src/core/migration-history.ts +19 -5
- package/src/core/model-tools.ts +28 -27
- package/src/core/phase3-host.ts +21 -5
- package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
- package/src/core/plugin-cli.ts +18 -19
- package/src/core/reconcile.test.ts +117 -0
- package/src/core/reconcile.ts +85 -17
- package/src/core/recovery-orchestrator.ts +19 -18
- package/src/core/reinstall.ts +26 -21
- package/src/core/restore.ts +20 -17
- package/src/core/rollback-file-diff.test.ts +159 -0
- package/src/core/rollback-ignore-rules.ts +126 -0
- package/src/core/rollback-point.test.ts +2603 -0
- package/src/core/rollback-point.ts +3755 -0
- package/src/core/run-registry.ts +6 -5
- package/src/core/session-health.ts +15 -0
- package/src/core/session-meta.test.ts +117 -1
- package/src/core/session-meta.ts +149 -2
- package/src/core/session-title.test.ts +127 -0
- package/src/core/session-title.ts +189 -0
- package/src/core/startup-barrier.test.ts +69 -0
- package/src/core/startup-barrier.ts +27 -0
- package/src/core/transaction-coordinator.ts +4 -3
- package/src/core/types.ts +18 -1
- package/src/core/validator.ts +5 -4
- package/src/core/verify-recovery.ts +31 -30
- package/src/foreign/bundle.ts +3 -2
- package/src/foreign/continue.ts +2 -1
- package/src/foreign/crush.ts +2 -1
- package/src/foreign/file-budget.test.ts +2 -0
- package/src/foreign/gemini.ts +2 -1
- package/src/foreign/goose.ts +2 -1
- package/src/foreign/kilocode.ts +2 -1
- package/src/foreign/kimi.ts +2 -1
- package/src/foreign/messages.ts +44 -0
- package/src/foreign/mimocode.ts +2 -1
- package/src/foreign/openclaw.ts +2 -1
- package/src/foreign/opencode.ts +2 -1
- package/src/foreign/pi.ts +2 -1
- package/src/foreign/qoder.ts +2 -1
- package/src/foreign/qwen.ts +2 -1
- package/src/foreign/registry.ts +7 -6
- package/src/foreign/teleagent.ts +2 -1
- package/src/foreign/workbuddy.ts +2 -1
- package/src/foreign/zcode.ts +2 -1
- package/src/foreign/zed.ts +2 -1
- package/src/index.facade.test.ts +340 -1
- package/src/index.ts +813 -57
- package/src/market/git-file-writer.ts +20 -19
- package/src/market/github-repos.test.ts +23 -0
- package/src/market/github-repos.ts +37 -19
- package/src/market/index-parser.ts +31 -30
- package/src/market/messages-market.ts +325 -0
- package/src/market/my-repo.ts +32 -29
- package/src/market/prepare.ts +18 -15
- package/src/market/security.ts +20 -19
- package/src/market/types.ts +2 -1
- package/src/market/url.ts +2 -1
- package/src/migrations/index.ts +6 -5
- package/src/migrations/messages-migrations.ts +59 -0
- package/src/profiles/dsh-profile-launcher.ts +2 -1
- package/src/profiles/dsh-profile-runtime.ts +2 -1
- package/src/profiles/messages.ts +31 -0
- package/src/routes/backup.ts +18 -2
- package/src/routes/import.ts +3 -3
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +9 -3
- package/src/routes/market.ts +1 -1
- package/src/routes/messages.ts +104 -0
- package/src/routes/profiles.ts +12 -6
- package/src/routes/recovery.test.ts +405 -1
- package/src/routes/recovery.ts +377 -109
- package/src/routes/snapshots.ts +51 -4
- package/src/routes/sync.ts +67 -16
- package/src/schema/config.ts +53 -20
- package/src/schema/index.ts +7 -5
- package/src/schema/manifest.ts +19 -18
- package/src/schema/messages.ts +120 -0
- package/src/schema/registry.test.ts +19 -5
- package/src/schema/section-registry.ts +64 -39
- package/src/schema/versions.ts +6 -5
- package/src/security/encryption.ts +16 -15
- package/src/security/integrity.ts +8 -7
- package/src/security/messages.ts +87 -0
- package/src/security/redaction.test.ts +17 -1
- package/src/security/redaction.ts +13 -0
- package/src/security/secret-scanner.ts +19 -12
- package/src/security/security.test.ts +47 -0
- package/src/security/vault.ts +2 -1
- package/src/security/zip-security.ts +4 -3
- package/src/sync/ancestor.ts +2 -1
- package/src/sync/autosync-config.ts +21 -0
- package/src/sync/autosync-scheduler.test.ts +63 -7
- package/src/sync/autosync-scheduler.ts +118 -78
- package/src/sync/backup-files.ts +3 -2
- package/src/sync/backup-scheduler.ts +6 -5
- package/src/sync/blob-store.ts +2 -1
- package/src/sync/git/git-transport.test.ts +170 -8
- package/src/sync/git/git-transport.ts +176 -2
- package/src/sync/github-auth.ts +10 -9
- package/src/sync/layout.ts +13 -12
- package/src/sync/messages.ts +153 -0
- package/src/sync/review-queue.ts +6 -5
- package/src/sync/s3/messages.ts +16 -0
- package/src/sync/s3/s3-providers.ts +9 -6
- package/src/sync/s3/s3-transport.ts +1 -1
- package/src/sync/s3/sigv4.ts +3 -1
- package/src/sync/snapshot-crypto.ts +6 -5
- package/src/sync/snapshot-json.ts +3 -2
- package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
- package/src/sync/sync-engine.test.ts +226 -8
- package/src/sync/sync-engine.ts +103 -41
- package/src/sync/sync-history.ts +7 -6
- package/src/sync/sync-selection.test.ts +12 -2
- package/src/sync/sync-selection.ts +18 -5
- package/src/sync/webdav/webdav-transport.ts +4 -1
- package/src/ui/artifact-view.ts +4 -3
- package/src/ui/commands.test.ts +1 -0
- package/src/ui/commands.ts +11 -1
- package/src/ui/disk-usage-view.test.ts +113 -6
- package/src/ui/disk-usage-view.ts +63 -7
- package/src/ui/errors.ts +4 -3
- package/src/ui/export-flow.ts +24 -20
- package/src/ui/foreign-view.test.ts +43 -0
- package/src/ui/foreign-view.ts +30 -3
- package/src/ui/i18n.ts +125 -0
- package/src/ui/import-wizard.ts +18 -10
- package/src/ui/progress.ts +2 -2
- package/src/ui/report.ts +7 -6
- package/src/ui/rollback-view.test.ts +819 -0
- package/src/ui/rollback-view.ts +1339 -0
- package/src/ui/selection-model.test.ts +33 -1
- package/src/ui/selection-model.ts +53 -0
- package/src/ui/session-inventory-view.test.ts +109 -0
- package/src/ui/session-inventory-view.ts +138 -3
- package/src/ui/session-issue-labels.test.ts +51 -0
- package/src/ui/session-issue-labels.ts +70 -0
- package/src/ui/sync-settings-view.test.ts +36 -0
- package/src/ui/sync-settings-view.ts +49 -0
- package/src/ui/types.ts +10 -4
- package/src/utils/atomic-write.ts +6 -5
- package/src/utils/bundle-scan.test.ts +2 -2
- package/src/utils/bundle-scan.ts +12 -1
- package/src/utils/env-lock.ts +83 -49
- package/src/utils/json.ts +7 -5
- package/src/utils/messages-utils.ts +232 -0
- package/src/utils/proxy.test.ts +25 -0
- package/src/utils/proxy.ts +19 -5
- package/src/utils/recursive-walk.test.ts +43 -0
- package/src/utils/recursive-walk.ts +75 -1
- package/src/utils/session-log-repair.ts +10 -9
- package/src/utils/zip.ts +27 -26
- package/lib/core/checkpoint.d.ts +0 -362
- package/lib/core/checkpoint.js +0 -1309
- package/src/core/checkpoint.test.ts +0 -665
- package/src/core/checkpoint.ts +0 -1614
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* security 层的领域消息目录(zh 源 / en 镜像)—— 经 engineMsg 取「调用期」语言文案。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是本域独立目录:src/security/** 的写作用域不含 src/core/messages.ts(既有键有冻结指纹),
|
|
5
|
+
* 所以本域的键用 registerMessageCatalog 在**模块加载时**注册进引擎目录,查找顺序见
|
|
6
|
+
* src/core/engine-locale.ts(基础目录 → 注册目录 → 键名)。
|
|
7
|
+
*
|
|
8
|
+
* 纪律(与 src/core/messages.ts 同口径):
|
|
9
|
+
* - zh 值 = 迁移前的既有中文文案**逐字不变**(既有测试断言依赖);
|
|
10
|
+
* - en 类型为 Record<keyof typeof zh, string> —— 缺键/多键即编译错误;
|
|
11
|
+
* - {param} 插值与缺参保留由 engineMsg 负责;
|
|
12
|
+
* - **凭据 / 密码值永不进消息**:这里只换文案,脱敏口径与拼接顺序逐字不变(转写前长什么样,
|
|
13
|
+
* 转写后还是什么样),绝不新增任何值参数。
|
|
14
|
+
*/
|
|
15
|
+
import { engineMsg, registerMessageCatalog } from '../core/engine-locale.ts';
|
|
16
|
+
import type { MsgParams } from '../core/msg-types.ts';
|
|
17
|
+
|
|
18
|
+
export const zh = {
|
|
19
|
+
// ---------- 加密容器(encryption.ts) ----------
|
|
20
|
+
'security.encryptionPasswordEmpty': '加密密码不能为空',
|
|
21
|
+
'security.secretsBlobTooSmall': 'secrets.enc 体积过小(截断或损坏)',
|
|
22
|
+
'security.unknownFileMagic': '未知文件格式 magic "{magic}",不是本插件产物',
|
|
23
|
+
'security.unsupportedSecretsVersion': '不支持的 secrets.enc 版本 {version}',
|
|
24
|
+
'security.invalidEncryptionParams': 'manifest 加密参数非法或不受支持',
|
|
25
|
+
'security.encryptionParamsMismatch': 'secrets.enc 与 manifest 加密参数不一致(可能被篡改)',
|
|
26
|
+
'security.keyDerivationFailed': '密钥派生失败: {reason}',
|
|
27
|
+
'security.decryptAuthFailed': '解密认证失败:密码错误或密文被篡改',
|
|
28
|
+
'security.archiveBlobTooSmall': '加密备份容器体积过小(截断或损坏)',
|
|
29
|
+
'security.unsupportedArchiveVersion': '不支持的加密备份版本 {version}',
|
|
30
|
+
'security.archiveParamsMismatch': '加密备份参数不一致(可能被篡改)',
|
|
31
|
+
|
|
32
|
+
// ---------- 完整性校验表(integrity.ts) ----------
|
|
33
|
+
'security.checksumsMustBeJsonObject': 'checksums.json 必须是 JSON 对象',
|
|
34
|
+
'security.checksumsTooManyEntries': 'checksums.json 条目数 {count} 超过上限 {max}',
|
|
35
|
+
'security.checksumsDangerousKey': 'checksums.json 含危险键 "{path}",已拒绝',
|
|
36
|
+
'security.checksumsIllegalPath': 'checksums.json 含非法相对路径 "{path}"',
|
|
37
|
+
'security.checksumsNotSha256': 'checksums.json["{path}"] 不是合法的 SHA-256 hex',
|
|
38
|
+
'security.checksumMismatch': '"{path}" (hash 不符)',
|
|
39
|
+
'security.checksumMissing': '"{path}" (缺失)',
|
|
40
|
+
|
|
41
|
+
// ---------- 秘密扫描(secret-scanner.ts) ----------
|
|
42
|
+
'security.scanDepthExceeded': '扫描深度 {depth} 超过上限 {max}',
|
|
43
|
+
'security.circularReference': '检测到循环引用,无法扫描(路径 {path})',
|
|
44
|
+
|
|
45
|
+
// ---------- vault / ZIP 安全(vault.ts / zip-security.ts) ----------
|
|
46
|
+
'security.vaultUnsafePath': 'vault: 非法敏感文件路径 "{path}"',
|
|
47
|
+
'security.zipTargetOutOfBounds': '解压目标越界: {name}',
|
|
48
|
+
'security.zipExecutableEntry': '条目 "{name}" 是潜在可执行文件,本插件不会执行它',
|
|
49
|
+
'security.zipEntryNotRegularFile': '解压产物不是普通文件: {rel}',
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
export const en: Record<keyof typeof zh, string> = {
|
|
53
|
+
'security.encryptionPasswordEmpty': 'Encryption password must not be empty',
|
|
54
|
+
'security.secretsBlobTooSmall': 'secrets.enc is too small (truncated or corrupted)',
|
|
55
|
+
'security.unknownFileMagic': 'Unknown file format magic "{magic}"; not a product of this plugin',
|
|
56
|
+
'security.unsupportedSecretsVersion': 'Unsupported secrets.enc version {version}',
|
|
57
|
+
'security.invalidEncryptionParams': 'Invalid or unsupported manifest encryption parameters',
|
|
58
|
+
'security.encryptionParamsMismatch': 'secrets.enc encryption parameters do not match the manifest (possibly tampered with)',
|
|
59
|
+
'security.keyDerivationFailed': 'Key derivation failed: {reason}',
|
|
60
|
+
'security.decryptAuthFailed': 'Decryption authentication failed: wrong password or tampered ciphertext',
|
|
61
|
+
'security.archiveBlobTooSmall': 'Encrypted backup container is too small (truncated or corrupted)',
|
|
62
|
+
'security.unsupportedArchiveVersion': 'Unsupported encrypted backup version {version}',
|
|
63
|
+
'security.archiveParamsMismatch': 'Encrypted backup parameters do not match (possibly tampered with)',
|
|
64
|
+
|
|
65
|
+
'security.checksumsMustBeJsonObject': 'checksums.json must be a JSON object',
|
|
66
|
+
'security.checksumsTooManyEntries': 'checksums.json has {count} entries, exceeding the limit of {max}',
|
|
67
|
+
'security.checksumsDangerousKey': 'checksums.json contains a dangerous key "{path}", rejected',
|
|
68
|
+
'security.checksumsIllegalPath': 'checksums.json contains an illegal relative path "{path}"',
|
|
69
|
+
'security.checksumsNotSha256': 'checksums.json["{path}"] is not a valid SHA-256 hex string',
|
|
70
|
+
'security.checksumMismatch': '"{path}" (hash mismatch)',
|
|
71
|
+
'security.checksumMissing': '"{path}" (missing)',
|
|
72
|
+
|
|
73
|
+
'security.scanDepthExceeded': 'Scan depth {depth} exceeds the limit of {max}',
|
|
74
|
+
'security.circularReference': 'Circular reference detected, cannot scan (path {path})',
|
|
75
|
+
|
|
76
|
+
'security.vaultUnsafePath': 'vault: illegal sensitive file path "{path}"',
|
|
77
|
+
'security.zipTargetOutOfBounds': 'Extraction target escapes the destination: {name}',
|
|
78
|
+
'security.zipExecutableEntry': 'Entry "{name}" is a potential executable; this plugin will not execute it',
|
|
79
|
+
'security.zipEntryNotRegularFile': 'Extracted entry is not a regular file: {rel}',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
registerMessageCatalog({ id: 'security', zh, en });
|
|
83
|
+
|
|
84
|
+
/** 本域翻译器:key 受目录约束(拼错键 = 编译错误),语言按 engineMsg 的活语言解析。 */
|
|
85
|
+
export function securityMsg(key: keyof typeof zh, params?: MsgParams): string {
|
|
86
|
+
return engineMsg(key, params);
|
|
87
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import test from 'node:test';
|
|
19
19
|
import assert from 'node:assert/strict';
|
|
20
20
|
|
|
21
|
-
import { REDACTION_VALUE_PATTERNS, REDACTED, redact } from './redaction.ts';
|
|
21
|
+
import { REDACTION_VALUE_PATTERNS, REDACTED, createRedactor, redact } from './redaction.ts';
|
|
22
22
|
import { SECRET_VALUE_PATTERNS, matchSecretValuePattern } from './secret-scanner.ts';
|
|
23
23
|
|
|
24
24
|
/** 真实凭据形态语料(scanner 必须命中;redact 必须掩掉原文) */
|
|
@@ -147,3 +147,19 @@ test('t78-f(**t83 后范围收窄**):显示层「更宽」只对未加形
|
|
|
147
147
|
assert.equal(matchSecretValuePattern('Bearer example-token-here'), null, 'scanner 侧放行(示例形态)');
|
|
148
148
|
assert.ok(redact('Bearer example-token-here').includes(REDACTED), 'redact 侧仍掩(显示层更宽,未变)');
|
|
149
149
|
});
|
|
150
|
+
|
|
151
|
+
/* ---------------- URL 内嵌凭据(userinfo 形态)必须掩码 ---------------- */
|
|
152
|
+
|
|
153
|
+
test('redact:scheme://user:password@host 的密码段被掩码(用户名保留),且幂等', () => {
|
|
154
|
+
const out = redact('clone https://oauth2:GLPAT-SECRETVALUE@gitlab.example.com/u/r.git now');
|
|
155
|
+
assert.equal(out.includes('GLPAT-SECRETVALUE'), false, 'URL 内嵌凭据不得明文保留');
|
|
156
|
+
assert.ok(out.includes('oauth2'), '用户名保留可读(与 URL_QUERY_RE 同一取舍)');
|
|
157
|
+
assert.ok(out.includes('gitlab.example.com'), '主机名不受影响');
|
|
158
|
+
assert.equal(redact(out), out, '幂等:重复脱敏结果不变');
|
|
159
|
+
// query 形态的既有行为不变
|
|
160
|
+
assert.equal(redact('https://h/p?token=abc123456').includes('abc123456'), false, 'query token 仍被掩码');
|
|
161
|
+
// 没有 userinfo 的普通 URL 一字不改
|
|
162
|
+
assert.equal(redact('https://gitlab.example.com/u/r.git'), 'https://gitlab.example.com/u/r.git');
|
|
163
|
+
// createRedactor 与 redact 同源(同一份规则)
|
|
164
|
+
assert.equal(createRedactor()('https://u:p4ssword@h/x').includes('p4ssword'), false);
|
|
165
|
+
});
|
|
@@ -54,6 +54,17 @@ const VALUE_PATTERNS = REDACTION_VALUE_PATTERNS;
|
|
|
54
54
|
/** URL query 中的敏感参数值(保留参数名,只替换值) */
|
|
55
55
|
const URL_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|access[_-]?token|password|auth)=)([^&\s"']+)/gi;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* URL **内嵌凭据**(\`scheme://user:password@host\`,GitLab/Gitea 常见的 \`https://oauth2:<token>@…\`)。
|
|
59
|
+
*
|
|
60
|
+
* 为什么必须补这一条:URL_QUERY_RE 只认 query 参数,而 userinfo 形态的凭据此前在**三条通道**
|
|
61
|
+
* 都不被掩码 —— 日志、界面渲染、以及 sessionStorage 落盘(run-store 的 \`redactPersistedValue\`)。
|
|
62
|
+
* 宿主侧 sync-config 的 validateRepoUrl/validateWebDavUrl 本来就拒绝带 userinfo 的地址,
|
|
63
|
+
* 所以这里只影响「掩码」:不改任何合法输入的行为,只让非法规格里的凭据不再明文可见。
|
|
64
|
+
* 只替换密码段(用户名保留可读),与 URL_QUERY_RE 的取舍一致。
|
|
65
|
+
*/
|
|
66
|
+
const URL_USERINFO_RE = /(\/\/[^\/@\s:]+:)([^\/@\s]+)(@)/g;
|
|
67
|
+
|
|
57
68
|
/* ---------------- 高熵长 token(journal / 迁移历史强脱敏共用) ---------------- */
|
|
58
69
|
|
|
59
70
|
/** 高熵值形状(长 hex/base64/随机 id):结构化 redact() 覆盖不了任意 secret,journal / 迁移
|
|
@@ -120,6 +131,7 @@ export function redact(text: string, blacklist: readonly string[] = []): string
|
|
|
120
131
|
out = out.replace(re, REDACTED);
|
|
121
132
|
}
|
|
122
133
|
out = out.replace(URL_QUERY_RE, `$1${REDACTED}`);
|
|
134
|
+
out = out.replace(URL_USERINFO_RE, `$1${REDACTED}$3`);
|
|
123
135
|
return out;
|
|
124
136
|
}
|
|
125
137
|
|
|
@@ -147,6 +159,7 @@ export function createRedactor(blacklist: readonly string[] = []): (text: string
|
|
|
147
159
|
out = out.replace(re, REDACTED);
|
|
148
160
|
}
|
|
149
161
|
out = out.replace(URL_QUERY_RE, `$1${REDACTED}`);
|
|
162
|
+
out = out.replace(URL_USERINFO_RE, `$1${REDACTED}$3`);
|
|
150
163
|
return out;
|
|
151
164
|
};
|
|
152
165
|
}
|
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
* - 高熵 base64 默认关(误伤率高)。
|
|
23
23
|
*
|
|
24
24
|
* 实现不变量:返回 sanitized 为**全新对象**(不改原数据);迭代式显式栈(防爆栈)+ 深度上限;
|
|
25
|
+
* 判环只看**当前路径的祖先链**(`ancestors`)——同一对象被两处引用(DAG)不是环,用全局 visited
|
|
26
|
+
* 会把共享引用误判成循环引用并让整次导出失败;
|
|
25
27
|
* 命中记录只含路径与字段名,**值永不外泄**。
|
|
26
28
|
*/
|
|
27
29
|
import { JsonDepthError } from '../utils/json.ts';
|
|
28
30
|
import type { SecretScanner, SensitiveHit } from '../core/types.ts';
|
|
31
|
+
import { securityMsg } from './messages.ts';
|
|
29
32
|
|
|
30
33
|
/** 剥离占位(与 core defaultSecretScanner 语义一致:空串 = 需补录提示) */
|
|
31
34
|
export const REDACTED_PLACEHOLDER = '';
|
|
@@ -351,15 +354,17 @@ export function scanAndRedact(data: unknown, opts: SecretScannerOptions = {}): {
|
|
|
351
354
|
if (data instanceof Uint8Array) return { sanitized: data, hits };
|
|
352
355
|
|
|
353
356
|
const rootOut: unknown = Array.isArray(data) ? [] : {};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
// 判环判据 = **当前路径的祖先链**,绝不是「本次扫描见过的对象集合」:settings 的 value/base 共享
|
|
358
|
+
// 未覆盖的嵌套分组、providers 的多 route 共享 raw 等 DAG 形态都是合法数据,全局 visited 会把它们
|
|
359
|
+
// 误判成「检测到循环引用」并让整次导出/同步失败(用户上报 2026-10-09:备份失败 → intent journal
|
|
360
|
+
// 记 NEEDS_ATTENTION + durable SAFE MODE)。祖先链只回答「它是不是它自己的祖先」。
|
|
361
|
+
interface Frame { src: unknown; dst: unknown; path: string; depth: number; ancestors: ReadonlySet<object> }
|
|
362
|
+
const stack: Frame[] = [{ src: data, dst: rootOut, path: '', depth: 1, ancestors: new Set<object>([data as object]) }];
|
|
358
363
|
|
|
359
364
|
while (stack.length > 0) {
|
|
360
365
|
const frame = stack.pop()!;
|
|
361
|
-
const { src, dst, path, depth } = frame;
|
|
362
|
-
if (depth > maxDepth) throw new JsonDepthError(
|
|
366
|
+
const { src, dst, path, depth, ancestors } = frame;
|
|
367
|
+
if (depth > maxDepth) throw new JsonDepthError(securityMsg('security.scanDepthExceeded', { depth, max: maxDepth }));
|
|
363
368
|
if (src instanceof Uint8Array) continue;
|
|
364
369
|
|
|
365
370
|
if (Array.isArray(src)) {
|
|
@@ -368,11 +373,12 @@ export function scanAndRedact(data: unknown, opts: SecretScannerOptions = {}): {
|
|
|
368
373
|
const v = src[i];
|
|
369
374
|
const childPath = `${path}[${i}]`;
|
|
370
375
|
if (v !== null && typeof v === 'object' && !(v instanceof Uint8Array)) {
|
|
371
|
-
if (
|
|
372
|
-
|
|
376
|
+
if (ancestors.has(v)) throw new Error(securityMsg('security.circularReference', { path: childPath }));
|
|
377
|
+
const childAncestors = new Set(ancestors);
|
|
378
|
+
childAncestors.add(v);
|
|
373
379
|
const childOut: unknown = Array.isArray(v) ? [] : {};
|
|
374
380
|
outArr.push(childOut);
|
|
375
|
-
stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1 });
|
|
381
|
+
stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1, ancestors: childAncestors });
|
|
376
382
|
} else {
|
|
377
383
|
outArr.push(v);
|
|
378
384
|
}
|
|
@@ -393,11 +399,12 @@ export function scanAndRedact(data: unknown, opts: SecretScannerOptions = {}): {
|
|
|
393
399
|
continue;
|
|
394
400
|
}
|
|
395
401
|
if (v !== null && typeof v === 'object' && !(v instanceof Uint8Array)) {
|
|
396
|
-
if (
|
|
397
|
-
|
|
402
|
+
if (ancestors.has(v)) throw new Error(securityMsg('security.circularReference', { path: childPath }));
|
|
403
|
+
const childAncestors = new Set(ancestors);
|
|
404
|
+
childAncestors.add(v);
|
|
398
405
|
const childOut: unknown = Array.isArray(v) ? [] : {};
|
|
399
406
|
outObj[k] = childOut;
|
|
400
|
-
stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1 });
|
|
407
|
+
stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1, ancestors: childAncestors });
|
|
401
408
|
} else {
|
|
402
409
|
outObj[k] = v;
|
|
403
410
|
}
|
|
@@ -727,6 +727,53 @@ test('scanner: 深层嵌套上限(≤64 通过,超限抛错)与循环引
|
|
|
727
727
|
assert.throws(() => scanAndRedact(cyclic), /循环引用/);
|
|
728
728
|
});
|
|
729
729
|
|
|
730
|
+
test('scanner: DAG 共享引用不得被误判为循环(用户上报 2026-10-09:settings value/base 共享 → 备份整体失败)', () => {
|
|
731
|
+
// 同一对象被两处引用(彼此不是祖先关系)是合法数据:settings 的 value/base 共享未覆盖的嵌套分组、
|
|
732
|
+
// providers 的多 route 共享 raw 都长这样。判环只看祖先链 → 放行,且两个分支各自完成脱敏。
|
|
733
|
+
const dream = { enabled: true, idleMinutes: 180, apiKey: 'sk-live-abcdefgh12345' };
|
|
734
|
+
const data = {
|
|
735
|
+
namespaces: {
|
|
736
|
+
'meow-memory': {
|
|
737
|
+
value: { promptLang: 'zh', dream },
|
|
738
|
+
base: { promptLang: 'zh', dream },
|
|
739
|
+
revision: 3,
|
|
740
|
+
secrets: [],
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
const { sanitized, hits } = scanAndRedact(data);
|
|
745
|
+
const rec = (sanitized as {
|
|
746
|
+
namespaces: Record<string, { value: { dream: Record<string, unknown> }; base: { dream: Record<string, unknown> } }>;
|
|
747
|
+
}).namespaces['meow-memory']!;
|
|
748
|
+
assert.equal(rec.value.dream['apiKey'], REDACTED_PLACEHOLDER, 'value 分支必须脱敏');
|
|
749
|
+
assert.equal(rec.base.dream['apiKey'], REDACTED_PLACEHOLDER, 'base 分支必须脱敏');
|
|
750
|
+
assert.equal(rec.value.dream['idleMinutes'], 180, '非敏感字段保留');
|
|
751
|
+
assert.equal(rec.base.dream['idleMinutes'], 180);
|
|
752
|
+
assert.notStrictEqual(rec.value.dream, rec.base.dream, '输出必须是各自独立的副本');
|
|
753
|
+
assert.ok(hits.some((h) => h.path === 'namespaces.meow-memory.value.dream.apiKey'), '命中路径须点名 value 分支');
|
|
754
|
+
assert.ok(hits.some((h) => h.path === 'namespaces.meow-memory.base.dream.apiKey'), '命中路径须点名 base 分支');
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test('scanner: 数组分支的共享引用放行,真环(对象环 / 数组环 / 深层回指)仍必须抛', () => {
|
|
758
|
+
const shared = { token: 'ghp_abcdefghijklmnopqrstuvwxyz012345' };
|
|
759
|
+
const { sanitized } = scanAndRedact({ a: { x: shared }, b: { y: shared }, c: { z: shared } });
|
|
760
|
+
const s = sanitized as { a: { x: { token: string } }; b: { y: { token: string } }; c: { z: { token: string } } };
|
|
761
|
+
assert.equal(s.a.x.token, REDACTED_PLACEHOLDER);
|
|
762
|
+
assert.equal(s.b.y.token, REDACTED_PLACEHOLDER);
|
|
763
|
+
assert.equal(s.c.z.token, REDACTED_PLACEHOLDER);
|
|
764
|
+
assert.notStrictEqual(s.a.x, s.b.y, '三处共享引用必须各写一份独立副本');
|
|
765
|
+
|
|
766
|
+
// 数组自环
|
|
767
|
+
const arrayCycle: unknown[] = [1];
|
|
768
|
+
arrayCycle.push(arrayCycle);
|
|
769
|
+
assert.throws(() => scanAndRedact({ x: arrayCycle }), /循环引用/);
|
|
770
|
+
|
|
771
|
+
// 同一对象既是自己的祖先(隔着若干层回指)→ 仍必须抛,不能因为「见过」就放行
|
|
772
|
+
const root: Record<string, unknown> = {};
|
|
773
|
+
root['deep'] = { mid: { back: root } };
|
|
774
|
+
assert.throws(() => scanAndRedact(root), /循环引用/);
|
|
775
|
+
});
|
|
776
|
+
|
|
730
777
|
test('scanner: Uint8Array 跳过、数组根、空字符串不报 hit、extra 名单扩展', () => {
|
|
731
778
|
const bin = new Uint8Array([1, 2, 3]);
|
|
732
779
|
const input = { data: bin, empty: '', clientSecret2: 'x', normal: 42 };
|
package/src/security/vault.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import type { FileSystemFacade } from '../core/types.ts';
|
|
25
25
|
import { isPathSafe, normalizePath } from '../utils/paths.ts';
|
|
26
|
+
import { securityMsg } from './messages.ts';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* 默认敏感文件清单(相对 $DSH_HOME):当前 DSH 唯一「整文件即秘密」的文件
|
|
@@ -68,7 +69,7 @@ export interface VaultEntry {
|
|
|
68
69
|
|
|
69
70
|
/** rel 必须为安全相对路径(拒绝绝对路径 / .. / 盘符 / NUL),防路径穿越 */
|
|
70
71
|
function assertSafeRel(rel: string): void {
|
|
71
|
-
if (!isPathSafe(rel)) throw new Error(
|
|
72
|
+
if (!isPathSafe(rel)) throw new Error(securityMsg('security.vaultUnsafePath', { path: rel }));
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/** 校验 + 归一化 rel 清单(统一 / 分隔,与 FileSystemFacade.listRecursive 产出对齐) */
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ZipSafetyError,
|
|
22
22
|
type ZipSafetyLimits,
|
|
23
23
|
} from '../utils/zip.ts';
|
|
24
|
+
import { securityMsg } from './messages.ts';
|
|
24
25
|
|
|
25
26
|
export { isPathSafe } from '../utils/paths.ts';
|
|
26
27
|
|
|
@@ -83,13 +84,13 @@ export async function safeExtractHardened(
|
|
|
83
84
|
if (meta.isDirectory) continue;
|
|
84
85
|
const target = path.join(destDir, ...meta.name.split('/'));
|
|
85
86
|
if (!isSameOrChild(target, destDir)) {
|
|
86
|
-
throw new ZipSafetyError(
|
|
87
|
+
throw new ZipSafetyError(securityMsg('security.zipTargetOutOfBounds', { name: meta.name }));
|
|
87
88
|
}
|
|
88
89
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
89
90
|
await fs.writeFile(target, archive.readEntry(meta.name));
|
|
90
91
|
extracted.push(meta.name);
|
|
91
92
|
if (isExecutableName(meta.name)) {
|
|
92
|
-
warnings.push(
|
|
93
|
+
warnings.push(securityMsg('security.zipExecutableEntry', { name: meta.name }));
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
// 解压后复查:全部产物必须是普通文件(防 symlink 逃逸 / 非常规写入)
|
|
@@ -97,7 +98,7 @@ export async function safeExtractHardened(
|
|
|
97
98
|
const p = path.join(destDir, ...rel.split('/'));
|
|
98
99
|
const st = await fs.lstat(p);
|
|
99
100
|
if (!st.isFile()) {
|
|
100
|
-
throw new ZipSafetyError(
|
|
101
|
+
throw new ZipSafetyError(securityMsg('security.zipEntryNotRegularFile', { rel }));
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
return { files: extracted, warnings };
|
package/src/sync/ancestor.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 纯逻辑 + 可注入 fs;不触碰真实 ~/.dsh。
|
|
5
5
|
*/
|
|
6
6
|
import { createSnapshotFs, joinFs } from './fs.ts';
|
|
7
|
+
import { engineMsg } from './messages.ts';
|
|
7
8
|
import type { SnapshotFs } from './fs.ts';
|
|
8
9
|
import { readSnapshotFromDir, writeSnapshotToDir } from './layout.ts';
|
|
9
10
|
import { SNAPSHOT_MANIFEST_FILE } from './layout.ts';
|
|
@@ -19,7 +20,7 @@ export async function loadAncestor(
|
|
|
19
20
|
fsx: SnapshotFs = createSnapshotFs(),
|
|
20
21
|
): Promise<SyncSnapshot> {
|
|
21
22
|
if (typeof snapshotId !== 'string' || snapshotId === '') {
|
|
22
|
-
throw new Error('
|
|
23
|
+
throw new Error(engineMsg('sync.ancestorSnapshotIdRequired'));
|
|
23
24
|
}
|
|
24
25
|
const dir = joinFs(localSnapshotsDir, snapshotId);
|
|
25
26
|
return await readSnapshotFromDir(dir, fsx);
|
|
@@ -52,6 +52,17 @@ export interface AutosyncConfig {
|
|
|
52
52
|
/** 最近一次自动同步执行状态 */
|
|
53
53
|
lastRunStatus?: AutosyncRunStatus;
|
|
54
54
|
lastRunMessage?: string;
|
|
55
|
+
/**
|
|
56
|
+
* 最近一次**跳过**的结构化原因码(issue #79)。
|
|
57
|
+
*
|
|
58
|
+
* 为什么是码而不是文案:语言在「取文案那一刻」解析(AGENTS.md i18n 铁律),状态行由界面
|
|
59
|
+
* 按码取自己的字典;存一句当时语言的中文,用户切到英文界面后会看到中英混排。
|
|
60
|
+
* 已知值见 `svc.autosyncSkip.*` 的字典键('conflict'/'mutation-locked'/'upToDate'…),
|
|
61
|
+
* **未知码一律渲染兜底文案,绝不渲染裸枚举**。
|
|
62
|
+
*/
|
|
63
|
+
lastRunSkipReason?: string;
|
|
64
|
+
/** 最近一次跳过涉及的分区数(当前仅 conflict:远端另有 N 个分区待确认) */
|
|
65
|
+
lastRunSkipCount?: number;
|
|
55
66
|
/** 最近一次自动同步触发的同步历史条目 id */
|
|
56
67
|
lastRunHistoryId?: string;
|
|
57
68
|
}
|
|
@@ -85,6 +96,13 @@ function parseV1Channel(obj: Record<string, unknown>): AutosyncConfig {
|
|
|
85
96
|
cfg.lastRunStatus = obj['lastRunStatus'];
|
|
86
97
|
}
|
|
87
98
|
if (typeof obj['lastRunMessage'] === 'string') cfg.lastRunMessage = obj['lastRunMessage'];
|
|
99
|
+
// issue #79:状态行的结构化跳过原因(旧文件没有这两个字段 → undefined,行为不变)
|
|
100
|
+
if (typeof obj['lastRunSkipReason'] === 'string' && obj['lastRunSkipReason'] !== '') {
|
|
101
|
+
cfg.lastRunSkipReason = obj['lastRunSkipReason'];
|
|
102
|
+
}
|
|
103
|
+
if (typeof obj['lastRunSkipCount'] === 'number' && Number.isFinite(obj['lastRunSkipCount']) && obj['lastRunSkipCount'] >= 0) {
|
|
104
|
+
cfg.lastRunSkipCount = obj['lastRunSkipCount'];
|
|
105
|
+
}
|
|
88
106
|
if (typeof obj['lastRunHistoryId'] === 'string') cfg.lastRunHistoryId = obj['lastRunHistoryId'];
|
|
89
107
|
return cfg;
|
|
90
108
|
}
|
|
@@ -161,6 +179,9 @@ function toChannelPayload(cfg: AutosyncConfig): Record<string, unknown> {
|
|
|
161
179
|
if (cfg.lastRunAt !== undefined && cfg.lastRunAt !== '') payload['lastRunAt'] = cfg.lastRunAt;
|
|
162
180
|
if (cfg.lastRunStatus !== undefined) payload['lastRunStatus'] = cfg.lastRunStatus;
|
|
163
181
|
if (cfg.lastRunMessage !== undefined && cfg.lastRunMessage !== '') payload['lastRunMessage'] = cfg.lastRunMessage;
|
|
182
|
+
// 空串/undefined 一律不写 → 本轮成功时,上一轮的跳过码/旧消息在文件里被真正清掉(issue #79)
|
|
183
|
+
if (cfg.lastRunSkipReason !== undefined && cfg.lastRunSkipReason !== '') payload['lastRunSkipReason'] = cfg.lastRunSkipReason;
|
|
184
|
+
if (cfg.lastRunSkipCount !== undefined) payload['lastRunSkipCount'] = cfg.lastRunSkipCount;
|
|
164
185
|
if (cfg.lastRunHistoryId !== undefined && cfg.lastRunHistoryId !== '') payload['lastRunHistoryId'] = cfg.lastRunHistoryId;
|
|
165
186
|
return payload;
|
|
166
187
|
}
|
|
@@ -283,8 +283,10 @@ test('runOnce: webdav 未配置(webdav.url 缺)→ skipped(unconfigured),
|
|
|
283
283
|
const result = await scheduler.runOnce('webdav');
|
|
284
284
|
assert.equal(result.status, 'skipped');
|
|
285
285
|
assert.equal(result.skipReason, 'unconfigured');
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
// issue #79:连续失败计数只统计「真的失败」——未配置属跳过,必须归零(此前保持 1,
|
|
287
|
+
// 面板于是长期显示「连续失败 N 次」,把用户引向网络排查)
|
|
288
|
+
assert.equal(result.consecutiveFailures, 0, '未配置不算失败 → 计数归零');
|
|
289
|
+
assert.equal(getConfig().consecutiveFailures, 0);
|
|
288
290
|
});
|
|
289
291
|
|
|
290
292
|
test('syncIsConfigured: git 看 git.repoUrl、webdav 看 webdav.url;null 未配置', () => {
|
|
@@ -306,19 +308,73 @@ test('runOnce: merge 抛错 → failed,连续失败计数 +1', async () => {
|
|
|
306
308
|
assert.ok(getEntries().some((e) => e.status === 'failed'), '写入失败历史');
|
|
307
309
|
});
|
|
308
310
|
|
|
309
|
-
test('
|
|
311
|
+
test('issue #79:远端冲突只挡下行 —— 不写本地,但仍推送本机改动(partial,不谎报 success/failed)', async () => {
|
|
310
312
|
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 1 };
|
|
313
|
+
const applyCalls: string[] = [];
|
|
314
|
+
const pushCalls: string[] = [];
|
|
311
315
|
const engine = {
|
|
316
|
+
hasNewRemoteSnapshot: async () => true,
|
|
317
|
+
hasLocalChanges: async () => true,
|
|
312
318
|
merge: async (): Promise<MergePlan> => makeMergePlan([['settings', 'conflict']]),
|
|
319
|
+
applyMergePlan: async () => {
|
|
320
|
+
applyCalls.push('apply');
|
|
321
|
+
return { ok: true, applied: [] as never[], restoreId: 'r1', rolledBack: false, review: [], warnings: [] };
|
|
322
|
+
},
|
|
323
|
+
push: async () => { pushCalls.push('push'); return { ok: true, snapshotId: 'snap-up', sections: [] as never, warnings: [] }; },
|
|
313
324
|
};
|
|
314
325
|
const { scheduler, getConfig, getEntries } = makeScheduler({ cfg, engine, history: [] });
|
|
315
326
|
const result = await scheduler.runOnce('git');
|
|
316
|
-
assert.equal(result.status, '
|
|
327
|
+
assert.equal(result.status, 'partial', '上行成功 + 下行待确认 → partial');
|
|
317
328
|
assert.equal(result.skipReason, 'conflict');
|
|
318
329
|
assert.deepEqual(result.conflictedSections, ['settings']);
|
|
319
|
-
assert.equal(result.
|
|
320
|
-
assert.equal(
|
|
321
|
-
assert.
|
|
330
|
+
assert.equal(result.pushedSnapshotId, 'snap-up', '本机快照仍有上行出口(issue #79 主诉)');
|
|
331
|
+
assert.equal(applyCalls.length, 0, '冲突绝不写本机配置');
|
|
332
|
+
assert.equal(pushCalls.length, 1, '纯上行不被冲突连坐');
|
|
333
|
+
assert.equal(result.consecutiveFailures, 0, '冲突跳过不是失败 → 计数归零');
|
|
334
|
+
assert.equal(getConfig().lastRunSkipReason, 'conflict', '跳过原因落到状态(界面按码取字典)');
|
|
335
|
+
assert.equal(getConfig().lastRunSkipCount, 1);
|
|
336
|
+
assert.ok(getEntries().some((e) => e.status === 'partial' && e.skipReason === 'conflict'), '写入 partial 历史');
|
|
337
|
+
assert.ok(getEntries().some((e) => e.pushedSnapshotId === 'snap-up'), '历史带上行快照 id');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('issue #79:冲突且本机无改动(非 startup)→ skipped(conflict) 且不带 push', async () => {
|
|
341
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
342
|
+
const pushCalls: string[] = [];
|
|
343
|
+
const engine = {
|
|
344
|
+
hasNewRemoteSnapshot: async () => true,
|
|
345
|
+
hasLocalChanges: async () => false,
|
|
346
|
+
merge: async (): Promise<MergePlan> => makeMergePlan([['settings', 'conflict']]),
|
|
347
|
+
push: async () => { pushCalls.push('push'); return { ok: true, snapshotId: 'x', sections: [] as never, warnings: [] }; },
|
|
348
|
+
};
|
|
349
|
+
const { scheduler, getConfig } = makeScheduler({ cfg, engine, history: [] });
|
|
350
|
+
const result = await scheduler.runOnce('git');
|
|
351
|
+
assert.equal(result.status, 'skipped');
|
|
352
|
+
assert.equal(result.skipReason, 'conflict');
|
|
353
|
+
assert.equal(pushCalls.length, 0, '本地无改动不上传');
|
|
354
|
+
assert.equal(getConfig().lastRunMessage ?? '', '', '本轮跳过不得残留上一轮的错误文案');
|
|
355
|
+
assert.equal(getConfig().lastRunSkipReason, 'conflict');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('issue #79:本轮成功必须清掉上一轮的错误文案与跳过码(陈旧信息不再挂面板)', async () => {
|
|
359
|
+
const cfg: AutosyncConfig = {
|
|
360
|
+
enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 2,
|
|
361
|
+
lastRunMessage: 'git pull 失败: ssh: connect to host 192.168.10.x port 2222: Connection timed out',
|
|
362
|
+
lastRunSkipReason: 'mutation-locked',
|
|
363
|
+
lastRunSkipCount: 7,
|
|
364
|
+
};
|
|
365
|
+
const engine = {
|
|
366
|
+
hasNewRemoteSnapshot: async () => false,
|
|
367
|
+
hasLocalChanges: async () => true,
|
|
368
|
+
push: async () => ({ ok: true, snapshotId: 'snap-clean', sections: [] as never, warnings: [] }),
|
|
369
|
+
};
|
|
370
|
+
const { scheduler, getConfig } = makeScheduler({ cfg, engine, history: [] });
|
|
371
|
+
const result = await scheduler.runOnce('git');
|
|
372
|
+
assert.equal(result.status, 'success');
|
|
373
|
+
const saved = getConfig();
|
|
374
|
+
assert.equal(saved.lastRunMessage, '', '成功即清空旧错误文案');
|
|
375
|
+
assert.equal(saved.lastRunSkipReason, undefined, '成功即清空旧跳过码');
|
|
376
|
+
assert.equal(saved.lastRunSkipCount, undefined);
|
|
377
|
+
assert.equal(saved.consecutiveFailures, 0, '成功归零连续失败');
|
|
322
378
|
});
|
|
323
379
|
|
|
324
380
|
test('runOnce: 无冲突且无变化 → success(pull,unchanged),不上传(本地也无改动)', async () => {
|