@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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* journal 专用文本脱敏(Security P1-1 / §29 已并入):
|
|
3
|
+
* 现有 redact()(结构字段 + 已知值形状)+ 高熵长 token 掩码。
|
|
4
|
+
* 用于 error / recovery.reason 等可能嵌入任意值的字段。
|
|
5
|
+
*/
|
|
6
|
+
export declare function redactJournalText(text: string): string;
|
|
7
|
+
export declare const JOURNAL_SCHEMA_VERSION = 1;
|
|
8
|
+
export declare const TRANSACTIONS_DIR = "transactions";
|
|
9
|
+
export declare const ACTIVE_DIR = "active";
|
|
10
|
+
export declare const COMPLETED_DIR = "completed";
|
|
11
|
+
export declare const QUARANTINE_DIR = "quarantine";
|
|
12
|
+
export declare const RECOVERY_HISTORY_DIR = "recovery-history";
|
|
13
|
+
export declare const SAFE_MODE_MARKER = "safe-mode";
|
|
14
|
+
export declare const NEEDS_ATTENTION_SIDECAR_SUFFIX = ".needs-attention";
|
|
15
|
+
export declare const VALID_OPERATION_ID_RE: RegExp;
|
|
16
|
+
export type JournalState = 'CREATED' | 'SNAPSHOT_CREATED' | 'APPLYING' | 'VALIDATING' | 'COMMITTED' | 'ROLLING_BACK' | 'ROLLED_BACK' | 'RECOVERING' | 'RECOVERED' | 'NEEDS_ATTENTION';
|
|
17
|
+
/** terminal states:进入后 operation 不可再改向(只可 move/quarantine/retention) */
|
|
18
|
+
export declare const TERMINAL_STATES: ReadonlySet<JournalState>;
|
|
19
|
+
/** 合法状态迁移表(严格;禁止无效 transition)。纯函数,供单测。 */
|
|
20
|
+
export declare const ALLOWED_TRANSITIONS: Record<JournalState, JournalState[]>;
|
|
21
|
+
export declare function isValidTransition(from: JournalState, to: JournalState): boolean;
|
|
22
|
+
export declare function isTerminalState(s: JournalState): boolean;
|
|
23
|
+
export type StepStatus = 'planned' | 'done' | 'failed' | 'skipped' | 'attention';
|
|
24
|
+
export interface JournalStep {
|
|
25
|
+
adapter: string;
|
|
26
|
+
ref: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
/** 外部副作用(插件/Git/WebDAV/reinstall)——crash 后不可证明,一律保守 */
|
|
29
|
+
external: boolean;
|
|
30
|
+
/** side effect 前目标内容指纹(null = 不可指纹) */
|
|
31
|
+
beforeFp: string | null;
|
|
32
|
+
/** side effect 完成后重读磁盘算出的指纹(null = 不可指纹) */
|
|
33
|
+
afterFp: string | null;
|
|
34
|
+
status: StepStatus;
|
|
35
|
+
appliedAt: string | null;
|
|
36
|
+
/** 该项的结论文案(issue #35:安装失败曾被记成 `skipped` 且无 message,
|
|
37
|
+
* 事后审计只能看到「用户跳过了这些插件」的错误结论)。可选,历史 journal 无此字段。 */
|
|
38
|
+
message?: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface JournalCommit {
|
|
41
|
+
at: string | null;
|
|
42
|
+
validated: boolean;
|
|
43
|
+
validationWarnings: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface JournalRollback {
|
|
46
|
+
attemptedAt: string | null;
|
|
47
|
+
full: boolean;
|
|
48
|
+
failed: string[];
|
|
49
|
+
/** 回滚 WAL:已补偿的 entry 序号(crash during rollback 判定用) */
|
|
50
|
+
entryDone: Record<number, boolean>;
|
|
51
|
+
}
|
|
52
|
+
/** recovery 元数据;outcome 为 true 表示已达 terminal */
|
|
53
|
+
export interface JournalRecovery {
|
|
54
|
+
attemptedAt: string | null;
|
|
55
|
+
outcome: 'RECOVERED' | 'ROLLED_BACK' | 'NEEDS_ATTENTION' | null;
|
|
56
|
+
reason: string;
|
|
57
|
+
attempts: number;
|
|
58
|
+
}
|
|
59
|
+
/** Phase 5 post-recovery verification verdict(§6.3)。 */
|
|
60
|
+
export type RecoveryVerificationVerdict = 'MATCH' | 'PARTIAL_MATCH' | 'MISMATCH' | 'VERIFICATION_ERROR';
|
|
61
|
+
/** Phase 5 post-recovery verification 结果(journal 可选字段,additive,不 bump schema)。 */
|
|
62
|
+
export interface RecoveryVerification {
|
|
63
|
+
verdict: RecoveryVerificationVerdict;
|
|
64
|
+
/** 每项检查结果(写入前过 redactJournalText 强脱敏)。 */
|
|
65
|
+
details: string[];
|
|
66
|
+
/** 需人工处理项(凭据等;写入前过 redactJournalText 强脱敏)。 */
|
|
67
|
+
manualHints: string[];
|
|
68
|
+
at: string;
|
|
69
|
+
}
|
|
70
|
+
/** 单个 operation 的 durable journal。不保存任何 secret 值。 */
|
|
71
|
+
export interface OperationJournal {
|
|
72
|
+
schemaVersion: number;
|
|
73
|
+
operationId: string;
|
|
74
|
+
operationType: string;
|
|
75
|
+
createdAt: string;
|
|
76
|
+
updatedAt: string;
|
|
77
|
+
state: JournalState;
|
|
78
|
+
/** Journal→Lock 单向绑定(不回填 environment.lock) */
|
|
79
|
+
ownerInstanceId: string;
|
|
80
|
+
lockId: string;
|
|
81
|
+
packageVersion: string;
|
|
82
|
+
environmentFingerprint: string;
|
|
83
|
+
snapshotId: string | null;
|
|
84
|
+
plannedSteps: string[];
|
|
85
|
+
steps: Record<string, JournalStep>;
|
|
86
|
+
commit: JournalCommit;
|
|
87
|
+
rollback: JournalRollback;
|
|
88
|
+
recovery: JournalRecovery;
|
|
89
|
+
/** 最后错误/原因文本(已 redact;非 secret) */
|
|
90
|
+
error: string;
|
|
91
|
+
/**
|
|
92
|
+
* Phase 5:post-recovery verification 结果(可选,additive)。
|
|
93
|
+
* 旧 journal 无此字段(parseSafe 不受影响,schemaVersion 仍为 1)。
|
|
94
|
+
* details/manualHints 写入前必须过 redactJournalText(journal 安全不变量:不保存 secret)。
|
|
95
|
+
*/
|
|
96
|
+
recoveryVerification?: RecoveryVerification;
|
|
97
|
+
}
|
|
98
|
+
export declare function createJournalEntry(operationType: string, lockCtx: {
|
|
99
|
+
operationId: string;
|
|
100
|
+
ownerInstanceId: string;
|
|
101
|
+
lockId: string;
|
|
102
|
+
packageVersion: string;
|
|
103
|
+
environmentFingerprint: string;
|
|
104
|
+
}, now: string): OperationJournal;
|
|
105
|
+
/** 把 journal 从旧状态迁移到新状态(校验合法 transition;非法抛错)。纯函数。 */
|
|
106
|
+
export declare function transitionJournalState(j: OperationJournal, to: JournalState): OperationJournal;
|
|
107
|
+
/** 可注入 IO(测 failure injection;默认包 node:fs/promises)。 */
|
|
108
|
+
export interface JournalIo {
|
|
109
|
+
mkdir(dir: string, opts: {
|
|
110
|
+
recursive: boolean;
|
|
111
|
+
}): Promise<void>;
|
|
112
|
+
readFileText(p: string): Promise<string>;
|
|
113
|
+
writeAll(target: string, content: string): Promise<void>;
|
|
114
|
+
rename(a: string, b: string): Promise<void>;
|
|
115
|
+
readdirNames(dir: string): Promise<string[]>;
|
|
116
|
+
readdirEntries(dir: string): Promise<Array<{
|
|
117
|
+
name: string;
|
|
118
|
+
isDirectory(): boolean;
|
|
119
|
+
}>>;
|
|
120
|
+
lstat(p: string): Promise<{
|
|
121
|
+
isSymbolicLink(): boolean;
|
|
122
|
+
} | null>;
|
|
123
|
+
rm(p: string, opts: {
|
|
124
|
+
recursive?: boolean;
|
|
125
|
+
force?: boolean;
|
|
126
|
+
}): Promise<void>;
|
|
127
|
+
exists(p: string): Promise<boolean>;
|
|
128
|
+
}
|
|
129
|
+
export interface JournalStoreOptions {
|
|
130
|
+
transactionsDir: string;
|
|
131
|
+
io?: JournalIo;
|
|
132
|
+
}
|
|
133
|
+
/** journal 严格 UUID 校验(防文件名穿越)。 */
|
|
134
|
+
export declare function isValidOperationId(id: unknown): id is string;
|
|
135
|
+
/** 判断文件 basename 是否是可追踪的 journal(<uuid>.json)。忽略 tmp 及其它。 */
|
|
136
|
+
export declare function isJournalBasename(name: string): boolean;
|
|
137
|
+
/** JournalStore:journal 持久化原语。不决定 transaction outcome。 */
|
|
138
|
+
export declare class JournalStore {
|
|
139
|
+
private readonly transactionsDir;
|
|
140
|
+
private readonly io;
|
|
141
|
+
constructor(opts: JournalStoreOptions);
|
|
142
|
+
private activeDir;
|
|
143
|
+
private completedDir;
|
|
144
|
+
private quarantineDir;
|
|
145
|
+
private recoveryHistoryDir;
|
|
146
|
+
private safeModePath;
|
|
147
|
+
private activePath;
|
|
148
|
+
private completedPath;
|
|
149
|
+
private quarantinePath;
|
|
150
|
+
ensureDirs(): Promise<void>;
|
|
151
|
+
/** 写 journal(atomic + 0600 + symlink reject)。返回写入后的 journal。 */
|
|
152
|
+
persist(operationId: string, j: OperationJournal): Promise<OperationJournal>;
|
|
153
|
+
/** 创建新 journal(CREATED)。调用方保证 active≤1。 */
|
|
154
|
+
create(entry: OperationJournal): Promise<OperationJournal>;
|
|
155
|
+
load(operationId: string): Promise<OperationJournal | null>;
|
|
156
|
+
loadActive(operationId: string): Promise<OperationJournal | null>;
|
|
157
|
+
/** 更新 journal(按 updater 修改后原子持久化)。不负责 transition 校验(调用方经 transitionJournalState)。 */
|
|
158
|
+
update(operationId: string, updater: (j: OperationJournal) => OperationJournal): Promise<OperationJournal>;
|
|
159
|
+
/** 原子迁移状态(校验合法 transition),并立即持久化。 */
|
|
160
|
+
transition(operationId: string, to: JournalState): Promise<OperationJournal>;
|
|
161
|
+
/** 扫描 active/ 下全部 journal op id(只认 <uuid>.json;忽略 tmp/损坏)。 */
|
|
162
|
+
scanActive(): Promise<string[]>;
|
|
163
|
+
/** 判断某 op 是否已 terminal(读 active 或 completed 的 journal state)。 */
|
|
164
|
+
isTerminal(operationId: string): Promise<boolean>;
|
|
165
|
+
isActive(operationId: string): Promise<boolean>;
|
|
166
|
+
terminalStateOf(operationId: string): Promise<JournalState | null>;
|
|
167
|
+
/** move active → completed(terminal journal 规整;复用 rename,失败抛错由调用方策略处理)。 */
|
|
168
|
+
moveToCompleted(operationId: string): Promise<void>;
|
|
169
|
+
/** quarantine 损坏/无法 parse/绑定失败的 journal(+ attention sidecar)。幂等。 */
|
|
170
|
+
quarantine(operationId: string, reason: string): Promise<void>;
|
|
171
|
+
/** 追加 recovery-history 事件(审计;best-effort 由调用方 try/catch 包裹)。 */
|
|
172
|
+
appendRecoveryHistory(marker: string, entry: unknown): Promise<void>;
|
|
173
|
+
listRecoveryHistory(): Promise<string[]>;
|
|
174
|
+
/**
|
|
175
|
+
* 收集已被「可恢复 / 未收敛 journal」引用的 snapshotId(Phase 4 F3 prune 保护):
|
|
176
|
+
* 扫描 active/ + quarantine/ 下的 journal,凡 state 非 COMMITTED(即可能仍需 recovery /
|
|
177
|
+
* NEEDS_ATTENTION / ROLLING_BACK / RECOVERY_REQUIRED / 非终态)且 snapshotId 合法
|
|
178
|
+
* → 收集。返回 Set<string>。COMMITTED 的 snapshot 已消费,不保护(可被 retention 淘汰)。
|
|
179
|
+
* 该集合用于 FileSnapshotStore.prune 豁免 —— 引用的 recovery snapshot 绝不可被自动淘汰。
|
|
180
|
+
*/
|
|
181
|
+
listReferencedSnapshotIds(): Promise<Set<string>>;
|
|
182
|
+
/** 读 SAFE MODE(RECOVERY_REQUIRED / NEEDS_ATTENTION)持久标记。存在且内容为 blocked → true。 */
|
|
183
|
+
readSafeMode(): Promise<boolean>;
|
|
184
|
+
/** 写/清 SAFE MODE 标记(atomic)。 */
|
|
185
|
+
writeSafeMode(blocked: boolean): Promise<void>;
|
|
186
|
+
/** 保留策略:completed 保留 N,recovery-history 保留 M。删最旧。幂等。 */
|
|
187
|
+
retention(completedLimit?: number, historyLimit?: number): Promise<void>;
|
|
188
|
+
private pruneOldest;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 环境指纹:hash(hostname + 持久化 per-install 随机 token)。
|
|
192
|
+
* token 存 <dataDir>/environment-fingerprint.token(0600,atomic),重启稳定、跨安装不同。
|
|
193
|
+
* 不能用 Date.now/pid/临时 id。
|
|
194
|
+
*/
|
|
195
|
+
export declare function environmentFingerprint(dataDir: string, io?: JournalIo): Promise<string>;
|
|
196
|
+
/** 生成 operationId(UUID v4,严格 <uuid>.json 形态)。 */
|
|
197
|
+
export declare function generateOperationId(): string;
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable Operation Journal(Phase 3:Crash Journal)。
|
|
3
|
+
*
|
|
4
|
+
* 职责(严格限定):journal 的 schema / 状态机 / 持久化 / atomic 更新 / active 扫描 /
|
|
5
|
+
* terminal 判定 / move(active→completed)/ quarantine / recovery-history / retention /
|
|
6
|
+
* safe-mode 标记 / environmentFingerprint 存储辅助。
|
|
7
|
+
*
|
|
8
|
+
* **JournalStore 不决定** rollback / resume / transaction outcome —— 那些属于
|
|
9
|
+
* `MutationTransactionCoordinator`(transaction-coordinator.ts)与 `Reconciler`(reconcile.ts)。
|
|
10
|
+
*
|
|
11
|
+
* 安全不变量(Phase 1 + Phase 3 Rev 3):
|
|
12
|
+
* - journal 更新一律经 Phase 1 `atomicWriteFile`(单文件 old-or-new,不 truncate-write)。
|
|
13
|
+
* - journal 文件 mode 0600;transactions 目录 0700;symlink reject 写 + lstat 读。
|
|
14
|
+
* - 只把 `<uuid>.json` 当作 journal;忽略 `.dshcm.*.tmp` 及其它非 journal 文件。
|
|
15
|
+
* - 不保存任何 secret(错误/recovery.reason 须过强 redaction)。
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs/promises';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
22
|
+
import { redact } from '../security/redaction.js';
|
|
23
|
+
/** 高熵值形状(长 hex/base64/随机 id):redact() 覆盖不了任意 secret,journal 级强脱敏补挡。 */
|
|
24
|
+
const HIGH_ENTROPY_RE = /([A-Za-z0-9+/_=-]{28,})/g;
|
|
25
|
+
/** 结构化时间戳/文件名形态:日期段必须由连字符连接(ISO 日期或紧凑时间戳)。
|
|
26
|
+
* 连字符不存在于 hex/base64 token 中,豁免不会误放行随机密钥。 */
|
|
27
|
+
const DATE_STAMP_RE = /\d{4}-\d{2}-\d{2}|\d{8}-\d{6}/;
|
|
28
|
+
/** 高熵长 token 掩码(日期戳形态豁免;回调逐个 run 判定,避免误伤文件名/时间戳)。 */
|
|
29
|
+
function maskHighEntropy(text) {
|
|
30
|
+
return text.replace(HIGH_ENTROPY_RE, (run) => (DATE_STAMP_RE.test(run) ? run : '[REDACTED]'));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* journal 专用文本脱敏(Security P1-1 / §29 已并入):
|
|
34
|
+
* 现有 redact()(结构字段 + 已知值形状)+ 高熵长 token 掩码。
|
|
35
|
+
* 用于 error / recovery.reason 等可能嵌入任意值的字段。
|
|
36
|
+
*/
|
|
37
|
+
export function redactJournalText(text) {
|
|
38
|
+
let out = text;
|
|
39
|
+
try {
|
|
40
|
+
out = redact(out);
|
|
41
|
+
}
|
|
42
|
+
catch { /* 脱敏失败保守处理 */ }
|
|
43
|
+
return maskHighEntropy(out);
|
|
44
|
+
}
|
|
45
|
+
// ---------- 常量 ----------
|
|
46
|
+
export const JOURNAL_SCHEMA_VERSION = 1;
|
|
47
|
+
export const TRANSACTIONS_DIR = 'transactions';
|
|
48
|
+
export const ACTIVE_DIR = 'active';
|
|
49
|
+
export const COMPLETED_DIR = 'completed';
|
|
50
|
+
export const QUARANTINE_DIR = 'quarantine';
|
|
51
|
+
export const RECOVERY_HISTORY_DIR = 'recovery-history';
|
|
52
|
+
export const SAFE_MODE_MARKER = 'safe-mode';
|
|
53
|
+
export const NEEDS_ATTENTION_SIDECAR_SUFFIX = '.needs-attention';
|
|
54
|
+
/** 只把 `<uuid>.json` 视作 journal(uuid v4 / v4-like),忽略 tmp 等。 */
|
|
55
|
+
const UUID_BASENAME_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i;
|
|
56
|
+
const TMP_PREFIX = '.dshcm.';
|
|
57
|
+
export const VALID_OPERATION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
58
|
+
/** terminal states:进入后 operation 不可再改向(只可 move/quarantine/retention) */
|
|
59
|
+
export const TERMINAL_STATES = new Set([
|
|
60
|
+
'COMMITTED', 'ROLLED_BACK', 'RECOVERED', 'NEEDS_ATTENTION',
|
|
61
|
+
]);
|
|
62
|
+
/** 合法状态迁移表(严格;禁止无效 transition)。纯函数,供单测。 */
|
|
63
|
+
export const ALLOWED_TRANSITIONS = {
|
|
64
|
+
CREATED: ['SNAPSHOT_CREATED', 'APPLYING', 'NEEDS_ATTENTION', 'RECOVERED', 'ROLLED_BACK'],
|
|
65
|
+
SNAPSHOT_CREATED: ['APPLYING', 'NEEDS_ATTENTION', 'RECOVERED', 'ROLLED_BACK'],
|
|
66
|
+
APPLYING: ['VALIDATING', 'ROLLING_BACK', 'NEEDS_ATTENTION', 'RECOVERED'],
|
|
67
|
+
VALIDATING: ['COMMITTED', 'ROLLING_BACK', 'NEEDS_ATTENTION', 'RECOVERED'],
|
|
68
|
+
COMMITTED: ['RECOVERED'], // COMMITTED 后只能由 recovery 规整为 RECOVERED(补记)
|
|
69
|
+
ROLLING_BACK: ['ROLLED_BACK', 'NEEDS_ATTENTION'],
|
|
70
|
+
ROLLED_BACK: ['RECOVERED'], // ROLLED_BACK 后 recovery 可规整为 RECOVERED(幂等)
|
|
71
|
+
RECOVERING: ['RECOVERED', 'ROLLED_BACK', 'NEEDS_ATTENTION'],
|
|
72
|
+
RECOVERED: [],
|
|
73
|
+
NEEDS_ATTENTION: ['RECOVERING', 'ROLLED_BACK', 'RECOVERED'], // 用户确认后可进入 recovery 流程
|
|
74
|
+
};
|
|
75
|
+
export function isValidTransition(from, to) {
|
|
76
|
+
return (ALLOWED_TRANSITIONS[from] ?? []).includes(to);
|
|
77
|
+
}
|
|
78
|
+
export function isTerminalState(s) {
|
|
79
|
+
return TERMINAL_STATES.has(s);
|
|
80
|
+
}
|
|
81
|
+
export function createJournalEntry(operationType, lockCtx, now) {
|
|
82
|
+
return {
|
|
83
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
84
|
+
operationId: lockCtx.operationId,
|
|
85
|
+
operationType,
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
state: 'CREATED',
|
|
89
|
+
ownerInstanceId: lockCtx.ownerInstanceId,
|
|
90
|
+
lockId: lockCtx.lockId,
|
|
91
|
+
packageVersion: lockCtx.packageVersion,
|
|
92
|
+
environmentFingerprint: lockCtx.environmentFingerprint,
|
|
93
|
+
snapshotId: null,
|
|
94
|
+
plannedSteps: [],
|
|
95
|
+
steps: {},
|
|
96
|
+
commit: { at: null, validated: false, validationWarnings: [] },
|
|
97
|
+
rollback: { attemptedAt: null, full: false, failed: [], entryDone: {} },
|
|
98
|
+
recovery: { attemptedAt: null, outcome: null, reason: '', attempts: 0 },
|
|
99
|
+
error: '',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** 把 journal 从旧状态迁移到新状态(校验合法 transition;非法抛错)。纯函数。 */
|
|
103
|
+
export function transitionJournalState(j, to) {
|
|
104
|
+
if (!isValidTransition(j.state, to)) {
|
|
105
|
+
throw new Error(`非法 journal state transition: ${j.state} → ${to} (op ${j.operationId})`);
|
|
106
|
+
}
|
|
107
|
+
return { ...j, state: to, updatedAt: new Date().toISOString() };
|
|
108
|
+
}
|
|
109
|
+
const defaultIo = {
|
|
110
|
+
async mkdir(d, o) { await fs.mkdir(d, o); },
|
|
111
|
+
async readFileText(p) { return (await fs.readFile(p, 'utf8')).toString(); },
|
|
112
|
+
async writeAll(t, c) { await atomicWriteFile(t, c, { mode: 0o600, symlink: 'reject' }); },
|
|
113
|
+
async rename(a, b) { await fs.rename(a, b); },
|
|
114
|
+
async readdirNames(d) { try {
|
|
115
|
+
return await fs.readdir(d);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
} },
|
|
120
|
+
async readdirEntries(d) { try {
|
|
121
|
+
return await fs.readdir(d, { withFileTypes: true });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return [];
|
|
125
|
+
} },
|
|
126
|
+
async lstat(p) { try {
|
|
127
|
+
return await fs.lstat(p);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
} },
|
|
132
|
+
async rm(p, o) { await fs.rm(p, o); },
|
|
133
|
+
async exists(p) { try {
|
|
134
|
+
await fs.access(p);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
} },
|
|
140
|
+
};
|
|
141
|
+
/** journal 严格 UUID 校验(防文件名穿越)。 */
|
|
142
|
+
export function isValidOperationId(id) {
|
|
143
|
+
return typeof id === 'string' && VALID_OPERATION_ID_RE.test(id);
|
|
144
|
+
}
|
|
145
|
+
/** 判断文件 basename 是否是可追踪的 journal(<uuid>.json)。忽略 tmp 及其它。 */
|
|
146
|
+
export function isJournalBasename(name) {
|
|
147
|
+
if (!UUID_BASENAME_RE.test(name))
|
|
148
|
+
return false;
|
|
149
|
+
return !name.startsWith(TMP_PREFIX);
|
|
150
|
+
}
|
|
151
|
+
function parseSafe(text) {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(text);
|
|
154
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
155
|
+
return null;
|
|
156
|
+
if (parsed.schemaVersion !== JOURNAL_SCHEMA_VERSION)
|
|
157
|
+
return null;
|
|
158
|
+
if (isValidOperationId(parsed.operationId) !== true)
|
|
159
|
+
return null;
|
|
160
|
+
if (typeof parsed.state !== 'string' || !isTerminalState(parsed.state) && !Object.keys(ALLOWED_TRANSITIONS).includes(parsed.state))
|
|
161
|
+
return null;
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** JournalStore:journal 持久化原语。不决定 transaction outcome。 */
|
|
169
|
+
export class JournalStore {
|
|
170
|
+
transactionsDir;
|
|
171
|
+
io;
|
|
172
|
+
constructor(opts) {
|
|
173
|
+
this.transactionsDir = opts.transactionsDir;
|
|
174
|
+
this.io = opts.io ?? defaultIo;
|
|
175
|
+
}
|
|
176
|
+
activeDir() { return path.join(this.transactionsDir, ACTIVE_DIR); }
|
|
177
|
+
completedDir() { return path.join(this.transactionsDir, COMPLETED_DIR); }
|
|
178
|
+
quarantineDir() { return path.join(this.transactionsDir, QUARANTINE_DIR); }
|
|
179
|
+
recoveryHistoryDir() { return path.join(this.transactionsDir, RECOVERY_HISTORY_DIR); }
|
|
180
|
+
safeModePath() { return path.join(this.transactionsDir, SAFE_MODE_MARKER); }
|
|
181
|
+
activePath(id) { return path.join(this.activeDir(), `${id}.json`); }
|
|
182
|
+
completedPath(id) { return path.join(this.completedDir(), `${id}.json`); }
|
|
183
|
+
quarantinePath(id) { return path.join(this.quarantineDir(), `${id}.json`); }
|
|
184
|
+
async ensureDirs() {
|
|
185
|
+
await this.io.mkdir(this.transactionsDir, { recursive: true });
|
|
186
|
+
await this.io.mkdir(this.activeDir(), { recursive: true });
|
|
187
|
+
await this.io.mkdir(this.completedDir(), { recursive: true });
|
|
188
|
+
await this.io.mkdir(this.quarantineDir(), { recursive: true });
|
|
189
|
+
await this.io.mkdir(this.recoveryHistoryDir(), { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
/** 写 journal(atomic + 0600 + symlink reject)。返回写入后的 journal。 */
|
|
192
|
+
async persist(operationId, j) {
|
|
193
|
+
if (!isValidOperationId(operationId))
|
|
194
|
+
throw new Error(`非法 operationId: ${JSON.stringify(operationId)}`);
|
|
195
|
+
await this.ensureDirs();
|
|
196
|
+
const updated = { ...j, updatedAt: new Date().toISOString() };
|
|
197
|
+
await this.io.writeAll(this.activePath(operationId), `${JSON.stringify(updated, null, 2)}\n`);
|
|
198
|
+
return updated;
|
|
199
|
+
}
|
|
200
|
+
/** 创建新 journal(CREATED)。调用方保证 active≤1。 */
|
|
201
|
+
async create(entry) {
|
|
202
|
+
return this.persist(entry.operationId, entry);
|
|
203
|
+
}
|
|
204
|
+
async load(operationId) {
|
|
205
|
+
if (!isValidOperationId(operationId))
|
|
206
|
+
return null;
|
|
207
|
+
const p = this.activePath(operationId);
|
|
208
|
+
if (await this.io.exists(p)) {
|
|
209
|
+
const text = await this.io.readFileText(p);
|
|
210
|
+
return parseSafe(text);
|
|
211
|
+
}
|
|
212
|
+
const c = this.completedPath(operationId);
|
|
213
|
+
if (await this.io.exists(c)) {
|
|
214
|
+
const text = await this.io.readFileText(c);
|
|
215
|
+
return parseSafe(text);
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
async loadActive(operationId) {
|
|
220
|
+
if (!isValidOperationId(operationId))
|
|
221
|
+
return null;
|
|
222
|
+
const p = this.activePath(operationId);
|
|
223
|
+
if (!(await this.io.exists(p)))
|
|
224
|
+
return null;
|
|
225
|
+
if ((await this.io.lstat(p))?.isSymbolicLink() === true)
|
|
226
|
+
return null; // symlink 防御
|
|
227
|
+
const text = await this.io.readFileText(p);
|
|
228
|
+
return parseSafe(text);
|
|
229
|
+
}
|
|
230
|
+
/** 更新 journal(按 updater 修改后原子持久化)。不负责 transition 校验(调用方经 transitionJournalState)。 */
|
|
231
|
+
async update(operationId, updater) {
|
|
232
|
+
const cur = await this.loadActive(operationId);
|
|
233
|
+
if (cur === null)
|
|
234
|
+
throw new Error(`journal 不存在或损坏: ${operationId}`);
|
|
235
|
+
return this.persist(operationId, updater(cur));
|
|
236
|
+
}
|
|
237
|
+
/** 原子迁移状态(校验合法 transition),并立即持久化。 */
|
|
238
|
+
async transition(operationId, to) {
|
|
239
|
+
return this.update(operationId, (j) => transitionJournalState(j, to));
|
|
240
|
+
}
|
|
241
|
+
/** 扫描 active/ 下全部 journal op id(只认 <uuid>.json;忽略 tmp/损坏)。 */
|
|
242
|
+
async scanActive() {
|
|
243
|
+
const names = await this.io.readdirNames(this.activeDir());
|
|
244
|
+
const out = [];
|
|
245
|
+
for (const n of names) {
|
|
246
|
+
if (!isJournalBasename(n))
|
|
247
|
+
continue;
|
|
248
|
+
out.push(n.slice(0, -'.json'.length));
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
/** 判断某 op 是否已 terminal(读 active 或 completed 的 journal state)。 */
|
|
253
|
+
async isTerminal(operationId) {
|
|
254
|
+
const j = await this.load(operationId);
|
|
255
|
+
return j !== null && isTerminalState(j.state);
|
|
256
|
+
}
|
|
257
|
+
async isActive(operationId) {
|
|
258
|
+
return (await this.loadActive(operationId)) !== null;
|
|
259
|
+
}
|
|
260
|
+
async terminalStateOf(operationId) {
|
|
261
|
+
const j = await this.load(operationId);
|
|
262
|
+
return j === null ? null : j.state;
|
|
263
|
+
}
|
|
264
|
+
/** move active → completed(terminal journal 规整;复用 rename,失败抛错由调用方策略处理)。 */
|
|
265
|
+
async moveToCompleted(operationId) {
|
|
266
|
+
if (!isValidOperationId(operationId))
|
|
267
|
+
throw new Error(`非法 operationId`);
|
|
268
|
+
;
|
|
269
|
+
await this.ensureDirs();
|
|
270
|
+
const src = this.activePath(operationId);
|
|
271
|
+
if (!(await this.io.exists(src)))
|
|
272
|
+
return; // 已不在 active(幂等)
|
|
273
|
+
// 只 move terminal 或已规整的;调用方保证
|
|
274
|
+
const j = await this.loadActive(operationId);
|
|
275
|
+
if (j === null)
|
|
276
|
+
return;
|
|
277
|
+
if (!isTerminalState(j.state)) {
|
|
278
|
+
throw new Error(`moveToCompleted 仅接受 terminal journal: ${operationId} state=${j.state}`);
|
|
279
|
+
}
|
|
280
|
+
const dst = this.completedPath(operationId);
|
|
281
|
+
if (await this.io.exists(dst)) {
|
|
282
|
+
// 目标已存在(重复规整)→ 删 active 副本(两者内容一致才删;保守:直接覆盖 dst)
|
|
283
|
+
await this.io.rm(src, { force: true });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
await this.io.rename(src, dst);
|
|
287
|
+
}
|
|
288
|
+
/** quarantine 损坏/无法 parse/绑定失败的 journal(+ attention sidecar)。幂等。 */
|
|
289
|
+
async quarantine(operationId, reason) {
|
|
290
|
+
if (!isValidOperationId(operationId))
|
|
291
|
+
return; // 非法文件名不 quarantine(避免穿越)
|
|
292
|
+
await this.ensureDirs();
|
|
293
|
+
const src = this.activePath(operationId);
|
|
294
|
+
if (!(await this.io.exists(src)))
|
|
295
|
+
return;
|
|
296
|
+
const dst = this.quarantinePath(operationId);
|
|
297
|
+
if (await this.io.exists(dst)) {
|
|
298
|
+
// 已 quarantine 过(幂等):移除 active 副本,不重复 move
|
|
299
|
+
await this.io.rm(src, { force: true });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
await this.io.rename(src, dst);
|
|
303
|
+
await this.io.writeAll(`${dst}${NEEDS_ATTENTION_SIDECAR_SUFFIX}`, `${JSON.stringify({ operationId, reason, at: new Date().toISOString() }, null, 2)}\n`);
|
|
304
|
+
}
|
|
305
|
+
/** 追加 recovery-history 事件(审计;best-effort 由调用方 try/catch 包裹)。 */
|
|
306
|
+
async appendRecoveryHistory(marker, entry) {
|
|
307
|
+
await this.ensureDirs();
|
|
308
|
+
const name = `${Date.now()}-${crypto.randomBytes(6).toString('hex')}.${marker}.json`;
|
|
309
|
+
await this.io.writeAll(path.join(this.recoveryHistoryDir(), name), `${JSON.stringify(entry, null, 2)}\n`);
|
|
310
|
+
}
|
|
311
|
+
async listRecoveryHistory() {
|
|
312
|
+
const names = await this.io.readdirNames(this.recoveryHistoryDir());
|
|
313
|
+
return names.sort();
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 收集已被「可恢复 / 未收敛 journal」引用的 snapshotId(Phase 4 F3 prune 保护):
|
|
317
|
+
* 扫描 active/ + quarantine/ 下的 journal,凡 state 非 COMMITTED(即可能仍需 recovery /
|
|
318
|
+
* NEEDS_ATTENTION / ROLLING_BACK / RECOVERY_REQUIRED / 非终态)且 snapshotId 合法
|
|
319
|
+
* → 收集。返回 Set<string>。COMMITTED 的 snapshot 已消费,不保护(可被 retention 淘汰)。
|
|
320
|
+
* 该集合用于 FileSnapshotStore.prune 豁免 —— 引用的 recovery snapshot 绝不可被自动淘汰。
|
|
321
|
+
*/
|
|
322
|
+
async listReferencedSnapshotIds() {
|
|
323
|
+
const out = new Set();
|
|
324
|
+
const dirs = [this.activeDir(), this.quarantineDir()];
|
|
325
|
+
for (const dir of dirs) {
|
|
326
|
+
for (const name of await this.io.readdirNames(dir)) {
|
|
327
|
+
if (!isJournalBasename(name))
|
|
328
|
+
continue;
|
|
329
|
+
const text = await this.io.readFileText(path.join(dir, name)).catch(() => null);
|
|
330
|
+
if (text === null)
|
|
331
|
+
continue;
|
|
332
|
+
const j = parseSafe(text);
|
|
333
|
+
if (j === null)
|
|
334
|
+
continue;
|
|
335
|
+
if (j.state === 'COMMITTED')
|
|
336
|
+
continue; // 已消费,不保护
|
|
337
|
+
if (typeof j.snapshotId === 'string' && j.snapshotId !== '')
|
|
338
|
+
out.add(j.snapshotId);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
// ---------- SAFE MODE ----------
|
|
344
|
+
/** 读 SAFE MODE(RECOVERY_REQUIRED / NEEDS_ATTENTION)持久标记。存在且内容为 blocked → true。 */
|
|
345
|
+
async readSafeMode() {
|
|
346
|
+
const p = this.safeModePath();
|
|
347
|
+
if (!(await this.io.exists(p)))
|
|
348
|
+
return false;
|
|
349
|
+
const text = await this.io.readFileText(p);
|
|
350
|
+
return /blocked|true/i.test(text);
|
|
351
|
+
}
|
|
352
|
+
/** 写/清 SAFE MODE 标记(atomic)。 */
|
|
353
|
+
async writeSafeMode(blocked) {
|
|
354
|
+
await this.ensureDirs();
|
|
355
|
+
const p = this.safeModePath();
|
|
356
|
+
if (!blocked) {
|
|
357
|
+
if (await this.io.exists(p))
|
|
358
|
+
await this.io.rm(p, { force: true });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await this.io.writeAll(p, `${JSON.stringify({ blocked: true, at: new Date().toISOString() }, null, 2)}\n`);
|
|
362
|
+
}
|
|
363
|
+
// ---------- retention ----------
|
|
364
|
+
/** 保留策略:completed 保留 N,recovery-history 保留 M。删最旧。幂等。 */
|
|
365
|
+
async retention(completedLimit = 50, historyLimit = 200) {
|
|
366
|
+
const completed = (await this.io.readdirNames(this.completedDir()))
|
|
367
|
+
.filter(isJournalBasename).sort();
|
|
368
|
+
await this.pruneOldest(this.completedDir(), completed, completedLimit);
|
|
369
|
+
const hist = (await this.io.readdirNames(this.recoveryHistoryDir())).sort();
|
|
370
|
+
await this.pruneOldest(this.recoveryHistoryDir(), hist, historyLimit);
|
|
371
|
+
}
|
|
372
|
+
async pruneOldest(dir, names, limit) {
|
|
373
|
+
if (names.length <= limit)
|
|
374
|
+
return;
|
|
375
|
+
const toRemove = names.slice(0, names.length - limit);
|
|
376
|
+
for (const n of toRemove) {
|
|
377
|
+
await this.io.rm(path.join(dir, n), { force: true }).catch(() => undefined);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// ---------- Environment Fingerprint ----------
|
|
382
|
+
/**
|
|
383
|
+
* 环境指纹:hash(hostname + 持久化 per-install 随机 token)。
|
|
384
|
+
* token 存 <dataDir>/environment-fingerprint.token(0600,atomic),重启稳定、跨安装不同。
|
|
385
|
+
* 不能用 Date.now/pid/临时 id。
|
|
386
|
+
*/
|
|
387
|
+
export async function environmentFingerprint(dataDir, io = defaultIo) {
|
|
388
|
+
const tokenPath = path.join(dataDir, 'environment-fingerprint.token');
|
|
389
|
+
let token;
|
|
390
|
+
try {
|
|
391
|
+
const tokenDir = path.dirname(tokenPath);
|
|
392
|
+
await io.mkdir(tokenDir, { recursive: true });
|
|
393
|
+
if (await io.exists(tokenPath)) {
|
|
394
|
+
const raw = (await io.readFileText(tokenPath)).trim();
|
|
395
|
+
token = /^[0-9a-f]{32,}$/i.test(raw) ? raw : crypto.randomBytes(24).toString('hex');
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
token = crypto.randomBytes(24).toString('hex');
|
|
399
|
+
await io.writeAll(tokenPath, `${token}\n`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
token = crypto.randomBytes(24).toString('hex'); // 读不到 → 新 token(跨启动变化,保守 fallback)
|
|
404
|
+
// 尽力落盘失败不阻塞(本指纹仅用于「是否同环境」,非安全边界)
|
|
405
|
+
}
|
|
406
|
+
const hostname = (() => { try {
|
|
407
|
+
return os.hostname();
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return 'unknown';
|
|
411
|
+
} })();
|
|
412
|
+
return crypto.createHash('sha256').update(`${hostname}|${token}`).digest('hex');
|
|
413
|
+
}
|
|
414
|
+
/** 生成 operationId(UUID v4,严格 <uuid>.json 形态)。 */
|
|
415
|
+
export function generateOperationId() {
|
|
416
|
+
return crypto.randomUUID();
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=journal.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parsePackOutput } from './local-plugin-pack.ts';
|
|
2
|
+
import type { PackExec, PackLocalPluginsResult } from './local-plugin-pack.ts';
|
|
3
|
+
import type { HostContext } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* 本地源插件打包钩子签名。
|
|
6
|
+
*
|
|
7
|
+
* 刻意在 core 内**重新声明**该签名(而非从 adapters 反向 import):架构边界测试
|
|
8
|
+
* (tests/architecture-boundaries.test.ts F7)规定 core 层只允许依赖
|
|
9
|
+
* node 内置 / core 内部 / schema / utils / security —— core → adapters 是违规方向。
|
|
10
|
+
* adapters 侧的 `LocalPluginPackHook` 与这是**结构等价**的,故可直接互赋(TS 结构化类型)。
|
|
11
|
+
* 注意:这里 host ctx 参数只需 HostContext,无需 adapters 的任何类型。
|
|
12
|
+
*/
|
|
13
|
+
export type LocalPluginPackHook = (plugins: readonly {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
spec?: string;
|
|
17
|
+
}[], ctx: Pick<HostContext, 'profile'>) => Promise<PackLocalPluginsResult>;
|
|
18
|
+
export interface LocalPluginPackHookOptions {
|
|
19
|
+
/** $DSH_HOME 绝对路径 */
|
|
20
|
+
homeDir: string;
|
|
21
|
+
/** 插件数据目录(临时打包目录建在其下,保证与既有目录生命周期一致) */
|
|
22
|
+
dataDir: string;
|
|
23
|
+
/** 覆盖 exec(测试注入) */
|
|
24
|
+
exec?: PackExec;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 构造注入给 `PluginsAdapter` 的打包钩子。
|
|
28
|
+
*
|
|
29
|
+
* 临时目录建在 `<dataDir>/tmp/`(与既有 tmp 语义一致),构造时不清扫、用完即删:
|
|
30
|
+
* 单次导出内建一个 `<dataDir>/tmp/local-pack-<random>` 目录,导出结束(无论成败)删除,
|
|
31
|
+
* 避免 tarball 残留在用户磁盘上。
|
|
32
|
+
*/
|
|
33
|
+
export declare function createLocalPluginPackHook(opts: LocalPluginPackHookOptions): LocalPluginPackHook;
|
|
34
|
+
/** 供其他模块复用(避免重复实现 npm 输出解析) */
|
|
35
|
+
export { parsePackOutput };
|