feat: init born-multi recipients.txt + which recipients line (EGB-283)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brian Majewski 2026-06-24 13:03:20 -07:00
parent cad5b66f77
commit 50b73083f1
2 changed files with 51 additions and 4 deletions

21
secrets
View file

@ -1260,6 +1260,11 @@ Your key file has been left untouched."
local pubkey
pubkey=$(get_pubkey)
# EGB-283: born-multi — seed recipients.txt with this store's public key so
# the store is multi-recipient-ready from day one. Committed (not gitignored),
# staged by the first push like .secrets-format.
printf '# self\n%s\n' "$pubkey" > "$RECIPIENTS_FILE"
info "Done! Your public key is:"
echo " $pubkey"
echo ""
@ -2195,6 +2200,22 @@ cmd_which() {
# v1 from v2 during the migration window. v1 = legacy store, no format marker.
echo "format: v$(_store_format)"
# EGB-283: surface the recipient set (store-scoped; one key per team member).
if [ -e "$RECIPIENTS_FILE" ] && [ ! -L "$RECIPIENTS_FILE" ]; then
local rcount=0 rk rn rnames=""
while IFS=$'\t' read -r rk rn; do
rcount=$((rcount + 1))
[ -n "$rn" ] && rnames="${rnames:+$rnames, }$rn"
done < <(_recipients_dump)
if [ -n "$rnames" ]; then
echo "recipients: $rcount ($rnames)"
else
echo "recipients: $rcount"
fi
else
echo "recipients: single-key (no $RECIPIENTS_FILE_NAME)"
fi
# v2 manifest (.secrets.json): validate and summarize. Validation here
# is deliberately fatal (symlink / malformed / unsupported version) so
# `secrets which` doubles as the manifest linter.

View file

@ -7,14 +7,14 @@ make_second_identity() {
BOB_PUB=$(age-keygen -y "$TEST_TMPDIR/bob.txt")
}
@test "push without recipients.txt stays single-key (legacy behavior)" {
@test "push with only-self recipients.txt encrypts to the store key (born-multi)" {
init_with_remote
# init now seeds recipients.txt with self — born-multi store.
[ -e "$SECRETS_DIR/recipients.txt" ]
create_project_dir myproj
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
# No recipients.txt was created by push.
[ ! -e "$SECRETS_DIR/recipients.txt" ]
# Blob decrypts with the store's own key.
# Blob must still decrypt with the store's own key.
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@ -49,6 +49,8 @@ make_second_identity() {
init_with_remote
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '%s\n' "$STORE_PUB" > "$TEST_TMPDIR/elsewhere.txt"
# Remove the born-multi recipients.txt so we can replace it with a symlink.
rm -f "$SECRETS_DIR/recipients.txt"
ln -s "$TEST_TMPDIR/elsewhere.txt" "$SECRETS_DIR/recipients.txt"
create_project_dir myproj
run "$SECRETS_BIN" push
@ -58,6 +60,8 @@ make_second_identity() {
@test "recipients list on a legacy store shows the single derived key" {
init_with_remote
# Simulate a legacy store by removing the born-multi recipients.txt.
rm -f "$SECRETS_DIR/recipients.txt"
run "$SECRETS_BIN" recipients list
[ "$status" -eq 0 ]
[[ "$output" == *"single-key"* ]] || false
@ -117,6 +121,8 @@ make_second_identity() {
create_project_dir myproj
run "$SECRETS_BIN" push
before=$(cat "$SECRETS_DIR/key.txt")
# Simulate a legacy store by removing the born-multi recipients.txt.
rm -f "$SECRETS_DIR/recipients.txt"
run "$SECRETS_BIN" rekey
[ "$status" -eq 0 ]
[ "$(cat "$SECRETS_DIR/key.txt")" != "$before" ]
@ -229,3 +235,23 @@ make_second_identity() {
[ "$status" -ne 0 ]
[[ "$output" == *"No recipient matches"* ]] || false
}
@test "init seeds recipients.txt with the new store key (born-multi)" {
run "$SECRETS_BIN" init
[ "$status" -eq 0 ]
[ -e "$SECRETS_DIR/recipients.txt" ]
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
run cat "$SECRETS_DIR/recipients.txt"
[[ "$output" == *"$STORE_PUB"* ]] || false
}
@test "which reports the recipient count" {
init_with_remote
make_second_identity
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob
create_project_dir myproj
run "$SECRETS_BIN" which
[ "$status" -eq 0 ]
[[ "$output" == *"recipients: 2"* ]] || false
[[ "$output" == *"bob"* ]] || false
}