Commit graph

11 commits

Author SHA1 Message Date
Brian Majewski
414c02b902 fix: pre-landing review fixes for verify (test assertions, ekeys discard, double-report, docs)
Pre-landing review (0 critical, all informational) auto-fixes:
- Tighten external-corrupt test to assert the decrypt-fail message, not any
  external finding (was *"external"*, now *"does not decrypt"*).
- Pin the verified-count in the nested+external happy-path test so a silent
  under-count (exit 0 while skipping a blob) is caught.
- Account for an unsafe dotenv entry in `expected` so a matching stray blob
  isn't double-reported as both unsafe and orphan.
- Discard the unused external `keys` read field (read -r etype epath _).
- Document the optional [project] positional in the README verify row.

Deferred to EGB-701 (stage-2 dedup): the external blob-path literal and the
find-walk overlap with cmd_rekey/cmd_list. Full suite 210/210.
2026-06-07 15:22:58 -07:00
Brian Majewski
33aad4f89a test: coverage for verify gap branches (external decrypt-fail, unsafe path, unknown flag, malformed manifest, empty --all)
Coverage audit found 5 untested branches in cmd_verify (all single-test
fills, no logic defects): external blob decrypt-failure (only the missing
case was covered), the rail-skip finding for an unsafe dotenv path in the
manifest, the unknown-flag die, a malformed manifest through the verify
entry point, and the empty-store 'verify --all' no-op. Full suite 210/210.
2026-06-07 15:14:54 -07:00
Brian Majewski
52528f2e06 feat: secrets verify — manifest↔store consistency + decrypt integrity (EGB-698)
Read-only integrity check, the safety net for the stage-2 store migration.

