Rotation

The failure the whole design is shaped around

A provider roll succeeds and the store write fails. The new value is sealed under the environment’s master key — rotating that is a separate and rarer act.

The new value exists only in this process. The old one is dead at the issuer. The Worker is holding a credential that no longer works.

Nothing downstream can repair that by trying harder — because trying harder at the roll produces a third value and loses the second.

So the ordering is the design

Rotating a secret walks a real entry through it, pithy secrets rotate is the command, and the reference has the fields that decide what is rotatable.

1. Refuse everything refusable before anything is called. A keyspace, an undeclared rotation, a provider secret with no rotator, the master key itself — each is answered with nothing rolled and nothing written. A refusal after a roll would be the worst of both.

2. Produce the value once. A local secret mints it; a provider secret calls the rotator. Exactly one call, never repeated for any reason.

3. Store with retries, against that value. Every attempt writes the same string. A store that cannot be made to succeed ends the run — it never reaches back for a fresh value.

What it reports, and never in aggregate

One outcome, naming the environments the new value reached and the environments it did not.

StateWhat it meansYour next move
failedThe old value is still liveRun the command again
unrecordedRolled at the issuer, not recordedGo to a console now

Those are separate members because the operator’s next move differs completely. An all rotated line printed over that distinction is exactly the shape this refuses to produce.

What happens to the value when the store will not take it

It is discarded, and the run says so.

Not printed. Not written to a file. Not put in an audit event. Not handed back to the caller — the outcome type has no field that could carry one.

The attempt is recorded at the act, not at the call site

A rotation that succeeds and records nothing leaves the secret reporting overdue forever.

That reached real behavior: the write path seeded a rotation row only on a first write, and a rotation dispatches an update — so a pithy secrets rotate that fully succeeded left lastRotatedAt untouched, while a rotation through the control-plane route recorded correctly.

Two paths to one act, disagreeing about whether the act happened — and an operator rotating on the command line during an incident being told by the product that they had not.

The fix was not a second recorder beside the first. The ledger is a required argument to the one function that performs a rotation. A caller that would rather not record cannot express that.

The row is opened before the roll

An in_progress row naming the secret and who asked is written first, and closed after.

A rotator that never returns still leaves a trace. That is the difference between an incident review that can see the attempt and one that reads a gap.

It is opened only after every refusal, so a rotation that never started writes no history at all.

A first write is not a rotation

A brand-new secret gets a baseline row, on the create branch. A rotation gets a rotation row.

Conflating them to make a count come out round would make the ledger unable to answer the question it exists for — when was this value last replaced.

Two implementations of the ledger, because there are two kinds of caller

The ledger is a table in the per-environment secrets D1.

A Worker holds that database directly. The CLI holds none — the master key is Worker-only, so every value-touching command is a dispatch to that environment’s manager Workflow — so it records the same way it writes.

Same rows, same columns, same sentence on a failure, because both close through one shared closure.

Master-key rotation is a different thing

Rotating a value replaces what a secret holds. Rotating the master key re-encrypts what is already there.

They share the versioned-envelope shape and nothing else. See the master key.

ESC