# Client Shield & Rotating Secrets


Every internal call — HTTP or NATS — is stamped with a shared secret. That
secret **rotates every hour**; a leaked one is useless within the rotation
window. The SDK owns both sides: the consumer slot/cache and the engine
the BE instantiates.

## The shield secret — `auth/client-shield-secret.ts`

- `CLIENT_SHIELD_SECRET_REF` — the canonical `{scope:"global", name:"service_client_shield_secret"}` ref.
- `ClientShieldSecretSlot = { current, previous?, rotated_at }` — the live
  copy each service keeps (`setClientShieldSecretSlot`/`getClientShieldSecretSlot`).
- `currentClientShieldSecret(fallback?)` — what to **stamp on outbound**
  calls (always `current`, else the static fallback).
- `validClientShieldSecrets(graceMs, fallback?)` — what to **accept on
  inbound**: `current` + `previous` while still inside `graceMs`.
- `SECRET_DEFAULTS = { rotationMs: 60 min, graceMs: 5 min }`.

## The engine — `secrets/rotating-secret.ts`

`RotatingSecretEngine` (instantiated once, in the BE): writes the Redis
slot `{current, previous, rotated_at}` with TTL `rotationMs + graceMs`,
rotates on the timer, and broadcasts the shielded `secret.rotated` event.

`SecretProvider` / `startSecretProvider` is the consumer side: fetches the
slot once at boot over `secret.get`, subscribes to `secret.rotated`, and
keeps the local slot warm — all stamped outbound headers automatically use
`current`.

`SECRET_SUBJECTS` = the governed subjects (`secret.get` request/reply via
correlation, `secret.rotated` broadcast).

## Serving is allowlisted — `nats/service-shield.ts`

`secret.get` is not a public request: the BE handler replies only to an
explicit `SERVED_SECRETS` list and each call passes `verifyNatsPublisher`
— the publisher must be a known, enabled, online service from the client
registry (`ShieldRegistryLike`, wired via `setNatsShieldRegistry`).

`subscribeShielded(handler)` wraps any pub/sub subscriber with the same
publisher verification — the standard for every lifecycle/config subject.

## Outbound plumbing — `http/internal-client.ts`

`internalFetch(url, init)` is the s2s fetch: it injects the service's
identity headers (set via `configureInternalClient(provider)`) plus the
current shield secret — consumers like `fetchDocsPack` build on it so
every cross-service HTTP call is stamped correctly by construction.
`resetInternalClient` exists for tests.

## Who does what

| Side | Code | Duty |
|---|---|---|
| **BE (owner)** | `RotatingSecretEngine` + `SERVED_SECRETS` allowlist + publisher verifier | generate, persist, rotate, serve, broadcast |
| **US (consumer)** | `SecretProvider` + slot + `validClientShieldSecrets` on inbound middleware | fetch at boot, refresh on `secret.rotated`, stamp outbound, accept current+grace inbound |
| **Static fallback** | `config_entries.service_client_shield_secret` | boot before first rotation / Redis down — never the production value |
