feat: 'file' external type — whole-file sync for .secrets-files (EGB-652)

Built for the Beacon Android upload keystore: binary files outside the
project root can now ride the encrypted store.

- manifest: 'file <path>' (no keys; keys present = rejected loudly)
- push: encrypts the file verbatim (age is binary-safe)
- pull: restores next to target (atomic same-fs mv), mode 600, TOCTOU
  symlink recheck, divergent existing target backed up to .secrets-bak
- _validate_external_target_path parameterized by type (basename
  restriction stays gradle-properties-only; $HOME/../symlink rails apply
  to both)
- README/help/CLAUDE docs + 7 bats tests (133/133 pass)
This commit is contained in:
Brian Majewski 2026-06-07 06:43:45 -07:00
parent ab45c4c94b
commit e0474f3ce9
5 changed files with 226 additions and 40 deletions

View file

@ -54,7 +54,7 @@ flowchart TD
Files like `.envrc` (direnv) and `.environment-*` are intentionally **not** tracked.
Beyond project files, `secrets` can also sync designated keys from files that live *outside* the project — like `~/.gradle/gradle.properties` — merging them in without clobbering unrelated keys. See [External files (Gradle properties)](#external-files-gradle-properties).
Beyond project files, `secrets` can also sync files that live *outside* the project — designated keys from `~/.gradle/gradle.properties` (merged without clobbering unrelated keys), or whole files like an Android upload keystore. See [External files (Gradle properties)](#external-files-gradle-properties).
## Prerequisites
@ -359,9 +359,9 @@ You declare what to sync in a committed `.secrets-files` manifest at your projec
gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest beaconClerkPkLive
```
- **type**`gradle-properties` (the only supported type today).
- **path** — absolute or `~/`-relative. The basename must be `gradle.properties` and must resolve inside `$HOME`.
- **keys** — the property names to sync. Only these keys are read on push and merged on pull; everything else in the file is left alone.
- **type**`gradle-properties` (sync named keys) or `file` (sync the whole file — see below).
- **path** — absolute or `~/`-relative; must resolve inside `$HOME`. For `gradle-properties` the basename must be `gradle.properties`.
- **keys** — the property names to sync (`gradle-properties` only). Only these keys are read on push and merged on pull; everything else in the file is left alone. `file` entries take no keys.
#### Syncing to a second machine
@ -389,6 +389,17 @@ secrets pull
> **Note:** unlike `.env` files, merged Gradle keys are written as **permanent plaintext** into the target file — `secrets clear` does **not** remove them. This is appropriate for publishable / low-secrecy values (like Clerk publishable keys, `pk_*`). For high-value secrets that should never sit on disk, use `secrets run` with a `.env` instead.
#### Whole files (`file` type)
Some external secrets are whole binary files — an Android upload keystore, a certificate. The `file` type syncs the file verbatim (binary-safe, encrypted with age like everything else):
```
# <type> <path>
file ~/keystores/beacon-upload.keystore
```
On `secrets push` the file is encrypted into `<project>/external/`. On `secrets pull` it is restored to the same path with mode `600`; if a different version already exists there, it is backed up to `<name>.secrets-bak` first. The same path rules apply (inside `$HOME`, no `..`, no symlinks). Like merged Gradle keys, restored files are permanent plaintext on disk — `secrets clear` does not remove them.
## Safety features
- **`secrets run` auto-clears** — plaintext files are deleted when the command exits, errors, or is interrupted with Ctrl-C