@cleocode/core 2026.9.26 → 2026.10.2
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/dist/build-provenance.json +2 -2
- package/dist/cloud/journal.d.ts +9 -5
- package/dist/cloud/journal.d.ts.map +1 -1
- package/dist/cloud/journal.js +29 -8
- package/dist/cloud/journal.js.map +1 -1
- package/dist/cloud/manifest-check.d.ts +140 -4
- package/dist/cloud/manifest-check.d.ts.map +1 -1
- package/dist/cloud/manifest-check.js +309 -10
- package/dist/cloud/manifest-check.js.map +1 -1
- package/dist/cloud/nexus-attach.d.ts +119 -0
- package/dist/cloud/nexus-attach.d.ts.map +1 -0
- package/dist/cloud/nexus-attach.js +267 -0
- package/dist/cloud/nexus-attach.js.map +1 -0
- package/dist/cloud/nexus-auth.d.ts +15 -3
- package/dist/cloud/nexus-auth.d.ts.map +1 -1
- package/dist/cloud/nexus-auth.js +90 -6
- package/dist/cloud/nexus-auth.js.map +1 -1
- package/dist/cloud/nexus-cloud-activity.d.ts +37 -0
- package/dist/cloud/nexus-cloud-activity.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud-activity.js +79 -0
- package/dist/cloud/nexus-cloud-activity.js.map +1 -0
- package/dist/cloud/nexus-cloud-status.d.ts +132 -0
- package/dist/cloud/nexus-cloud-status.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud-status.js +476 -0
- package/dist/cloud/nexus-cloud-status.js.map +1 -0
- package/dist/cloud/nexus-cloud.d.ts +179 -0
- package/dist/cloud/nexus-cloud.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud.js +296 -0
- package/dist/cloud/nexus-cloud.js.map +1 -0
- package/dist/cloud/nexus-device.d.ts +32 -7
- package/dist/cloud/nexus-device.d.ts.map +1 -1
- package/dist/cloud/nexus-device.js +54 -7
- package/dist/cloud/nexus-device.js.map +1 -1
- package/dist/cloud/nexus-enrol.d.ts +35 -4
- package/dist/cloud/nexus-enrol.d.ts.map +1 -1
- package/dist/cloud/nexus-enrol.js +192 -47
- package/dist/cloud/nexus-enrol.js.map +1 -1
- package/dist/cloud/nexus-home.d.ts +74 -0
- package/dist/cloud/nexus-home.d.ts.map +1 -0
- package/dist/cloud/nexus-home.js +97 -0
- package/dist/cloud/nexus-home.js.map +1 -0
- package/dist/cloud/nexus-link.d.ts +8 -1
- package/dist/cloud/nexus-link.d.ts.map +1 -1
- package/dist/cloud/nexus-link.js +138 -11
- package/dist/cloud/nexus-link.js.map +1 -1
- package/dist/cloud/nexus-logout.d.ts +114 -0
- package/dist/cloud/nexus-logout.d.ts.map +1 -0
- package/dist/cloud/nexus-logout.js +429 -0
- package/dist/cloud/nexus-logout.js.map +1 -0
- package/dist/cloud/nexus-vault-keys.d.ts +129 -0
- package/dist/cloud/nexus-vault-keys.d.ts.map +1 -0
- package/dist/cloud/nexus-vault-keys.js +277 -0
- package/dist/cloud/nexus-vault-keys.js.map +1 -0
- package/dist/cloud/nexus-vault-state.d.ts +121 -0
- package/dist/cloud/nexus-vault-state.d.ts.map +1 -0
- package/dist/cloud/nexus-vault-state.js +274 -0
- package/dist/cloud/nexus-vault-state.js.map +1 -0
- package/dist/cloud/nexus-vault.d.ts +160 -0
- package/dist/cloud/nexus-vault.d.ts.map +1 -0
- package/dist/cloud/nexus-vault.js +1733 -0
- package/dist/cloud/nexus-vault.js.map +1 -0
- package/dist/cloud/signing.d.ts +24 -8
- package/dist/cloud/signing.d.ts.map +1 -1
- package/dist/cloud/signing.js +61 -15
- package/dist/cloud/signing.js.map +1 -1
- package/dist/docs/export-document.js +8341 -3317
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/migrations.d.ts +75 -0
- package/dist/doctor/migrations.d.ts.map +1 -0
- package/dist/doctor/migrations.js +136 -0
- package/dist/doctor/migrations.js.map +1 -0
- package/dist/doctor/sync-sealer.d.ts +48 -0
- package/dist/doctor/sync-sealer.d.ts.map +1 -0
- package/dist/doctor/sync-sealer.js +109 -0
- package/dist/doctor/sync-sealer.js.map +1 -0
- package/dist/doctor/sync-triggers.d.ts +53 -0
- package/dist/doctor/sync-triggers.d.ts.map +1 -0
- package/dist/doctor/sync-triggers.js +157 -0
- package/dist/doctor/sync-triggers.js.map +1 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +5 -1
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/llm/plugin-facade.js +8668 -3644
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/nexus/projects-clean.d.ts +3 -0
- package/dist/nexus/projects-clean.d.ts.map +1 -1
- package/dist/nexus/projects-clean.js +9 -3
- package/dist/nexus/projects-clean.js.map +1 -1
- package/dist/nexus/registry-roots.d.ts +3 -1
- package/dist/nexus/registry-roots.d.ts.map +1 -1
- package/dist/nexus/registry-roots.js +5 -2
- package/dist/nexus/registry-roots.js.map +1 -1
- package/dist/nexus/registry.d.ts +8 -1
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +24 -10
- package/dist/nexus/registry.js.map +1 -1
- package/dist/nexus/workspace.d.ts +4 -1
- package/dist/nexus/workspace.d.ts.map +1 -1
- package/dist/nexus/workspace.js +10 -4
- package/dist/nexus/workspace.js.map +1 -1
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +7 -2
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestrate/worker-verify.d.ts +121 -12
- package/dist/orchestrate/worker-verify.d.ts.map +1 -1
- package/dist/orchestrate/worker-verify.js +172 -21
- package/dist/orchestrate/worker-verify.js.map +1 -1
- package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
- package/dist/orchestration/spawn-prompt.js +15 -9
- package/dist/orchestration/spawn-prompt.js.map +1 -1
- package/dist/release/ci-evidence.d.ts +9 -0
- package/dist/release/ci-evidence.d.ts.map +1 -1
- package/dist/release/ci-evidence.js +9 -2
- package/dist/release/ci-evidence.js.map +1 -1
- package/dist/resources/backend.d.ts +15 -3
- package/dist/resources/backend.d.ts.map +1 -1
- package/dist/resources/backend.js +7 -3
- package/dist/resources/backend.js.map +1 -1
- package/dist/resources/darwin-backend.d.ts +142 -0
- package/dist/resources/darwin-backend.d.ts.map +1 -0
- package/dist/resources/darwin-backend.js +288 -0
- package/dist/resources/darwin-backend.js.map +1 -0
- package/dist/resources/governor.d.ts +44 -1
- package/dist/resources/governor.d.ts.map +1 -1
- package/dist/resources/governor.js +100 -36
- package/dist/resources/governor.js.map +1 -1
- package/dist/resources/heavy-command.d.ts +132 -0
- package/dist/resources/heavy-command.d.ts.map +1 -0
- package/dist/resources/heavy-command.js +768 -0
- package/dist/resources/heavy-command.js.map +1 -0
- package/dist/resources/monitor.d.ts +50 -1
- package/dist/resources/monitor.d.ts.map +1 -1
- package/dist/resources/monitor.js +83 -1
- package/dist/resources/monitor.js.map +1 -1
- package/dist/resources/run-admission.d.ts +303 -0
- package/dist/resources/run-admission.d.ts.map +1 -0
- package/dist/resources/run-admission.js +648 -0
- package/dist/resources/run-admission.js.map +1 -0
- package/dist/resources/run-class.d.ts +111 -0
- package/dist/resources/run-class.d.ts.map +1 -0
- package/dist/resources/run-class.js +658 -0
- package/dist/resources/run-class.js.map +1 -0
- package/dist/resources/run-governed.d.ts +158 -0
- package/dist/resources/run-governed.d.ts.map +1 -0
- package/dist/resources/run-governed.js +406 -0
- package/dist/resources/run-governed.js.map +1 -0
- package/dist/resources/slot-holder.d.ts +267 -0
- package/dist/resources/slot-holder.d.ts.map +1 -0
- package/dist/resources/slot-holder.js +505 -0
- package/dist/resources/slot-holder.js.map +1 -0
- package/dist/resources/tool-groups.d.ts +74 -0
- package/dist/resources/tool-groups.d.ts.map +1 -0
- package/dist/resources/tool-groups.js +118 -0
- package/dist/resources/tool-groups.js.map +1 -0
- package/dist/routing/build-command-groups.d.ts.map +1 -1
- package/dist/routing/build-command-groups.js +1 -0
- package/dist/routing/build-command-groups.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +8676 -3652
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sentient/cross-project-hygiene.d.ts +3 -2
- package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
- package/dist/sentient/cross-project-hygiene.js +5 -2
- package/dist/sentient/cross-project-hygiene.js.map +1 -1
- package/dist/sentient/state.d.ts +7 -0
- package/dist/sentient/state.d.ts.map +1 -1
- package/dist/sentient/state.js.map +1 -1
- package/dist/sentient/tick.d.ts +14 -2
- package/dist/sentient/tick.d.ts.map +1 -1
- package/dist/sentient/tick.js +63 -4
- package/dist/sentient/tick.js.map +1 -1
- package/dist/setup/sections/nexus-account.d.ts +8 -6
- package/dist/setup/sections/nexus-account.d.ts.map +1 -1
- package/dist/setup/sections/nexus-account.js +53 -9
- package/dist/setup/sections/nexus-account.js.map +1 -1
- package/dist/store/backup-recover.d.ts.map +1 -1
- package/dist/store/backup-recover.js +5 -0
- package/dist/store/backup-recover.js.map +1 -1
- package/dist/store/data-accessor.d.ts +1 -0
- package/dist/store/data-accessor.d.ts.map +1 -1
- package/dist/store/data-accessor.js +7 -0
- package/dist/store/data-accessor.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts +23 -1
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +126 -36
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/recovery.d.ts.map +1 -1
- package/dist/store/exodus/recovery.js +15 -0
- package/dist/store/exodus/recovery.js.map +1 -1
- package/dist/store/legacy-tasks-lineage.d.ts.map +1 -1
- package/dist/store/legacy-tasks-lineage.js +5 -0
- package/dist/store/legacy-tasks-lineage.js.map +1 -1
- package/dist/store/lock.d.ts +8 -1
- package/dist/store/lock.d.ts.map +1 -1
- package/dist/store/lock.js +10 -2
- package/dist/store/lock.js.map +1 -1
- package/dist/store/migration-manager.d.ts +5 -0
- package/dist/store/migration-manager.d.ts.map +1 -1
- package/dist/store/migration-manager.js +11 -0
- package/dist/store/migration-manager.js.map +1 -1
- package/dist/store/migration-runner.d.ts +158 -0
- package/dist/store/migration-runner.d.ts.map +1 -0
- package/dist/store/migration-runner.js +493 -0
- package/dist/store/migration-runner.js.map +1 -0
- package/dist/store/portable-bundle-import.d.ts +15 -1
- package/dist/store/portable-bundle-import.d.ts.map +1 -1
- package/dist/store/portable-bundle-import.js +24 -4
- package/dist/store/portable-bundle-import.js.map +1 -1
- package/dist/store/portable-bundle-relocate.d.ts +43 -0
- package/dist/store/portable-bundle-relocate.d.ts.map +1 -1
- package/dist/store/portable-bundle-relocate.js +79 -5
- package/dist/store/portable-bundle-relocate.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts +20 -0
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +38 -3
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/portable-bundle.d.ts +33 -0
- package/dist/store/portable-bundle.d.ts.map +1 -1
- package/dist/store/portable-bundle.js +34 -7
- package/dist/store/portable-bundle.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts +137 -7
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +364 -9
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/row-identity.d.ts.map +1 -1
- package/dist/store/row-identity.js +9 -10
- package/dist/store/row-identity.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +10 -0
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sqlite.d.ts +31 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +5 -1
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sync/capture.d.ts +203 -0
- package/dist/store/sync/capture.d.ts.map +1 -0
- package/dist/store/sync/capture.js +562 -0
- package/dist/store/sync/capture.js.map +1 -0
- package/dist/store/sync/clock-store.d.ts.map +1 -1
- package/dist/store/sync/clock-store.js +2 -0
- package/dist/store/sync/clock-store.js.map +1 -1
- package/dist/store/sync/fk-orphans.d.ts +133 -0
- package/dist/store/sync/fk-orphans.d.ts.map +1 -0
- package/dist/store/sync/fk-orphans.js +265 -0
- package/dist/store/sync/fk-orphans.js.map +1 -0
- package/dist/store/sync/flags.d.ts +7 -0
- package/dist/store/sync/flags.d.ts.map +1 -1
- package/dist/store/sync/flags.js +16 -0
- package/dist/store/sync/flags.js.map +1 -1
- package/dist/store/sync/hlc.d.ts.map +1 -1
- package/dist/store/sync/hlc.js +4 -0
- package/dist/store/sync/hlc.js.map +1 -1
- package/dist/store/sync/identity-fill.d.ts +45 -0
- package/dist/store/sync/identity-fill.d.ts.map +1 -0
- package/dist/store/sync/identity-fill.js +85 -0
- package/dist/store/sync/identity-fill.js.map +1 -0
- package/dist/store/sync/machinery.d.ts +27 -0
- package/dist/store/sync/machinery.d.ts.map +1 -0
- package/dist/store/sync/machinery.js +44 -0
- package/dist/store/sync/machinery.js.map +1 -0
- package/dist/store/sync/netting.d.ts +120 -0
- package/dist/store/sync/netting.d.ts.map +1 -0
- package/dist/store/sync/netting.js +336 -0
- package/dist/store/sync/netting.js.map +1 -0
- package/dist/store/sync/replica.d.ts +59 -3
- package/dist/store/sync/replica.d.ts.map +1 -1
- package/dist/store/sync/replica.js +84 -3
- package/dist/store/sync/replica.js.map +1 -1
- package/dist/store/sync/schema.d.ts +18 -3
- package/dist/store/sync/schema.d.ts.map +1 -1
- package/dist/store/sync/schema.js +27 -3
- package/dist/store/sync/schema.js.map +1 -1
- package/dist/store/sync/seal-backlog.d.ts +21 -0
- package/dist/store/sync/seal-backlog.d.ts.map +1 -0
- package/dist/store/sync/seal-backlog.js +24 -0
- package/dist/store/sync/seal-backlog.js.map +1 -0
- package/dist/store/sync/sealer-values.d.ts +34 -0
- package/dist/store/sync/sealer-values.d.ts.map +1 -0
- package/dist/store/sync/sealer-values.js +53 -0
- package/dist/store/sync/sealer-values.js.map +1 -0
- package/dist/store/sync/sealer.d.ts +145 -0
- package/dist/store/sync/sealer.d.ts.map +1 -0
- package/dist/store/sync/sealer.js +906 -0
- package/dist/store/sync/sealer.js.map +1 -0
- package/dist/store/sync/structural.d.ts +63 -0
- package/dist/store/sync/structural.d.ts.map +1 -0
- package/dist/store/sync/structural.js +180 -0
- package/dist/store/sync/structural.js.map +1 -0
- package/dist/store/sync/timestamps.d.ts +92 -0
- package/dist/store/sync/timestamps.d.ts.map +1 -0
- package/dist/store/sync/timestamps.js +228 -0
- package/dist/store/sync/timestamps.js.map +1 -0
- package/dist/store/sync/tombstones.d.ts +75 -0
- package/dist/store/sync/tombstones.d.ts.map +1 -0
- package/dist/store/sync/tombstones.js +103 -0
- package/dist/store/sync/tombstones.js.map +1 -0
- package/dist/store/sync/trigger-classes.d.ts +221 -0
- package/dist/store/sync/trigger-classes.d.ts.map +1 -0
- package/dist/store/sync/trigger-classes.js +441 -0
- package/dist/store/sync/trigger-classes.js.map +1 -0
- package/dist/store/sync/trigger-suspend-at-path.d.ts +23 -0
- package/dist/store/sync/trigger-suspend-at-path.d.ts.map +1 -0
- package/dist/store/sync/trigger-suspend-at-path.js +44 -0
- package/dist/store/sync/trigger-suspend-at-path.js.map +1 -0
- package/dist/store/sync/writer-version.d.ts +43 -0
- package/dist/store/sync/writer-version.d.ts.map +1 -0
- package/dist/store/sync/writer-version.js +90 -0
- package/dist/store/sync/writer-version.js.map +1 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +102 -0
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/trigger-suspend-schema.d.ts +44 -0
- package/dist/store/trigger-suspend-schema.d.ts.map +1 -0
- package/dist/store/trigger-suspend-schema.js +25 -0
- package/dist/store/trigger-suspend-schema.js.map +1 -0
- package/dist/store/vault-manifest.d.ts +365 -0
- package/dist/store/vault-manifest.d.ts.map +1 -0
- package/dist/store/vault-manifest.js +812 -0
- package/dist/store/vault-manifest.js.map +1 -0
- package/dist/store/writer-lease.d.ts +31 -0
- package/dist/store/writer-lease.d.ts.map +1 -1
- package/dist/store/writer-lease.js +65 -2
- package/dist/store/writer-lease.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +21 -0
- package/dist/system/health.js.map +1 -1
- package/dist/system/project-health.d.ts.map +1 -1
- package/dist/system/project-health.js +3 -1
- package/dist/system/project-health.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +58 -0
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +75 -6
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-scope.d.ts +150 -23
- package/dist/tasks/affected-scope.d.ts.map +1 -1
- package/dist/tasks/affected-scope.js +320 -32
- package/dist/tasks/affected-scope.js.map +1 -1
- package/dist/tasks/change-set.d.ts +96 -0
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +169 -4
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/complete-ci.d.ts +115 -0
- package/dist/tasks/complete-ci.d.ts.map +1 -0
- package/dist/tasks/complete-ci.js +171 -0
- package/dist/tasks/complete-ci.js.map +1 -0
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +50 -17
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/compute-task-view.d.ts.map +1 -1
- package/dist/tasks/compute-task-view.js +7 -0
- package/dist/tasks/compute-task-view.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +51 -30
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/done-record.d.ts +6 -0
- package/dist/tasks/done-record.d.ts.map +1 -1
- package/dist/tasks/done-record.js +9 -3
- package/dist/tasks/done-record.js.map +1 -1
- package/dist/tasks/evidence.d.ts +3 -2
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +143 -12
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-notes.d.ts +22 -0
- package/dist/tasks/gate-notes.d.ts.map +1 -0
- package/dist/tasks/gate-notes.js +33 -0
- package/dist/tasks/gate-notes.js.map +1 -0
- package/dist/tasks/index.d.ts +2 -1
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +2 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/test-run-binding.d.ts +117 -0
- package/dist/tasks/test-run-binding.d.ts.map +1 -0
- package/dist/tasks/test-run-binding.js +365 -0
- package/dist/tasks/test-run-binding.js.map +1 -0
- package/dist/tasks/tool-cache-env.d.ts +123 -0
- package/dist/tasks/tool-cache-env.d.ts.map +1 -0
- package/dist/tasks/tool-cache-env.js +287 -0
- package/dist/tasks/tool-cache-env.js.map +1 -0
- package/dist/tasks/tool-cache-failed-first.d.ts +155 -0
- package/dist/tasks/tool-cache-failed-first.d.ts.map +1 -0
- package/dist/tasks/tool-cache-failed-first.js +285 -0
- package/dist/tasks/tool-cache-failed-first.js.map +1 -0
- package/dist/tasks/tool-cache.d.ts +375 -55
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +912 -303
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tasks/tool-resolver.d.ts +4 -3
- package/dist/tasks/tool-resolver.d.ts.map +1 -1
- package/dist/tasks/tool-resolver.js +15 -4
- package/dist/tasks/tool-resolver.js.map +1 -1
- package/dist/tasks/tool-semaphore.d.ts +67 -7
- package/dist/tasks/tool-semaphore.d.ts.map +1 -1
- package/dist/tasks/tool-semaphore.js +134 -56
- package/dist/tasks/tool-semaphore.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +4 -0
- package/dist/upgrade.js.map +1 -1
- package/dist/validation/engine-ops.d.ts +6 -0
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +8 -0
- package/dist/validation/engine-ops.js.map +1 -1
- package/migrations/drizzle-cleo-project/20260930170000_t12819-trigger-suspend-clause/migration.sql +367 -0
- package/migrations/sync-journal/20260930120000_t12343-capture/migration.sql +47 -0
- package/migrations/sync-journal/20261001170000_t12984-sealer/migration.sql +101 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +5 -5
- package/templates/CLEO-REFERENCE.md +8 -4
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cloud vault's snapshot manifest: per-table row counts and keyed hashes
|
|
3
|
+
* of a `cleo.db`, computed the same way on every machine so two machines can
|
|
4
|
+
* tell whether they hold the same data (T12336, T12950).
|
|
5
|
+
*
|
|
6
|
+
* - Only tables whose class syncs across devices are listed
|
|
7
|
+
* ({@link isPortableTableClass}), minus `portable-secret` (a vault bundle
|
|
8
|
+
* never carries credentials). Local-only, derived, pending and unclassified
|
|
9
|
+
* tables differ per machine by design and are left out.
|
|
10
|
+
* - A table's hash is HMAC-SHA256, keyed by a key derived from the stream's
|
|
11
|
+
* data key, over the sorted per-row SHA-256 digests. Sorting makes it
|
|
12
|
+
* independent of physical row order (a VACUUM can renumber rowids), and the
|
|
13
|
+
* key keeps the plaintext manifest from being a dictionary oracle for the
|
|
14
|
+
* server: it sees counts, never guessable digests.
|
|
15
|
+
* - Cells a restore relocates (path and JSON locator columns, as
|
|
16
|
+
* {@link relocatableColumn} decides) are hashed as if relocated to a
|
|
17
|
+
* placeholder root, so a relocated store hashes the same as its source.
|
|
18
|
+
* Historical text, which a restore leaves as written, is hashed as is.
|
|
19
|
+
* - Cells that never sync hash as NULL ({@link vaultLocalColumns}): credential
|
|
20
|
+
* columns (an unencrypted bundle clears them) and every column the
|
|
21
|
+
* classification registry gives its own `local-only`, `portable-secret` or
|
|
22
|
+
* `strip` class. A restore keeps this machine's values for them
|
|
23
|
+
* ({@link carryMachineState}), so the two sides must agree without them.
|
|
24
|
+
* - The rest of a snapshot (other primary databases, plain files) is folded
|
|
25
|
+
* into pseudo-table entries ({@link vaultDatabaseEntry},
|
|
26
|
+
* {@link vaultFilesEntry}) so a change anywhere in the bundle is seen.
|
|
27
|
+
*
|
|
28
|
+
* @task T12336
|
|
29
|
+
* @epic T12322
|
|
30
|
+
*/
|
|
31
|
+
import crypto from 'node:crypto';
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
34
|
+
import { SYNC_SCHEMA_VERSION } from '@cleocode/contracts';
|
|
35
|
+
import { isVaultRemotePath, VAULT_REMOTE_PATH_PREFIX } from '@cleocode/paths';
|
|
36
|
+
import { RELOCATED_JSON_FILES, relocatableColumn, relocateCell, relocatedJsonText, } from './portable-bundle-relocate.js';
|
|
37
|
+
import { CREDENTIAL_COLUMNS, credentialRemedy, sha256File } from './portable-bundle-scan.js';
|
|
38
|
+
import { applyPerfPragmas } from './sqlite-pragmas.js';
|
|
39
|
+
import { hasTriggerSuspendTable, TRIGGER_SUSPEND_TABLE, withTriggersSuspended, } from './sync/trigger-classes.js';
|
|
40
|
+
import { classifyTable, getTableRegistry, isPortableTableClass } from './table-classification.js';
|
|
41
|
+
// node:sqlite interop (createRequire — Vitest strips `node:` prefix)
|
|
42
|
+
const _require = createRequire(import.meta.url);
|
|
43
|
+
const { DatabaseSync } = _require('node:sqlite');
|
|
44
|
+
/**
|
|
45
|
+
* Version of the manifest computation (which cells hash, and how; which
|
|
46
|
+
* pseudo-entries exist). 2: non-syncing columns hash as NULL, plus the
|
|
47
|
+
* `zz_vault_db_*` and `zz_vault_files` entries (T12967, T12969).
|
|
48
|
+
*
|
|
49
|
+
* It is not the manifest's wire `schemaVersion`: that is
|
|
50
|
+
* {@link SYNC_SCHEMA_VERSION}, the stream data numbering the vault shares with
|
|
51
|
+
* the change journal (T13034), and a computation change must not move it (the
|
|
52
|
+
* server reads a rise there as a schema transition every v3 checkpoint pins).
|
|
53
|
+
* Each snapshot records its format in a {@link VAULT_FORMAT_KEY} entry
|
|
54
|
+
* ({@link vaultFormatEntry}), so a reader tells "hashed another way" from
|
|
55
|
+
* "does not match" ({@link vaultManifestFormat}). Bump it with any change to
|
|
56
|
+
* what a manifest hashes.
|
|
57
|
+
*/
|
|
58
|
+
export const VAULT_MANIFEST_FORMAT_VERSION = 2;
|
|
59
|
+
/**
|
|
60
|
+
* Manifest key of a snapshot's format record (T13034): which
|
|
61
|
+
* {@link VAULT_MANIFEST_FORMAT_VERSION} its hashes were computed under. Zero
|
|
62
|
+
* rows, so the server's count check is unaffected; an annotation, never a
|
|
63
|
+
* table, so comparisons ignore it. Its hash is keyed: only key holders read
|
|
64
|
+
* the version.
|
|
65
|
+
*/
|
|
66
|
+
export const VAULT_FORMAT_KEY = 'zz_vault_format';
|
|
67
|
+
/** A manifest key the wire contract accepts. */
|
|
68
|
+
const MANIFEST_KEY = /^[a-z][a-z0-9_]{0,62}$/;
|
|
69
|
+
/** Root that relocatable cells are re-rooted onto before hashing. */
|
|
70
|
+
const ROOT_PLACEHOLDER = '\u0000CLEO_ROOT\u0000';
|
|
71
|
+
/** Manifest key of a snapshot's plain-file inventory (rows: files, hash: keyed inventory hash). */
|
|
72
|
+
export const VAULT_FILES_KEY = 'zz_vault_files';
|
|
73
|
+
/** Prefix of the manifest key of each primary database other than `cleo.db`. */
|
|
74
|
+
export const VAULT_DB_KEY_PREFIX = 'zz_vault_db_';
|
|
75
|
+
/**
|
|
76
|
+
* The manifest key of a non-`cleo.db` primary database, from its path in the
|
|
77
|
+
* section (`blobs/manifest.db` -> `zz_vault_db_blobs_manifest_db`).
|
|
78
|
+
*
|
|
79
|
+
* @param relPath - Path relative to the section root.
|
|
80
|
+
* @returns A key the wire contract accepts.
|
|
81
|
+
*/
|
|
82
|
+
export function vaultDatabaseKey(relPath) {
|
|
83
|
+
const slug = relPath
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
86
|
+
.replace(/^_+|_+$/g, '');
|
|
87
|
+
const key = `${VAULT_DB_KEY_PREFIX}${slug}`;
|
|
88
|
+
if (key.length <= 63)
|
|
89
|
+
return key;
|
|
90
|
+
const digest = crypto.createHash('sha256').update(relPath).digest('hex').slice(0, 8);
|
|
91
|
+
return `${key.slice(0, 54)}_${digest}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Columns of `table` that never sync, so the vault neither compares nor
|
|
95
|
+
* restores them over this machine's values: credential columns
|
|
96
|
+
* ({@link CREDENTIAL_COLUMNS}) and whole-column overrides of the
|
|
97
|
+
* classification registry (`local-only`, `portable-secret`, `strip`). A
|
|
98
|
+
* JSON-path override (part of a value) does not exclude the column.
|
|
99
|
+
*
|
|
100
|
+
* @param scope - Store scope.
|
|
101
|
+
* @param table - Table name.
|
|
102
|
+
* @returns Column names (possibly absent from the actual table).
|
|
103
|
+
*/
|
|
104
|
+
export function vaultLocalColumns(scope, table) {
|
|
105
|
+
const out = new Set(CREDENTIAL_COLUMNS[table] ?? []);
|
|
106
|
+
const c = classifyTable(scope, table);
|
|
107
|
+
if (c.kind === 'entry') {
|
|
108
|
+
for (const col of c.entry.columns ?? [])
|
|
109
|
+
if (col.jsonPath === undefined)
|
|
110
|
+
out.add(col.column);
|
|
111
|
+
}
|
|
112
|
+
return [...out];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Whole columns the classification registry gives the `strip` class (a value
|
|
116
|
+
* stripped from everything that leaves the device, such as a git remote URL,
|
|
117
|
+
* which can embed a token), by table. A vault snapshot never carries them
|
|
118
|
+
* (T13007); like every non-syncing column they hash as NULL and a restore
|
|
119
|
+
* keeps this machine's values.
|
|
120
|
+
*
|
|
121
|
+
* @param scope - Store scope.
|
|
122
|
+
* @returns Table to stripped column names.
|
|
123
|
+
*/
|
|
124
|
+
export function vaultStripColumns(scope) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const [table, entry] of Object.entries(getTableRegistry(scope).tables)) {
|
|
127
|
+
const cols = (entry.columns ?? [])
|
|
128
|
+
.filter((c) => c.class === 'strip' && c.jsonPath === undefined)
|
|
129
|
+
.map((c) => c.column);
|
|
130
|
+
if (cols.length > 0)
|
|
131
|
+
out[table] = cols;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether a table belongs in a vault manifest of `scope`.
|
|
137
|
+
*
|
|
138
|
+
* @param scope - Store scope.
|
|
139
|
+
* @param name - Physical table name.
|
|
140
|
+
* @returns `true` for syncing, non-secret tables.
|
|
141
|
+
*/
|
|
142
|
+
export function isVaultManifestTable(scope, name) {
|
|
143
|
+
const c = classifyTable(scope, name);
|
|
144
|
+
if (c.kind === 'pending' || c.kind === 'unclassified')
|
|
145
|
+
return false;
|
|
146
|
+
return isPortableTableClass(c.class) && c.class !== 'portable-secret';
|
|
147
|
+
}
|
|
148
|
+
function canonicalValue(v, relocate) {
|
|
149
|
+
if (v === null || v === undefined)
|
|
150
|
+
return null;
|
|
151
|
+
if (typeof v === 'bigint')
|
|
152
|
+
return { i: v.toString() };
|
|
153
|
+
if (typeof v === 'number')
|
|
154
|
+
return Number.isInteger(v) ? { i: String(v) } : { f: v };
|
|
155
|
+
if (typeof v === 'string')
|
|
156
|
+
return relocate ? relocate(v) : v;
|
|
157
|
+
if (v instanceof Uint8Array)
|
|
158
|
+
return { b: Buffer.from(v).toString('hex') };
|
|
159
|
+
return String(v);
|
|
160
|
+
}
|
|
161
|
+
function quoteIdent(name) {
|
|
162
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Build the vault manifest of one `cleo.db`.
|
|
166
|
+
*
|
|
167
|
+
* Opens the file read-only; the caller passes a snapshot (a staged bundle
|
|
168
|
+
* copy) or the live store, whose single read transaction keeps it consistent.
|
|
169
|
+
*
|
|
170
|
+
* @param dbPath - The database file.
|
|
171
|
+
* @param opts - Scope, hash key and root to normalise.
|
|
172
|
+
* @returns The manifest and the tables skipped for their name.
|
|
173
|
+
*/
|
|
174
|
+
export function buildVaultManifest(dbPath, opts) {
|
|
175
|
+
const key = Buffer.from(opts.hashKey);
|
|
176
|
+
const root = opts.root === null ? '' : opts.root.replace(/[\\/]+$/, '');
|
|
177
|
+
const tables = {};
|
|
178
|
+
const skipped = [];
|
|
179
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
180
|
+
try {
|
|
181
|
+
applyPerfPragmas(db, { enableWal: false });
|
|
182
|
+
db.exec('BEGIN');
|
|
183
|
+
try {
|
|
184
|
+
const all = db
|
|
185
|
+
.prepare("SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
|
186
|
+
.all();
|
|
187
|
+
const isVirtual = (t) => (t.sql ?? '').toUpperCase().startsWith('CREATE VIRTUAL TABLE');
|
|
188
|
+
const virtualNames = all.filter(isVirtual).map((t) => t.name);
|
|
189
|
+
const names = all.filter((t) => !isVirtual(t) && (opts.allTables === true || isVaultManifestTable(opts.scope, t.name)));
|
|
190
|
+
for (const { name, sql } of names) {
|
|
191
|
+
if (opts.allTables !== true && !MANIFEST_KEY.test(name)) {
|
|
192
|
+
skipped.push(name);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const digests = [];
|
|
196
|
+
// An unencrypted bundle clears credential columns in every database it
|
|
197
|
+
// carries, so even an unclassified database hashes them as NULL.
|
|
198
|
+
const cleared = new Set(opts.allTables === true
|
|
199
|
+
? (CREDENTIAL_COLUMNS[name] ?? [])
|
|
200
|
+
: vaultLocalColumns(opts.scope, name));
|
|
201
|
+
// Mirror relocateDatabase: virtual-table shadow tables are never relocated.
|
|
202
|
+
const shadow = virtualNames.some((v) => name.startsWith(`${v}_`));
|
|
203
|
+
const withoutRowid = (sql ?? '').toUpperCase().includes('WITHOUT ROWID');
|
|
204
|
+
const relocators = new Map();
|
|
205
|
+
const relocatorOf = (col) => {
|
|
206
|
+
if (!relocators.has(col)) {
|
|
207
|
+
const kind = root === '' || shadow ? null : relocatableColumn(name, col, withoutRowid);
|
|
208
|
+
relocators.set(col, kind === null ? null : (s) => relocateCell(kind, s, root, ROOT_PLACEHOLDER));
|
|
209
|
+
}
|
|
210
|
+
return relocators.get(col) ?? null;
|
|
211
|
+
};
|
|
212
|
+
const select = db.prepare(`SELECT * FROM ${quoteIdent(name)}`);
|
|
213
|
+
// Integers past 2^53 (nanosecond times, inode numbers) would throw as numbers.
|
|
214
|
+
select.setReadBigInts(true);
|
|
215
|
+
for (const row of select.iterate()) {
|
|
216
|
+
const values = Object.entries(row).map(([col, v]) => cleared.has(col) ? null : canonicalValue(v, relocatorOf(col)));
|
|
217
|
+
digests.push(crypto.createHash('sha256').update(JSON.stringify(values)).digest());
|
|
218
|
+
}
|
|
219
|
+
digests.sort(Buffer.compare);
|
|
220
|
+
const mac = crypto.createHmac('sha256', key);
|
|
221
|
+
mac.update(`cleo-vault-table/v1\n${name}\n${digests.length}\n`);
|
|
222
|
+
for (const d of digests)
|
|
223
|
+
mac.update(d);
|
|
224
|
+
tables[name] = { rows: digests.length, hash: mac.digest('hex') };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
db.exec('COMMIT');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
db.close();
|
|
233
|
+
}
|
|
234
|
+
return { manifest: { schemaVersion: SYNC_SCHEMA_VERSION, tables }, skipped };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* One manifest entry for a whole primary database other than `cleo.db`
|
|
238
|
+
* (blobs manifest, attachments index): every table, relocation-aware like
|
|
239
|
+
* {@link buildVaultManifest}, folded into one count and keyed hash.
|
|
240
|
+
*
|
|
241
|
+
* @param dbPath - The database file.
|
|
242
|
+
* @param opts - Hash key and root to normalise.
|
|
243
|
+
* @returns Rows over all tables, and the keyed hash over every table's hash.
|
|
244
|
+
*/
|
|
245
|
+
export function vaultDatabaseEntry(dbPath, opts) {
|
|
246
|
+
const { manifest } = buildVaultManifest(dbPath, {
|
|
247
|
+
scope: 'project',
|
|
248
|
+
allTables: true,
|
|
249
|
+
hashKey: opts.hashKey,
|
|
250
|
+
root: opts.root,
|
|
251
|
+
});
|
|
252
|
+
const mac = crypto.createHmac('sha256', Buffer.from(opts.hashKey));
|
|
253
|
+
mac.update('cleo-vault-db/v1\n');
|
|
254
|
+
let rows = 0;
|
|
255
|
+
for (const name of Object.keys(manifest.tables).sort()) {
|
|
256
|
+
const t = manifest.tables[name];
|
|
257
|
+
rows += t.rows;
|
|
258
|
+
mac.update(`${name}\n${t.rows}\n${t.hash}\n`);
|
|
259
|
+
}
|
|
260
|
+
return { rows, hash: mac.digest('hex') };
|
|
261
|
+
}
|
|
262
|
+
/** JSON text with every object's keys sorted, so key order is not content. */
|
|
263
|
+
function canonicalJson(value) {
|
|
264
|
+
return JSON.stringify(value, (_key, v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
265
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
266
|
+
: v);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Keys of the global `config.json` that belong to the install, not to the
|
|
270
|
+
* user (T13022): never compared, and a restore keeps this machine's value.
|
|
271
|
+
*/
|
|
272
|
+
export const VAULT_GLOBAL_CONFIG_LOCAL_KEYS = [
|
|
273
|
+
['telemetry', 'installId'],
|
|
274
|
+
];
|
|
275
|
+
/**
|
|
276
|
+
* The digest a vault file inventory records for one plain file (T13005,
|
|
277
|
+
* T13022). Config files are hashed as JSON with sorted keys, so reordering is
|
|
278
|
+
* not a change: the project JSON files a restore relocates
|
|
279
|
+
* ({@link RELOCATED_JSON_FILES}: `config.json`, `project-context.json`) with
|
|
280
|
+
* their paths re-rooted from `root` onto a placeholder, so a relocated copy
|
|
281
|
+
* hashes like its source; the global `config.json` without its install keys
|
|
282
|
+
* ({@link VAULT_GLOBAL_CONFIG_LOCAL_KEYS}). Every other file, and a config
|
|
283
|
+
* file that is not JSON, is its SHA-256.
|
|
284
|
+
*
|
|
285
|
+
* @param absPath - The file.
|
|
286
|
+
* @param relPath - Its path in the section.
|
|
287
|
+
* @param opts - Scope, root and known SHA-256.
|
|
288
|
+
* @returns Hex digest.
|
|
289
|
+
*/
|
|
290
|
+
export async function vaultFileDigest(absPath, relPath, opts) {
|
|
291
|
+
const project = opts.scope === 'project' && opts.root !== null;
|
|
292
|
+
const globalConfig = opts.scope === 'global' && relPath === 'config.json';
|
|
293
|
+
if ((project && RELOCATED_JSON_FILES.includes(relPath)) || globalConfig) {
|
|
294
|
+
const text = fs.readFileSync(absPath, 'utf8');
|
|
295
|
+
const relocated = project && opts.root !== null
|
|
296
|
+
? relocatedJsonText(text, opts.root.replace(/[\\/]+$/, ''), ROOT_PLACEHOLDER)
|
|
297
|
+
: text;
|
|
298
|
+
let value;
|
|
299
|
+
try {
|
|
300
|
+
value = relocated === null ? undefined : JSON.parse(relocated);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
value = undefined;
|
|
304
|
+
}
|
|
305
|
+
if (value !== undefined) {
|
|
306
|
+
if (globalConfig)
|
|
307
|
+
value = withoutGlobalConfigLocalKeys(value);
|
|
308
|
+
return crypto
|
|
309
|
+
.createHash('sha256')
|
|
310
|
+
.update(`cleo-vault-json/v2\n${canonicalJson(value)}`)
|
|
311
|
+
.digest('hex');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return opts.sha256 ?? sha256File(absPath);
|
|
315
|
+
}
|
|
316
|
+
/** A copy of a parsed global `config.json` without {@link VAULT_GLOBAL_CONFIG_LOCAL_KEYS}. */
|
|
317
|
+
function withoutGlobalConfigLocalKeys(value) {
|
|
318
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
319
|
+
return value;
|
|
320
|
+
const out = structuredClone(value);
|
|
321
|
+
for (const [section, key] of VAULT_GLOBAL_CONFIG_LOCAL_KEYS) {
|
|
322
|
+
const inner = out[section];
|
|
323
|
+
if (inner !== undefined &&
|
|
324
|
+
inner !== null &&
|
|
325
|
+
typeof inner === 'object' &&
|
|
326
|
+
!Array.isArray(inner)) {
|
|
327
|
+
delete inner[key];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* One manifest entry for a snapshot's plain files: the count, and a keyed
|
|
334
|
+
* hash over every (path, sha256) pair, sorted by path.
|
|
335
|
+
*
|
|
336
|
+
* @param files - Files as the bundle manifest lists them.
|
|
337
|
+
* @param hashKey - The manifest hash key.
|
|
338
|
+
* @returns The entry.
|
|
339
|
+
*/
|
|
340
|
+
export function vaultFilesEntry(files, hashKey) {
|
|
341
|
+
const mac = crypto.createHmac('sha256', Buffer.from(hashKey));
|
|
342
|
+
mac.update(`cleo-vault-files/v1\n${files.length}\n`);
|
|
343
|
+
for (const f of [...files].sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0)) {
|
|
344
|
+
mac.update(`${f.relPath}\u0000${f.sha256}\n`);
|
|
345
|
+
}
|
|
346
|
+
return { rows: files.length, hash: mac.digest('hex') };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Manifest key that labels a snapshot as a fork (T13007): pushed with
|
|
350
|
+
* `--force` over a head its device had not synced, or after taking another
|
|
351
|
+
* device's live lease. Zero rows, so the server's count check is unaffected;
|
|
352
|
+
* an annotation, never a table, so comparisons ignore it. Being on the signed
|
|
353
|
+
* checkpoint, the label outlives the lease.
|
|
354
|
+
*/
|
|
355
|
+
export const VAULT_FORK_KEY = 'zz_vault_fork';
|
|
356
|
+
/**
|
|
357
|
+
* The {@link VAULT_FORK_KEY} entry of a fork pushed over `overCheckpointId`.
|
|
358
|
+
*
|
|
359
|
+
* @param hashKey - The manifest hash key.
|
|
360
|
+
* @param overCheckpointId - The head the fork was pushed over.
|
|
361
|
+
* @returns The entry (0 rows; the hash binds it to that head).
|
|
362
|
+
*/
|
|
363
|
+
export function vaultForkEntry(hashKey, overCheckpointId) {
|
|
364
|
+
return {
|
|
365
|
+
rows: 0,
|
|
366
|
+
hash: crypto
|
|
367
|
+
.createHmac('sha256', Buffer.from(hashKey))
|
|
368
|
+
.update(`cleo-vault-fork/v1\n${overCheckpointId}\n`)
|
|
369
|
+
.digest('hex'),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The {@link VAULT_FORMAT_KEY} entry of a snapshot hashed under `format`.
|
|
374
|
+
*
|
|
375
|
+
* @param hashKey - The manifest hash key.
|
|
376
|
+
* @param format - The manifest format (default: this CLEO's).
|
|
377
|
+
* @returns The entry (0 rows; the hash names the format).
|
|
378
|
+
*/
|
|
379
|
+
export function vaultFormatEntry(hashKey, format = VAULT_MANIFEST_FORMAT_VERSION) {
|
|
380
|
+
return {
|
|
381
|
+
rows: 0,
|
|
382
|
+
hash: crypto
|
|
383
|
+
.createHmac('sha256', Buffer.from(hashKey))
|
|
384
|
+
.update(`cleo-vault-format/v1\n${format}\n`)
|
|
385
|
+
.digest('hex'),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The manifest format a snapshot's hashes were computed under (T13034): the
|
|
390
|
+
* version its {@link VAULT_FORMAT_KEY} entry names, or `'unknown'` when the
|
|
391
|
+
* entry names no format this CLEO knows (a different vault format wrote it,
|
|
392
|
+
* most likely a newer CLEO). Read as the legacy format 2:
|
|
393
|
+
*
|
|
394
|
+
* - no entry: the snapshot predates the record, when vaults stamped their
|
|
395
|
+
* format as the wire `schemaVersion` (1 or 2), so it reads as 1 for
|
|
396
|
+
* `schemaVersion` 1 and as 2 otherwise;
|
|
397
|
+
* - an entry holding the empty-table hash: a build from before the record
|
|
398
|
+
* carried the parent's entry forward as an emptied table (it only skipped
|
|
399
|
+
* the fork label), over hashes it computed in format 2. Reading it as
|
|
400
|
+
* unknown would leave the stream unrestorable and unpushable for every
|
|
401
|
+
* later CLEO.
|
|
402
|
+
*
|
|
403
|
+
* @param manifest - A checkpoint's manifest.
|
|
404
|
+
* @param hashKey - The manifest hash key.
|
|
405
|
+
* @returns The format, or `'unknown'`.
|
|
406
|
+
*/
|
|
407
|
+
export function vaultManifestFormat(manifest, hashKey) {
|
|
408
|
+
const entry = Object.hasOwn(manifest.tables, VAULT_FORMAT_KEY)
|
|
409
|
+
? manifest.tables[VAULT_FORMAT_KEY]
|
|
410
|
+
: undefined;
|
|
411
|
+
if (entry === undefined)
|
|
412
|
+
return manifest.schemaVersion === 1 ? 1 : 2;
|
|
413
|
+
if (entry.hash === emptyVaultTableHash(hashKey, VAULT_FORMAT_KEY))
|
|
414
|
+
return 2;
|
|
415
|
+
for (let format = 1; format <= VAULT_MANIFEST_FORMAT_VERSION; format++) {
|
|
416
|
+
if (vaultFormatEntry(hashKey, format).hash === entry.hash)
|
|
417
|
+
return format;
|
|
418
|
+
}
|
|
419
|
+
return 'unknown';
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Whether a manifest key is an annotation (the fork label, the format
|
|
423
|
+
* record) rather than a table: never compared, counted or carried forward.
|
|
424
|
+
*
|
|
425
|
+
* @param key - A manifest key.
|
|
426
|
+
* @returns `true` for {@link VAULT_FORK_KEY} and {@link VAULT_FORMAT_KEY}.
|
|
427
|
+
*/
|
|
428
|
+
export function isVaultAnnotationKey(key) {
|
|
429
|
+
return key === VAULT_FORK_KEY || key === VAULT_FORMAT_KEY;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Whether a snapshot manifest carries the fork label.
|
|
433
|
+
*
|
|
434
|
+
* @param manifest - A checkpoint's manifest.
|
|
435
|
+
* @returns `true` when {@link VAULT_FORK_KEY} is present.
|
|
436
|
+
*/
|
|
437
|
+
export function isVaultForkManifest(manifest) {
|
|
438
|
+
return Object.hasOwn(manifest.tables, VAULT_FORK_KEY);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Compare two manifests table by table (union of both key sets, sorted). A
|
|
442
|
+
* table one side does not list and the other lists with 0 rows is the same
|
|
443
|
+
* (an emptied table stays listed on the cloud side); the annotations
|
|
444
|
+
* ({@link isVaultAnnotationKey}: fork label, format record) are not tables
|
|
445
|
+
* and are left out.
|
|
446
|
+
*
|
|
447
|
+
* @param local - This side.
|
|
448
|
+
* @param cloud - The other side.
|
|
449
|
+
* @returns One line per table.
|
|
450
|
+
*/
|
|
451
|
+
export function compareVaultManifests(local, cloud) {
|
|
452
|
+
const names = [...new Set([...Object.keys(local.tables), ...Object.keys(cloud.tables)])]
|
|
453
|
+
.filter((table) => !isVaultAnnotationKey(table))
|
|
454
|
+
.sort();
|
|
455
|
+
return names.map((table) => {
|
|
456
|
+
const l = local.tables[table];
|
|
457
|
+
const c = cloud.tables[table];
|
|
458
|
+
const emptyVsAbsent = (l === undefined && c?.rows === 0) || (c === undefined && l?.rows === 0);
|
|
459
|
+
return {
|
|
460
|
+
table,
|
|
461
|
+
localRows: l?.rows ?? null,
|
|
462
|
+
cloudRows: c?.rows ?? null,
|
|
463
|
+
match: emptyVsAbsent ||
|
|
464
|
+
(l !== undefined && c !== undefined && l.rows === c.rows && l.hash === c.hash),
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* True when two manifests list the same tables with the same counts and hashes.
|
|
470
|
+
*
|
|
471
|
+
* @param a - One manifest.
|
|
472
|
+
* @param b - The other.
|
|
473
|
+
* @returns Whether they are equal.
|
|
474
|
+
*/
|
|
475
|
+
export function sameVaultManifest(a, b) {
|
|
476
|
+
return compareVaultManifests(a, b).every((t) => t.match);
|
|
477
|
+
}
|
|
478
|
+
function hasValue(v) {
|
|
479
|
+
return v !== null && v !== undefined && v !== '';
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* The columns rows are matched on across machines: the primary key when it is
|
|
483
|
+
* stable, else the first non-partial UNIQUE index whose columns both stores
|
|
484
|
+
* have (a natural key, e.g. `skills_skills.name`, `accounts(provider, label)`).
|
|
485
|
+
* An integer primary key (a rowid alias or AUTOINCREMENT) is minted per
|
|
486
|
+
* machine, so it is never used. `null` when there is no stable key.
|
|
487
|
+
*/
|
|
488
|
+
function stableKey(db, table, sql, info, common) {
|
|
489
|
+
const pk = info
|
|
490
|
+
.filter((c) => c.pk > 0)
|
|
491
|
+
.sort((x, y) => x.pk - y.pk)
|
|
492
|
+
.map((c) => c.name);
|
|
493
|
+
const unstablePk = /\bAUTOINCREMENT\b/i.test(sql)
|
|
494
|
+
? pk
|
|
495
|
+
: info.filter((c) => c.pk > 0 && /INT/i.test(c.type)).map((c) => c.name);
|
|
496
|
+
if (pk.length > 0 && unstablePk.length === 0) {
|
|
497
|
+
return pk.every((k) => common.has(k)) ? { key: pk, unstablePk } : null;
|
|
498
|
+
}
|
|
499
|
+
const indexes = db.prepare(`PRAGMA index_list(${quoteIdent(table)})`).all().filter((i) => i.unique === 1 && i.partial === 0 && i.origin !== 'pk');
|
|
500
|
+
for (const i of indexes.sort((x, y) => (x.origin === 'u' ? 0 : 1) - (y.origin === 'u' ? 0 : 1))) {
|
|
501
|
+
const cols = db.prepare(`PRAGMA index_info(${quoteIdent(i.name)})`).all().map((c) => c.name);
|
|
502
|
+
if (cols.length > 0 && cols.every((c) => c !== null && common.has(c))) {
|
|
503
|
+
return { key: cols, unstablePk };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Before a vault restore activates a snapshot, carry this machine's own state
|
|
510
|
+
* into the staged copy, so a pull never replaces it with another device's
|
|
511
|
+
* (or with the empty cells an unencrypted bundle carries):
|
|
512
|
+
*
|
|
513
|
+
* - `local-only` tables: all rows come from the live store (replica binding,
|
|
514
|
+
* machine state); a table absent here is emptied.
|
|
515
|
+
* - `portable-secret` tables: live rows are upserted by a stable key (the
|
|
516
|
+
* snapshot's copies arrive with their secrets cleared). Without a stable
|
|
517
|
+
* key the live table is kept whole.
|
|
518
|
+
* - Every other table: its non-syncing columns ({@link vaultLocalColumns}:
|
|
519
|
+
* credentials, machine-local paths) are copied from the live row with the
|
|
520
|
+
* same stable key. A table without one is skipped and reported; rows are
|
|
521
|
+
* never matched by an integer id, which each machine mints for itself.
|
|
522
|
+
* `strip` columns ({@link vaultStripColumns}) are not copied: they are NULL
|
|
523
|
+
* on every row, so this machine recomputes them against the restored data.
|
|
524
|
+
* - Rows only the snapshot has (a project or skill on another machine only,
|
|
525
|
+
* a task this machine has not seen) never bring that machine's own values:
|
|
526
|
+
* every non-syncing cell (claims, leases, counters, paths) is cleared (NULL).
|
|
527
|
+
* A NOT NULL path outside `snapshotRoot` becomes a
|
|
528
|
+
* {@link VAULT_REMOTE_PATH_PREFIX} placeholder that is not a path and is
|
|
529
|
+
* never treated as an orphan; another NOT NULL cell takes its declared
|
|
530
|
+
* default, or stays when it has none. A path under `snapshotRoot` in a
|
|
531
|
+
* column the relocation rewrites stays (it becomes this machine's path).
|
|
532
|
+
*
|
|
533
|
+
* Syncing cells keep the snapshot's values (they are what the manifest
|
|
534
|
+
* verified; non-syncing cells hash as NULL, so none of this changes the
|
|
535
|
+
* manifest). A whole table is carried only when it has the same columns in
|
|
536
|
+
* both stores; a column merge uses the columns both have. A live credential
|
|
537
|
+
* that has nowhere to go is reported in `lost` with its re-entry remedy.
|
|
538
|
+
*
|
|
539
|
+
* @param stagedDbPath - The staged snapshot database (written in place).
|
|
540
|
+
* @param liveDbPath - This machine's current database (read only), or `null` when it has none
|
|
541
|
+
* yet (a project restored onto this machine): every row is then a snapshot-only row.
|
|
542
|
+
* @param scope - Which `cleo.db` this is.
|
|
543
|
+
* @param opts - The snapshot's root (see {@link CarryMachineStateOptions}).
|
|
544
|
+
* @returns What was carried, skipped, lost and scrubbed.
|
|
545
|
+
*/
|
|
546
|
+
export function carryMachineState(stagedDbPath, liveDbPath, scope, opts = { snapshotRoot: null }) {
|
|
547
|
+
const out = {
|
|
548
|
+
preserved: [],
|
|
549
|
+
carried: [],
|
|
550
|
+
skipped: [],
|
|
551
|
+
lost: [],
|
|
552
|
+
scrubbed: [],
|
|
553
|
+
};
|
|
554
|
+
// No store here yet (a project restored onto this machine): carry against an
|
|
555
|
+
// empty one, so no local-only row and no non-syncing cell of the pusher's arrives.
|
|
556
|
+
const live = liveDbPath === null
|
|
557
|
+
? new DatabaseSync(':memory:') // schema-guard-exempt: an empty in-memory stand-in for a store this machine does not have; only read
|
|
558
|
+
: new DatabaseSync(liveDbPath, { readOnly: true });
|
|
559
|
+
const staged = new DatabaseSync(stagedDbPath); // schema-guard-exempt: the staged restore copy this step owns; carrying machine state is DML only
|
|
560
|
+
const snapshotRoot = opts.snapshotRoot === null ? null : opts.snapshotRoot.replace(/[\\/]+$/, '');
|
|
561
|
+
try {
|
|
562
|
+
const tablesOf = (db) => new Map(db
|
|
563
|
+
.prepare("SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND sql NOT LIKE 'CREATE VIRTUAL TABLE%'")
|
|
564
|
+
.all().map((r) => [r.name, r.sql ?? '']));
|
|
565
|
+
const infoOf = (db, t) => db.prepare(`PRAGMA table_info(${quoteIdent(t)})`).all();
|
|
566
|
+
const rowsOf = (db, t) => {
|
|
567
|
+
const select = db.prepare(`SELECT * FROM ${quoteIdent(t)}`);
|
|
568
|
+
// Integers past 2^53 (nanosecond times, inode numbers) would throw as numbers.
|
|
569
|
+
select.setReadBigInts(true);
|
|
570
|
+
return select.all();
|
|
571
|
+
};
|
|
572
|
+
const liveTables = tablesOf(live);
|
|
573
|
+
const lose = (table, rows) => {
|
|
574
|
+
if (rows > 0)
|
|
575
|
+
out.lost.push({ table, rows, remedy: credentialRemedy(table) });
|
|
576
|
+
};
|
|
577
|
+
const keyOf = (r, key) => {
|
|
578
|
+
const values = key.map((k) => r[k] ?? null);
|
|
579
|
+
return values.some((v) => v === null) ? null : JSON.stringify(values.map((v) => String(v)));
|
|
580
|
+
};
|
|
581
|
+
const underSnapshotRoot = (v) => snapshotRoot !== null &&
|
|
582
|
+
(v === snapshotRoot ||
|
|
583
|
+
(v.startsWith(snapshotRoot) && /^[\\/]/.test(v.slice(snapshotRoot.length))));
|
|
584
|
+
const absolute = (v) => v.startsWith('/') || /^[A-Za-z]:[\\/]/.test(v) || v.startsWith('\\\\');
|
|
585
|
+
// A path the restore relocates onto this machine: a relocatable column
|
|
586
|
+
// under the snapshot's root (the relocation rewrites it).
|
|
587
|
+
const relocatedHere = (v, relocatable) => relocatable && typeof v === 'string' && absolute(v) && underSnapshotRoot(v);
|
|
588
|
+
// An absolute path a restore will not relocate onto this machine.
|
|
589
|
+
const foreignPath = (v, relocatable) => typeof v === 'string' &&
|
|
590
|
+
!isVaultRemotePath(v) &&
|
|
591
|
+
absolute(v) &&
|
|
592
|
+
!relocatedHere(v, relocatable);
|
|
593
|
+
const stripped = vaultStripColumns(scope);
|
|
594
|
+
staged.exec('PRAGMA foreign_keys = OFF');
|
|
595
|
+
staged.exec('BEGIN IMMEDIATE');
|
|
596
|
+
try {
|
|
597
|
+
const carry = () => {
|
|
598
|
+
for (const [t, sql] of [...tablesOf(staged)].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
599
|
+
// The trigger-suspension flags are transient machinery the carry itself
|
|
600
|
+
// holds: carrying the live (empty) table would lift its own suspension.
|
|
601
|
+
if (t === TRIGGER_SUSPEND_TABLE)
|
|
602
|
+
continue;
|
|
603
|
+
const c = classifyTable(scope, t);
|
|
604
|
+
if (c.kind !== 'entry' && c.kind !== 'pattern')
|
|
605
|
+
continue;
|
|
606
|
+
const credentialCols = CREDENTIAL_COLUMNS[t] ?? [];
|
|
607
|
+
const stagedInfo = infoOf(staged, t);
|
|
608
|
+
const stagedCols = stagedInfo.map((x) => x.name);
|
|
609
|
+
const liveInfo = liveTables.has(t) ? infoOf(live, t) : [];
|
|
610
|
+
const liveCols = liveInfo.map((x) => x.name);
|
|
611
|
+
const sameShape = liveCols.join('\u0000') === stagedCols.join('\u0000');
|
|
612
|
+
const holdsSecret = (r) => c.class === 'portable-secret' || credentialCols.some((k) => hasValue(r[k]));
|
|
613
|
+
const replaceWhole = (rows) => {
|
|
614
|
+
staged.prepare(`DELETE FROM ${quoteIdent(t)}`).run();
|
|
615
|
+
insertRows(staged, t, liveCols, rows);
|
|
616
|
+
out.preserved.push(t);
|
|
617
|
+
};
|
|
618
|
+
if (c.class === 'local-only') {
|
|
619
|
+
// Machine state never comes from another machine: a table this store
|
|
620
|
+
// does not have yet is emptied, not filled with the pusher's rows.
|
|
621
|
+
if (!liveTables.has(t)) {
|
|
622
|
+
staged.prepare(`DELETE FROM ${quoteIdent(t)}`).run();
|
|
623
|
+
out.preserved.push(t);
|
|
624
|
+
}
|
|
625
|
+
else if (!sameShape) {
|
|
626
|
+
out.skipped.push(t);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
replaceWhole(rowsOf(live, t));
|
|
630
|
+
}
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const common = new Set(stagedCols.filter((n) => liveCols.includes(n)));
|
|
634
|
+
const local = vaultLocalColumns(scope, t).filter((n) => stagedCols.includes(n));
|
|
635
|
+
if (c.class !== 'portable-secret' && local.length === 0)
|
|
636
|
+
continue;
|
|
637
|
+
const liveRows = liveTables.has(t) ? rowsOf(live, t) : [];
|
|
638
|
+
// A table this store lacks matches no live row; its key still names placeholders.
|
|
639
|
+
const stable = stableKey(staged, t, sql, stagedInfo, liveTables.has(t) ? common : new Set(stagedCols));
|
|
640
|
+
const liveKeys = new Set();
|
|
641
|
+
if (stable === null) {
|
|
642
|
+
if (c.class === 'portable-secret' && liveTables.has(t) && sameShape) {
|
|
643
|
+
// No key to match rows by: this machine's secrets are kept whole.
|
|
644
|
+
replaceWhole(liveRows);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (liveRows.length > 0)
|
|
648
|
+
out.skipped.push(t);
|
|
649
|
+
lose(t, liveRows.filter(holdsSecret).length);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
const { key, unstablePk } = stable;
|
|
653
|
+
// A `strip` column is recomputed here, never carried (T13022).
|
|
654
|
+
const merge = c.class === 'portable-secret'
|
|
655
|
+
? [...common].filter((n) => !key.includes(n) && !unstablePk.includes(n))
|
|
656
|
+
: local.filter((n) => common.has(n) && !(stripped[t] ?? []).includes(n));
|
|
657
|
+
const where = key.map((k) => `${quoteIdent(k)} = ?`).join(' AND ');
|
|
658
|
+
const exists = staged.prepare(`SELECT 1 FROM ${quoteIdent(t)} WHERE ${where} LIMIT 1`);
|
|
659
|
+
const update = merge.length > 0
|
|
660
|
+
? staged.prepare(`UPDATE ${quoteIdent(t)} SET ${merge.map((k) => `${quoteIdent(k)} = ?`).join(', ')} WHERE ${where}`)
|
|
661
|
+
: null;
|
|
662
|
+
// A live-only secret row is inserted without its machine-minted integer id.
|
|
663
|
+
const cols = [...common].filter((n) => !unstablePk.includes(n));
|
|
664
|
+
const insert = cols.length > 0
|
|
665
|
+
? staged.prepare(`INSERT INTO ${quoteIdent(t)} (${cols.map(quoteIdent).join(', ')}) VALUES (${cols.map(() => '?').join(', ')})`)
|
|
666
|
+
: null;
|
|
667
|
+
let rows = 0;
|
|
668
|
+
let lost = 0;
|
|
669
|
+
for (const r of liveRows) {
|
|
670
|
+
const k = keyOf(r, key);
|
|
671
|
+
if (k === null) {
|
|
672
|
+
if (holdsSecret(r))
|
|
673
|
+
lost += 1;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
liveKeys.add(k);
|
|
677
|
+
const keyValues = key.map((col) => r[col] ?? null);
|
|
678
|
+
try {
|
|
679
|
+
if (exists.get(...keyValues) !== undefined) {
|
|
680
|
+
update?.run(...merge.map((col) => r[col] ?? null), ...keyValues);
|
|
681
|
+
rows += 1;
|
|
682
|
+
}
|
|
683
|
+
else if (c.class === 'portable-secret' && insert !== null) {
|
|
684
|
+
// A secret row this machine holds and the snapshot lacks: keep it.
|
|
685
|
+
insert.run(...cols.map((col) => r[col] ?? null));
|
|
686
|
+
rows += 1;
|
|
687
|
+
}
|
|
688
|
+
else if (holdsSecret(r)) {
|
|
689
|
+
lost += 1;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// A constraint (e.g. a path another snapshot row now owns): the snapshot value stays.
|
|
694
|
+
if (holdsSecret(r))
|
|
695
|
+
lost += 1;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (rows > 0)
|
|
699
|
+
out.carried.push({ table: t, columns: merge, rows });
|
|
700
|
+
lose(t, lost);
|
|
701
|
+
}
|
|
702
|
+
// `strip` columns (a derived pointer such as `tree_id`, a git remote URL)
|
|
703
|
+
// are NULL on every row, so this machine recomputes them (T13022).
|
|
704
|
+
const nullable = new Set(stagedInfo.filter((x) => x.notnull !== 1).map((x) => x.name));
|
|
705
|
+
for (const col of (stripped[t] ?? []).filter((n) => nullable.has(n))) {
|
|
706
|
+
staged
|
|
707
|
+
.prepare(`UPDATE ${quoteIdent(t)} SET ${quoteIdent(col)} = NULL WHERE ${quoteIdent(col)} IS NOT NULL`)
|
|
708
|
+
.run();
|
|
709
|
+
}
|
|
710
|
+
// Rows only the snapshot has never bring another machine's own values.
|
|
711
|
+
if (c.class === 'portable-secret')
|
|
712
|
+
continue;
|
|
713
|
+
if (local.length === 0 || /WITHOUT ROWID/i.test(sql))
|
|
714
|
+
continue;
|
|
715
|
+
const info = new Map(stagedInfo.map((x) => [x.name, x]));
|
|
716
|
+
const scrubKey = stable?.key ?? null;
|
|
717
|
+
let scrubbed = 0;
|
|
718
|
+
const select = staged.prepare(`SELECT rowid AS rid, * FROM ${quoteIdent(t)}`);
|
|
719
|
+
select.setReadBigInts(true);
|
|
720
|
+
for (const r of select.all()) {
|
|
721
|
+
const k = scrubKey === null ? null : keyOf(r, scrubKey);
|
|
722
|
+
if (k !== null && liveKeys.has(k))
|
|
723
|
+
continue;
|
|
724
|
+
const sets = [];
|
|
725
|
+
const values = [];
|
|
726
|
+
const defaults = [];
|
|
727
|
+
for (const col of local) {
|
|
728
|
+
const v = r[col] ?? null;
|
|
729
|
+
if (!hasValue(v) || isVaultRemotePath(typeof v === 'string' ? v : null))
|
|
730
|
+
continue;
|
|
731
|
+
const relocatable = relocatableColumn(t, col, false) === 'path';
|
|
732
|
+
if (relocatedHere(v, relocatable))
|
|
733
|
+
continue;
|
|
734
|
+
const column = info.get(col);
|
|
735
|
+
if (column?.notnull !== 1) {
|
|
736
|
+
// Another machine's claim, lease, counter or path (T13007 N5): NULL here.
|
|
737
|
+
sets.push(`${quoteIdent(col)} = NULL`);
|
|
738
|
+
}
|
|
739
|
+
else if (foreignPath(v, relocatable)) {
|
|
740
|
+
// A NOT NULL path: a placeholder that is not a path and never an orphan.
|
|
741
|
+
sets.push(`${quoteIdent(col)} = ?`);
|
|
742
|
+
values.push(`${VAULT_REMOTE_PATH_PREFIX}${t}:${k ?? String(r['rid'])}:${col}`);
|
|
743
|
+
}
|
|
744
|
+
else if (column.dflt_value !== null) {
|
|
745
|
+
// A NOT NULL value with a declared default: the default (the schema's own text).
|
|
746
|
+
defaults.push(`${quoteIdent(col)} = ${column.dflt_value}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (sets.length + defaults.length === 0)
|
|
750
|
+
continue;
|
|
751
|
+
const scrub = (assignments) => staged
|
|
752
|
+
.prepare(`UPDATE ${quoteIdent(t)} SET ${assignments.join(', ')} WHERE rowid = ?`)
|
|
753
|
+
.run(...values, r['rid'] ?? null);
|
|
754
|
+
try {
|
|
755
|
+
scrub([...sets, ...defaults]);
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
// A default another row already holds under a UNIQUE index: that value stays.
|
|
759
|
+
if (sets.length === 0)
|
|
760
|
+
continue;
|
|
761
|
+
if (defaults.length === 0)
|
|
762
|
+
throw err;
|
|
763
|
+
scrub(sets);
|
|
764
|
+
}
|
|
765
|
+
scrubbed += 1;
|
|
766
|
+
}
|
|
767
|
+
if (scrubbed > 0)
|
|
768
|
+
out.scrubbed.push({ table: t, rows: scrubbed });
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
// Carrying this machine's values is not a change to capture, and no
|
|
772
|
+
// side-effect trigger may act on it (journal S2, T12819); guards stay on.
|
|
773
|
+
if (hasTriggerSuspendTable(staged)) {
|
|
774
|
+
withTriggersSuspended(staged, ['capture', 'side-effect'], 'forward', carry);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
carry();
|
|
778
|
+
}
|
|
779
|
+
staged.exec('COMMIT');
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
staged.exec('ROLLBACK');
|
|
783
|
+
throw err;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
staged.close();
|
|
788
|
+
live.close();
|
|
789
|
+
}
|
|
790
|
+
return out;
|
|
791
|
+
}
|
|
792
|
+
function insertRows(db, table, cols, rows) {
|
|
793
|
+
if (rows.length === 0)
|
|
794
|
+
return;
|
|
795
|
+
const insert = db.prepare(`INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(', ')}) VALUES (${cols.map(() => '?').join(', ')})`);
|
|
796
|
+
for (const row of rows)
|
|
797
|
+
insert.run(...cols.map((c) => row[c] ?? null));
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* The keyed hash of a table with no rows, as {@link buildVaultManifest} computes it.
|
|
801
|
+
*
|
|
802
|
+
* @param hashKey - The manifest hash key.
|
|
803
|
+
* @param table - Table name.
|
|
804
|
+
* @returns HMAC-SHA256 hex.
|
|
805
|
+
*/
|
|
806
|
+
export function emptyVaultTableHash(hashKey, table) {
|
|
807
|
+
return crypto
|
|
808
|
+
.createHmac('sha256', Buffer.from(hashKey))
|
|
809
|
+
.update(`cleo-vault-table/v1\n${table}\n0\n`)
|
|
810
|
+
.digest('hex');
|
|
811
|
+
}
|
|
812
|
+
//# sourceMappingURL=vault-manifest.js.map
|