List remote snapshots without requiring the private key (closes #64)
All checks were successful
check / check (push) Successful in 6s

ListSnapshots built its table entirely from the local SQLite index. The
only remote access, reportRemoteDrift, was gated on AgeSecretKey != "",
so on a correctly configured host - which by design holds no private key
- snapshot list never contacted the destination store at all. A user who
lost their local index could not see their own backups, and the
"<remote only>" cell the README documents was unreachable dead code.

The listing is now the union of the local index and the destination
store, with no age_secret_key gate. Remote-only snapshots cannot have
their hostname or name recovered - RemoteSnapshotKey is one-way and the
manifest stores the hash - so they are listed by abbreviated remote key
with the real timestamp and compressed size from the manifest, and
"<remote only>" in the two columns that require the local index. Nothing
new is written to remote storage and the human ID is never fabricated.

An unreachable destination degrades to local-only with a warning and a
zero exit code. remote_present is null rather than false in that case,
so "absent" and "unknown" stay distinguishable and no drift is claimed
from a listing that never happened.

Also:

- Snapshot timestamps are normalised to UTC in scanSnapshotRows, the
  single point where they enter the domain. Previously one of three
  scanners omitted .UTC(), so on a non-UTC host the same snapshot
  rendered a different time depending on whether it was locally tracked.
- The 1000-row cap and the unreadable-manifest count are reported in
  --json mode as well as table mode, so machine consumers cannot be
  silently truncated. The JSON shape is unchanged.
- Warnings raised while listing are routed to stderr rather than the
  logger, which writes to stdout and would corrupt the JSON document.
  This is a local workaround for the logger bug tracked in #82 and
  should be removed when that lands.
- downloadManifestByKey is now the only remote manifest reader, so the
  manifest privacy question in #81 has a single call site to change.
- The orphaned "vaultik snapshot cleanup" hint now names vaultik prune;
  that command was folded into prune by the 2026-07-02 consolidation.
This commit was merged in pull request #83.
This commit is contained in:
2026-08-09 07:34:15 +02:00
parent af607e3597
commit 50e20b460e
10 changed files with 1637 additions and 314 deletions

31
TODO.md
View File

@@ -19,6 +19,37 @@ or delete the branch.
# Completed Steps
- 2026-08-09: Made `snapshot list` list the destination store without
the private key (issue #64). The listing is now the union of the
local index and a single streamed listing of the `metadata/` prefix,
with no `age_secret_key` gate — the manifest is unencrypted, so a
host holding only the public key can enumerate its own backups and a
host that lost its local index can still see them. A remote-only
snapshot's hostname and name are deliberately not recovered (they are
not recoverable without the private key, and making them so would
undo the privacy property tracked in issue #81); such rows are
labelled by an abbreviation of their remote key and carry the real
timestamp and compressed size from the manifest, with `<remote only>`
in the two columns that require the local index. Local-only snapshots
are reported as drift, and the hint now names `vaultik prune`, which
exists, instead of `vaultik snapshot cleanup`, which does not.
`reportRemoteDrift` collapsed into the merged view. Every remote
manifest read in the codebase now goes through
`downloadManifestByKey`, so issue #81 has one call site to change.
Review rework: snapshot timestamps now normalize to UTC in
`scanSnapshotRows`, the one place they enter the domain, so the merged
TIMESTAMP column cannot show local time for a locally tracked row and
UTC for a remote-only row on a non-UTC host; `GetIncompleteByHostname`
was folded onto that same scanner. `--json` now reports the
unreadable-manifest count and the 1000-row truncation on stderr
instead of returning a silently short document (the document's shape
is unchanged). The two per-snapshot `log.Warn` calls on the listing
path now route through the same JSON-aware writer as the existing
workaround, so one corrupt manifest can no longer put a log line on
stdout ahead of the document and break `| jq` — still a local
workaround pending issue #82. Verified with `script/cibuild` and with
an uncached `make check` (`0 issues.`, no cached test packages), plus
end to end against a `file://` destination with no secret key present.
- 2026-08-09: Closed the gap between `make lint` and CI (issue #78).
`script/lint` now runs the digest-pinned `golangci-lint` image taken
from the `Dockerfile` lint stage, which is the single source of truth