Commit graph

115 commits

Author SHA1 Message Date
Brian Majewski
d984e2abc7 docs: repoint forge references from Codeberg to git.dev.egbt.com
The origin moved to the private Forgejo instance at git.dev.egbt.com
(egbt/secrets). Git history, all 16 branches, 13 tags, and all 13 releases
were migrated with identical SHAs and release bodies.

- CLAUDE.md: replace the "Codeberg operations" section with "Forge
  operations", switching tea invocations to `--login egbt --repo
  egbt/secrets`. Records three things that are easy to get wrong:
  Forgejo's SSH listens on port 2222 (port 22 is the host's own sshd, so
  a bare git@host:path fails with "Permission denied (publickey)"); the
  host is Tailscale-only, so off-VPN failures are connectivity, not
  credentials; and the stale `codeberg` tea login is still configured and
  can be silently fallen back to in non-interactive mode.
- CLAUDE.md: note the instance has an Actions runner available, though no
  workflow is configured yet — local bats remains the merge gate.
- README.md: clone URLs now point at the new host.
- CHANGELOG.md: release links now point at the new host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
2026-09-08 16:00:54 -07:00
EGBT Technologies
c83935a92f Merge pull request 'v0.7.7.0 fix: discover workspaces in pnpm and yarn monorepos (EGB-1232)' (#16) from brian/egb-1232-pnpm-workspace-discovery into main 2026-09-09 00:26:05 +02:00
Brian Majewski
c09ac38b16 fix: discover workspaces in pnpm and yarn monorepos (EGB-1232)
Workspace patterns came from package.json's `workspaces` key only, at both
call sites. pnpm declares them in pnpm-workspace.yaml instead, so no pnpm
monorepo ever resolved a workspace: `push -w` refused outright, and plain
`push` failed silently — _maybe_workspace_env_files returned 0 the moment
the key was absent, leaving the auto-discovery that covers push's root-only
scan inert and printing "Nothing new to add", indistinguishable from a repo
with genuinely nothing new. That silence cost two sessions on the same repo.

Both call sites now resolve through one shared source, _workspace_patterns:
package.json when it declares any, else pnpm-workspace.yaml's `packages:`
block. The YAML read is deliberately not a parser — block sequence only,
stopping at the next top-level key so pnpm 10's onlyBuiltDependencies:/
catalog: cannot leak in as globs, with quote/comment handling and a symlink
refusal.

Also fixes yarn's object form. `.workspaces // .workspaces.packages | .[]`
short-circuits on the truthy object, iterating its values and yielding the
pattern array as one token; only npm's array form ever worked. Note the
obvious reorder is NOT the fix — `.workspaces.packages` errors on an array —
so the filter is type-aware.

Patterns are validated before reaching the unquoted glob expansion (no
absolute paths, `..`, metacharacters, or whitespace; pnpm `!` negations
skipped), matching the .secrets-store/.secrets-files posture. jq is now
required only when package.json is the source. A monorepo-shaped root that
resolves nothing warns and points at `secrets add` instead of returning in
silence, and `-w`'s error names pnpm-workspace.yaml when that is the file
present.

Scope note: the workspace re-scan still runs only for projects that already
have a .secrets.json — push's root-scan-only first push is by design
(EGB-677 E13), and this bug is the fallback covering it never engaging.

