A grant is a bearer credential: a list of scopes plus an HMAC over
account_id ":" dbname ":" scope[,scope...], signed with a key derived from the server
secret under signing.GrantLabel (internal/signing/signing.go:228, 331).
There is nothing else in the message. No expiry, no serial number, no issued-at. Verification
(Server.granted, internal/server/server.go:833-841) checks the signature and
the account/database binding and nothing more. So a grant is good forever, and the only way
to withdraw one is to change s.options.Secret — which is the same secret that signs
connection strings, so rotating it invalidates every connection string on the server at the same
time.
signing.Held carries AccountID, DBName and
Scopes. There is no time field anywhere in the signed message, and
Grants takes no clock.server.go:833-841: granted is signature-check plus binding-check. There
is no revocation list, no issuance record, and no state on the server that a grant is checked
against — deliberately, since the whole design is offline issuance.Does it lose data, break a promise on a public surface, or stop somebody installing and running? Yes — the middle one, and by the second-order reading rather than the first.
Say it straight, because this is the deferral-or-not that most deserves an argument. A grant that never expires does not, by itself, break a promise: the CHANGELOG says up front that it does not expire, so nobody is misled. By the narrow reading of the triage question this is Defer.
It is Fix in 1.0.0 because the surface that has to change is the signed message, and
the signed message is the public surface. Adding exp means adding a field to the bytes
that go into the HMAC, in the Go signer, in the Go verifier, and in the TypeScript signer, all
together. Before the tag that is an edit. After the tag it is a break: every grant minted by 1.0.0
fails verification on 1.1.0, or 1.1.0 has to accept both message shapes forever, which is a permanent
branch in a security check — the worst place in a codebase to keep two answers.
There is a second reason, weaker but real: the only revocation available today is rotating a secret that also invalidates every connection string. That is not a revocation mechanism, it is an outage. An operator who needs to withdraw one scope from one account has no proportionate action, which makes scopes hard to recommend for the case they exist for.
exp means writing them twice. Decide the message first, then write every signer
once.