Default mode (current project) cross-checks $PWD/.secrets.json against the
store both ways — declared-but-missing blobs and orphaned blobs (no manifest
entry) — and decrypt-tests every dotenv + external blob with the current key,
streaming plaintext to /dev/null so nothing is ever written to disk. `verify
--all` decrypt-tests every blob in every project (integrity only; the store
carries no manifests, so consistency can't be checked store-wide). Both
recurse the whole project tree (find -type f), the same walk rekey/list use,
so nested manifest blobs are covered. Exits non-zero on any finding so it can
gate `migrate --finalize` and CI.

12 bats tests (clean, nested+external, missing blob, decrypt failure, orphan,
missing external, no-manifest die, symlink refusal, --all clean/corrupt/orphan,
nested decrypt failure). Full suite 205/205. bash 3.2 clean.
2026-06-07 14:55:42 -07:00
Brian Majewski
c6ea724ddb fix: rekey and list recurse into nested manifest blobs (EGB-677 stage 1)
Pre-landing review (testing + checklist specialists, reproduced) caught a
data-loss bug: cmd_rekey's decrypt/re-encrypt globs were non-recursive and
only special-cased external/. Nested manifest dotenv blobs
(<project>/<relpath>.age, new this branch) were never visited, so after a key
rotation they stayed encrypted under the discarded old key = permanently
undecryptable. cmd_list had the same blind spot (cosmetic: nested entries
invisible in listings).

Both now walk the entire project tree with `find -type f` (bash 3.2 safe,
includes dotfiles natively), unifying top-level / nested / external blobs into
one recursive pass and dropping the now-redundant external/ special-casing.

Regression tests: nested-blob rekey round-trip (survives rotation) + list
shows nested entry. Full suite 193/193.
2026-06-07 13:12:27 -07:00
Brian Majewski
5489f89446 test: coverage backfill — which UNSAFE marker, malformed-manifest add, dry-run declared list, frozen absorb suppression, file-type absorb round-trip, external dedup 2026-06-07 10:17:07 -07:00
Brian Majewski
ecc4f23494 test: coverage for stage-1 gap paths (json rail warns, symlink legacy, directed errors, dry-run/which branches) 2026-06-07 09:40:43 -07:00
Brian Majewski
89e851278b feat: jq gating, platform-aware install hints, stage-1 docs (EGB-677 stage 1)
- jq required only when .secrets.json exists or is being written;
  manifest-less projects run jq-free (features skipped with a notice) —
  clone-and-run for v1 users survives (no-jq fixture excludes /usr/bin,
  macOS ships jq there now)
- check_cmd: platform-aware hints (brew/apt-get/dnf/generic) instead of
  hardcoded brew — correct guidance on Linux/CI
- cmd_help: add command, push flags, manifest section with example
- README: manifest section, external files rewritten around
  .secrets.json (legacy .secrets-files documented as absorbed),
  troubleshooting entries, command table, test instructions
- CLAUDE.md: manifest architecture notes, bash-3.2 '[[ ]] || false'
  testing convention, project structure refresh
2026-06-07 08:58:16 -07:00
Brian Majewski
0049584d9b feat: manifest-driven pull — nested restore, restore-time rail, empty no-op (EGB-677 stage 1)
- pull with .secrets.json restores exactly the declared entries (nested
  paths get mkdir -p); stray store blobs are not restored
- dotenv rail re-runs at restore time: unsafe entries warn+skip (pull
  never dies on one bad entry), missing blobs warn with a directed hint
- empty manifest = warn no-op instead of a confusing 'not found' death
- manifest-less projects keep the legacy glob pull verbatim
2026-06-07 08:49:02 -07:00
Brian Majewski
d7e1400487 feat: external entries via .secrets.json + legacy absorb + properties rail (EGB-677 stage 1)
- .secrets.json external[] drives push/pull: type 'properties' (alias of
  gradle-properties; blob suffix stays legacy-compatible in stage 1) and
  type 'file'; same charset rails as the legacy parser
- push absorbs uncovered .secrets-files entries into the manifest
  (idempotent, gradle-properties → properties) with a delete hint
- pull: manifest wins entirely; a coexisting .secrets-files warns as
  superseded instead of being silently ignored
- basename rail generalized: properties targets must end '.properties'
  (was exact 'gradle.properties') — rc files/gitconfig still blocked;
  EGB-531 wrong-basename test updated for the sanctioned change
2026-06-07 08:44:02 -07:00
Brian Majewski
884da0965c feat: manifest-aware push — generator auto-add, autoAdd toggle, --frozen/--dry-run (EGB-677 stage 1)
- push syncs FROM the manifest; v1 store layout unchanged (nested
  entries land at <project>/<relpath>.age, same shape -w always used)
- discovery (root globs + quiet package.json workspace re-scan when a
  manifest exists) feeds the manifest as a generator; new files auto-add
  with ==> notice + undo guidance
- options.autoAdd committed toggle (default ON when absent); explicit
  false warns on undeclared files instead of enrolling them
- push --frozen: declared-only for one invocation; push --dry-run:
  reports would-add/would-sync, touches nothing
- bootstrap ordering: manifest written only after >=1 blob encrypts
- declared-but-missing warns and continues; unsafe manifest path dies
- jq // falsy gotcha: explicit autoAdd:false compared directly
2026-06-07 08:32:17 -07:00
Brian Majewski
18018dbd3b feat: .secrets.json manifest core — add command, rails, canonical form (EGB-677 stage 1)
- secrets add <path>: explicit manifest writer, idempotent, atomic write
- _validate_dotenv_rel_path: project-relative confinement rail (no .. /
  absolute / shell metas; @ allowed for npm-scoped workspace dirs)
- _check_manifest_file: refuses symlinks, malformed JSON (jq error with
  file named), unsupported schema versions (directed upgrade error)
- canonical serialization: jq --sort-keys + sorted/deduped dotenv —
  add order produces byte-identical manifests
- which: validates + summarizes the manifest (doubles as linter)
- jq required only when a manifest exists/is written
2026-06-07 08:24:35 -07:00