test/workspaces.bats: 18 new tests. Full suite 371/371 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
2026-09-08 15:22:04 -07:00
EGBT Technologies
b3a727c8fb Merge pull request 'v0.7.6.0 fix: loud store sync failures + secrets sync reconcile (EGB-1230, EGB-1231)' (#15) from brian/egb-1230-1231-store-sync into main 2026-09-08 23:32:25 +02:00
Brian Majewski
f1cf9d9e7e docs: correct Codeberg tea invocation in CLAUDE.md
`tea` autodetection fails in this repo, and the machine carries a second
login (`egbt`) pointing at a different forge that tea silently falls back
to in non-interactive mode. Document the explicit
`--login codeberg --repo egbt/secrets` form for every verb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
2026-09-08 14:31:16 -07:00
Brian Majewski
dd6a025fa0 fix: surface store sync failures + add secrets sync (EGB-1230, EGB-1231)
EGB-1230: `cmd_pull` synced the store with `git pull >/dev/null 2>&1` under
`set -euo pipefail`. A store that couldn't fast-forward killed the script at
that line with git's exit 128 and nothing on stdout or stderr — a banner, no
restored files, no reason, and invisible in a pipeline. The sync now routes
through `_store_sync_pull`, which guards the pull, captures git's output as
the diagnosis, and dies naming the store path and `secrets sync`. It is now
`--ff-only` to match the push path, so a plain pull can no longer manufacture
a merge commit in the store.

EGB-1231: once a store diverged there was no way out — push demanded a
fast-forward and pointed at pull, which couldn't fast-forward either, so the
advice looped and recovery meant hand-running git next to encrypted blobs.
Adds `secrets sync`: fetch, stash, rebase onto the remote, restore the stash,
then a confirmation-gated push of local commits (`--yes` to skip the prompt,
`--dry-run` to report only). Non-destructive by construction — no merge, no
force-push, no `reset --hard`, no `stash drop`; a rebase conflict names the
conflicting files and leaves the store exactly as found. `secrets which` gains
a `remote:` line reporting ahead/behind/dirty, and push's dead-end message now
points at `sync`.

test/sync.bats: 25 new tests. Full suite 353/353 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
2026-09-08 14:20:20 -07:00
EGBT Technologies
2558ea3c23 Merge pull request 'v0.7.5.0 feat: multi-recipient age encryption (recipients/reencrypt, EGB-283)' (#14) from brian/egb-283-secrets-multi-recipient-age-encryption-multiple-keys-per into main 2026-06-24 23:55:29 +02:00
Brian Majewski
e29024bd63 Merge origin/main into EGB-283 (multi-recipient age encryption)
Reconcile the multi-recipient branch (cut from v0.6.1.0) with origin/main
at v0.7.4.0. The two feature lines are largely orthogonal; the one real
integration point is the external-blob encrypt path:

- EGB-712 added an additive-v2 dual-write loop (_external_blob_write_targets,
  writing v2 + any v1 twin). EGB-283 routes every encrypt site through
  RECIPIENT_ARGS for N-recipient encryption. Resolution keeps the dual-write
  loop but encrypts each target to the full recipient set
  (age "${RECIPIENT_ARGS[@]}" per write target), so dual-write and
  multi-recipient compose. cmd_push loads recipients before both external
  push sites; legacy single-key rekey keeps its fresh-keypair pubkey path.

Version: 0.6.2.0 + 0.7.4.0 -> 0.7.5.0. Docs (CLAUDE.md/README/CHANGELOG)
merged to carry both feature sets; subcommand list now includes
recipients/reencrypt and upgrade.

Tests: full `bats test/` green except 6 pre-existing host-environment
failures (4 chmod-600 restore assertions + 2 jq-PATH-shadow tests, all
macOS-authored), none touching merged code. recipients.bats 34/34 pass;
external/dual-write area passes except the same mode-600 host artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:52:32 -07:00
Brian Majewski
17772dfec6 fix: validate-before-mutate in recipients add/rm + verify messaging + doc/UX polish (EGB-283)
- Fix 1 (IMPORTANT): _recipients_rm and _recipients_add now call _load_recipients
  BEFORE any mutation. A hand-corrupted recipients.txt dies at validation, leaving
  the file untouched — prevents inconsistent state where the file is changed but
  blobs are not re-encrypted. On a legacy store (no recipients.txt), _load_recipients
  succeeds via the derived-pubkey path so the bootstrap path still works.

- Fix 2 (MINOR): Guard _check_blob_recipient_count behind a successful decrypt in
  both _verify_all and _verify_project — an undecryptable blob no longer produces a
  spurious "encrypted to 0 recipients" finding. Reword _verify_all summary to
  "failed (decrypt or recipient-count)" since both failure modes now increment the
  counter.

- Fix 3 (MINOR): Correct README offboarding comment from "New blobs are no longer
  readable" (contradicts the re-encrypt of EVERY blob) to "Existing blobs are
  re-encrypted; the removed key can no longer decrypt them."

- Fix 4 (MINOR): cmd_reencrypt prints an advisory when no recipients.txt exists
  (single-key store), so the operator knows they can add teammates.

- Fix 5 (MINOR): Test coverage for ambiguous-name rm refusing to remove when
  multiple recipients share a --name label.

Tests: 5 new tests in test/recipients.bats (34 total, all pass). Full suite
276 tests: 5 known pre-existing failures (3 mode-600/stat, 2 jq-PATH), none new.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:37:56 -07:00
Brian Majewski
f7576a3eae docs: multi-recipient age encryption (recipients/reencrypt) + bump 0.6.2.0 (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:25:33 -07:00
Brian Majewski
b09f94c92f test: pin security fixtures to the validation rail (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:21:33 -07:00
Brian Majewski
28f44f043c test: recipients.txt security regression fixtures + dangling-symlink fix (EGB-283)
Swap _load_recipients check order so [ -L ] (symlink) runs before [ ! -e ]
(missing), closing the gap where a dangling symlink bypassed the refusal
and silently fell back to legacy single-key mode.

Add 6 SECURITY-tagged fixtures to test/recipients.bats: dangling-symlink
refused (the ordering gap), shell-metachar injection (no execution), extra-
age-flag-looking line, control/ANSI chars, embedded whitespace via add, and
symlinked file refused on add. All 6 pass immediately after the ordering fix;
the existing rails (_validate_age_recipient, -L checks) were already tight
enough that only the production swap was needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:17:28 -07:00
Brian Majewski
e4cd524483 feat: verify asserts blob recipient-count matches recipients.txt (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:10:53 -07:00
Brian Majewski
ae52b8c077 test: restore bootstrap-path + which-legacy coverage (EGB-283)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:06:57 -07:00
Brian Majewski
50b73083f1 feat: init born-multi recipients.txt + which recipients line (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:03:20 -07:00
Brian Majewski
cad5b66f77 feat: secrets recipients rm with lockout guards (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:56:50 -07:00
Brian Majewski
4ba0234bd7 fix: harden recipients add --name validation (EGB-283)
Require a non-empty argument after --name (dies if it is the last token)
and reject whitespace-only labels that would write a blank comment line.
Two regression tests added to test/recipients.bats.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:53:27 -07:00
Brian Majewski
c262661030 feat: secrets recipients add (EGB-283)
Add `secrets recipients add <age1...> [--name <label>]`: validates the
age key and optional display name, bootstraps recipients.txt with the
local pubkey on a legacy store (keeping the operator as a recipient),
rejects duplicates, appends the new key (with optional name comment),
then re-encrypts the entire store to the updated recipient list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:50:00 -07:00
Brian Majewski
a10f390519 feat: shared _reencrypt_all + reencrypt cmd + dual rekey (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:44:44 -07:00
Brian Majewski
649aa78063 feat: secrets recipients list (EGB-283) 2026-06-24 12:39:52 -07:00
Brian Majewski
14cf86fba9 docs: plan fixups from Task 1 review (blob path, dangling-symlink fix to Task 8)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:37:55 -07:00
Brian Majewski
0f9de1c2fd feat: multi-recipient encrypt core + recipients.txt (EGB-283)
- Add RECIPIENTS_FILE_NAME / RECIPIENTS_FILE constants; update resolve_store to re-derive RECIPIENTS_FILE after store resolution.
- Add _validate_age_recipient (native age1 X25519 key format check, injection rail).
- Add RECIPIENT_ARGS global array and _load_recipients (absent → single pubkey legacy path; present → parse+validate recipients.txt, refuse symlink, die on bad/empty).
- Rewire all 5 push encrypt sites (push_dir_to_project, cmd_push inline, push_external_files ×2, cmd_push_workspaces) to use RECIPIENT_ARGS; drop pubkey threading from push_dir_to_project and push_external_files signatures.
- New test/recipients.bats (4 tests): legacy single-key, multi-recipient decrypt, invalid key rejection, symlink rejection.
- Fix test/test_helper.bash: set GIT_AUTHOR/COMMITTER env vars so git commit works with isolated $HOME.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:33:05 -07:00
Brian Majewski
3a7eea5529 docs: multi-recipient age encryption implementation plan (EGB-283)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:20:32 -07:00
Brian Majewski
9e2a563059 docs: multi-recipient age encryption design spec (EGB-283)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:12:14 -07:00
EGBT Technologies
6b98ee8208 Merge pull request 'v0.7.4.0 feat: secrets upgrade verb — self-update + re-check version skew (EGB-716)' (#13) from brian/egb-716-secrets-secrets-upgrade-verb-self-update-re-check-version into main 2026-06-18 21:16:21 +02:00
Brian Majewski
a17ae4448b chore: bump version and changelog (v0.7.4.0)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:01:17 -07:00
Brian Majewski
09c4ad54f9 feat: secrets upgrade verb — self-update + re-check version skew (EGB-716)
Pairs the EGB-713 skew WARNING with a fix path. `secrets upgrade` fast-forwards
the tool's own git checkout (git -C "$SCRIPT_DIR" pull --ff-only; never merges or
rewrites local commits), reports vOLD -> vNEW, then best-effort re-checks the
store's recorded writer-version against the new version so the operator sees
whether the nudge is cleared. `secrets upgrade --check` reports availability
without pulling. Thin and explicit: no auto-update, no background polling
(security tool). Directed errors for not-a-checkout / no-upstream / diverged /
offline. cmd_upgrade never calls check_initialized (it's about the tool, not
the store); the skew re-check is silent unless a store with a writer-version
resolves.

Wired into the dispatcher (upgrade) shift; cmd_upgrade "$@") and cmd_help.
Tests: test/upgrade.bats (8) run a relocated script copy in a throwaway git
repo with a bare upstream, so the real checkout is never touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:01:17 -07:00
EGBT Technologies
33a5bb0758 Merge pull request 'v0.7.3.1 refactor: external-extractor + read-guard dedup, legacy-pull nested-blob warning (EGB-701)' (#12) from brian/egb-701-secrets-stage-1-structural-cleanups-dedup-external-entry into main 2026-06-18 19:40:18 +02:00
Brian Majewski
3ece393cc5 chore: bump version and changelog (v0.7.3.1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:08:13 -07:00
Brian Majewski
1ee096cc33 refactor: dedup external extractor + read guards, warn on legacy-pull nested blobs (EGB-701)
EGB-677 stage-1 structural cleanups, no behavior change for the v2 happy path:

1. cmd_which reuses _json_external_entries (the push/pull extractor) instead of
   its own duplicated jq @tsv projection, so `which` applies the same
   properties->gradle-properties normalization + skip-with-warning rules the
   sync path does and can't drift from it.
2. The two external-manifest read guards are factored into _json_readable
   (plain regular file, silent) / _legacy_readable (warn+skip a symlinked
   .secrets-files), shared by _external_entries_for_push/_pull.
3. The legacy (manifest-less) pull path now warns when nested <project>/<relpath>.age
   blobs exist that its non-recursive globs can't see (external/ excluded —
   pull_external_files handles those), so it never silently under-restores.

Tests: +4 in test/manifest.bats (normalized which display, malformed external
skipped by which, nested-blob warning fires, external-only no false warning).
Full suite green (286/286).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:08:13 -07:00
EGBT Technologies
57f1280262 Merge pull request 'v0.7.3.0 feat: secrets join + verified onboarding scripts (EGB-671)' (#11) from brian/egb-671-create-real-install-scripts into main 2026-06-09 01:30:08 +02:00
Brian Majewski
4d975d447d chore: rewrite onboarding docs + bump version (v0.7.3.0)
README rewritten to the install.sh + init --remote + join flow; dropped the
macOS-only prerequisite (age+jq install hints now cover apt/dnf too). CHANGELOG
entry for EGB-671.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:23:50 -07:00
Brian Majewski
7b041af68b feat: thin install.sh onboarding bootstrap (EGB-671)
Ships in the repo (clone already done). Checks age + jq + git, then PRINTS the
PATH line, onboarding next-steps, upgrade one-liner, and key-transfer hint.
Never edits shell rc, never runs sudo (prints the command). Exits non-zero with
an install hint when a dependency is missing.
2026-06-08 16:23:50 -07:00
Brian Majewski
6319313ee4 feat: secrets join + init --remote + verified onboarding (EGB-671)
Add second-machine onboarding as a first-class verb rather than a manual
clone + key-copy sequence:

- secrets join --remote <url> --key <path>: clone the vault, install the key
  at mode 600, then decrypt-test it before declaring success. An empty vault
  reports "nothing to verify yet" (never a false VERIFIED); a wrong key fails
  loudly. Reuses the audited core (resolve_store, get_pubkey, _verify_all) —
  no security logic re-implemented.
- secrets init --remote <url>: wire the remote and push the initial store so
  the upstream branch exists (fixes the commit_and_push_secrets pull --ff-only
  die against a brand-new empty remote). init also offers an interactive
  first-add of a project (default No; skipped under --yes / non-interactive).
- cmd_push first-manifest scaffold writes an explicit committed options.autoAdd
  value, asked once when interactive (EGB-677 contract #2).
- secrets pull now dies loudly when a blob fails to decrypt (all three decrypt
  paths) instead of warning and exiting 0 — a wrong key can't pass silently.
- Interactive prompts gate on stdin AND stdout being ttys, so bats/CI never hang.
- Dispatcher routes init/join args correctly; second-machine trap points at join.

Tests: 20 new (join, autoAdd, pty-no-hang regression); 2 trap tests updated.
2026-06-08 16:23:50 -07:00
EGBT Technologies
ec538d7ef0 Merge pull request 'v0.7.2.0 feat: secrets list --json machine-readable output (EGB-699)' (#10) from brian/egb-699-secrets-list-json-machine-readable-output into main 2026-06-08 23:00:47 +02:00
Brian Majewski
446256caf1 feat: secrets list --json machine-readable output (EGB-699)
Add a --json flag to `secrets list` that emits a structured object for
tooling/CI instead of the human table — feeds the EGB-671 install scripts,
which need to enumerate a cloned store programmatically.

Contract: {"store", "projects":[{"name","entries":[...]}]}, each entry
self-describing via a type discriminator — {type:dotenv,path} or
{type:external,subtype:properties|file,path}. cmd_list_json mirrors the same
recursive store walk as the human list (nested <project>/<relpath>.age +
external/<slug>.age); jq assembles the JSON so paths escape correctly and
stdout stays pure JSON (the non-default-store hint is suppressed; jq is a
hard dep only in --json mode).

Tests: 7 new bats cases (dotenv, nested relpath, external properties + file
subtypes, empty store, pure-stdout-under-notice, store path). Full suite
261 pass / 0 fail.

VERSION 0.7.1.0 -> 0.7.2.0; CHANGELOG/README/CLAUDE.md updated.
2026-06-08 13:56:45 -07:00
EGBT Technologies
b8fe20f9bf Merge pull request 'v0.7.1.0 feat: version-skew nudge (EGB-713)' (#9) from brian/egb-713-secrets-warn-when-the-store-was-written-by-a-newer-client into main 2026-06-08 21:11:31 +02:00
Brian Majewski
50476e19fd docs: version-skew nudge + writer-version; bump 0.7.1.0 (EGB-713) 2026-06-08 12:07:51 -07:00
Brian Majewski
5461418c5d feat: version-skew nudge — stamp store writer-version, warn when behind (EGB-713) 2026-06-08 12:07:50 -07:00
Brian Majewski
367b70cba1 docs: EGB-713 version-skew nudge plan 2026-06-08 11:56:47 -07:00
EGBT Technologies
40635f9e1f Merge pull request 'v0.7.0.0 feat: additive-v2 dual-write — optional finalize (EGB-712)' (#8) from brian/egb-712-secrets-additive-v2-dual-write-defuse-the-finalize into main 2026-06-08 20:01:47 +02:00
Brian Majewski
9b9af2f30c docs: additive-v2 propagation + optional-GC finalize; bump 0.7.0.0 (EGB-712) 2026-06-08 10:52:34 -07:00
Brian Majewski
040782cad0 feat: secrets migrate --status surfaces v2-only externals (coverage, EGB-712) 2026-06-08 10:44:34 -07:00
Brian Majewski
69ab9636e9 test: fabricate old-client v1 blobs in migrate/finalize/status fixtures (additive v2, EGB-712) 2026-06-08 10:38:18 -07:00
Brian Majewski
2f36fe1898 feat: twin-rule write targets — dual-write existing, v2-only for new (additive v2, EGB-712) 2026-06-08 10:31:00 -07:00
Brian Majewski
2866e5f4b1 feat: read-resolver tries both external suffixes (additive v2, EGB-712) 2026-06-08 10:08:41 -07:00
Brian Majewski
ee4ea413ef docs: additive-v2 implementation plan + spec §3 fix (no marker auto-stamp) 2026-06-08 09:58:23 -07:00
Brian Majewski
54575af61c docs: additive-v2 dual-write design spec (defuse the finalize gate) 2026-06-08 09:45:10 -07:00
EGBT Technologies
836b418a12 Merge pull request 'v0.6.1.0 feat: secrets migrate guided flow (manifest-free + --status, EGB-710)' (#7) from brian/egb-710-secrets-make-migrate-a-guidedinteractive-flow-not-a-dead-end into main 2026-06-08 18:06:18 +02:00
Brian Majewski
3ac2a86a94 docs: fix stale manifest-dependency comments in migrate (EGB-710) 2026-06-08 08:26:16 -07:00