secret store holds foreign secrets, and notify reads from it

putSecret/getSecret, `bun run secret:set`, and notify taking its credentials
from the store with env as the fallback. this was described two messages ago
as a later thing; it is about fifteen lines and it was blocking the honest
answer to "where does the .p8 go", so it is done.

getKey was the wrong door for anything supplied from outside — it MINTS a
random 32-byte value when the purpose is absent, which for a foreign
credential means silently authenticating to apple with a fake. getSecret
returns null instead, which is what lets the env fallback work.

putSecret retires the previous row rather than deleting it: a mistyped paste
over a working key should be recoverable, and retiredKeys already exists for
that shape. one transaction, because the partial unique index means the retire
has to land before the insert.

the CLI reads stdin and has deliberately no --value flag: a secret in argv is
in shell history, in ps output, and in any audit log that records command
lines. offering the flag would make the convenient path the leaky one. it
prints a length and the last four characters, never the value.

also `bun run notify:test`, which fires officer/core:test at a registered
device. the sidecar's port is ephemeral, so it now writes its address to
data/notify-server.url on boot and removes it on clean shutdown; the CLI
treats an unreachable address as "not running" rather than trusting the file,
because a crash leaves one behind.

the script says what actually happened, because the day-waster here is that a
send with nothing configured returns success and delivers nothing — it refuses
up front when channels is empty, and calls out sent:0/failed:0 as "no device
registered" rather than letting it read as success.

verified end to end with a p-256 key generated locally and then deleted from
the store: zero APNS vars in .env, sidecar reported channels: apns, and the
send reached the channel. store is back to jwt and offscale only.
This commit is contained in:
2026-08-19 19:57:51 +00:00
parent 2cefa3d378
commit 16369f3570
10 changed files with 288 additions and 27 deletions
+16 -9
View File
@@ -205,16 +205,23 @@ Env only, on the sidecar, never in the platform process and never in the databas
The `.p8` does not expire and can push to your apps forever. Treat it as spending-grade.
**Env is the interim, not the destination.** These belong in the secret store
(`src/databases/officer_db/src/secret-store.ts`, built 2026-08-13), and for the reason that store exists:
bun auto-loads `.env` into every PM2 process, so a key there is readable from `/proc/<pid>/environ` of
twenty processes — `officer-music` has no business being able to read a credential that can push to your
apps forever.
**The secret store is where these live now** — env is only the fallback.
It cannot move there yet, and the obstacle is not a missing purpose string. `getKey(purpose)` **generates**
a key; there is no setter, because every secret in the store so far is one the platform minted itself. The
`.p8` and the FCM service account come from outside, so holding them needs a `putSecret` first. Tracked in
`TODO.md`.
```bash
cat AuthKey_ABCD123456.p8 | bun run secret:set apns-p8
echo -n ABCD123456 | bun run secret:set apns-key-id
echo -n RGHC4NJC77 | bun run secret:set apns-team-id
pm2 restart officer-notify # _health should then report channels: ["apns"]
```
Purposes: `apns-p8`, `apns-key-id`, `apns-team-id`, `fcm-service-account`, `discord-webhook`. Read with
`getSecret` and NOT `getKey` — `getKey` mints a random value for an absent purpose, which for a credential
supplied from outside would mean silently authenticating to Apple with a fake.
Env is still read when the store has nothing, so an install that predates this keeps working. But it is a
fallback: bun auto-loads `.env` into every PM2 process, so a key there is readable from `/proc/<pid>/environ`
of twenty of them, and `officer-music` has no business being able to read a credential that can push to your
apps forever.
`APNS_ENV` appeared in the first design's table and is deliberately **not** implemented: the host is chosen
per device from the `environment` column, not per process, so one server pushes to a TestFlight build and a