`revokeGrant` discovers the addressed grant's equivalence tuple before taking
that tuple's advisory lock, which is safe because those columns are immutable.
The all-equivalent update that follows then matches on the tuple rather than on
the addressed id — and nothing re-checked the addressed grant once the lock was
finally held.
That allowed a revocation to succeed by revoking a grant it never addressed. A
revoker reads its target and stalls before the lock; a second revoker takes the
lock, revokes that same target, audits it, commits; a creator takes the lock,
sees no effective equivalent, creates a replacement, commits; the first revoker
finally acquires the lock, finds its stale tuple still valid, and revokes the
replacement — returning success for an already-revoked target that should have
been not-found. No serial order produces those three results together.
Reproduced deterministically rather than by racing: a test-only connection holds
the exact tuple key, computed with the identical SQL expression the store uses,
while the stale revocation is proven — via `pg_locks`/`pg_stat_activity`, not
assumed — to be blocked on that lock; ordinary administrative statements then
commit the revoked target, its audit row, and the replacement, because the
advisory lock is cooperative. Before the fix the stale revocation fulfilled, the
replacement was revoked, and a `grant.revoke` audit row was written for it.
The correction re-reads the addressed grant after the lock is acquired,
re-asserting every condition the pre-lock read relied on: the exact id, still
unrevoked, still matching the caller's explicit/relationship source guard, and
still carrying the tuple the lock was taken for. A miss is
AUTHORIZATION_GRANT_NOT_FOUND. It is a plain read, not `for update`, so the
tuple advisory lock remains the single create/revoke serialization order and no
second lock ordering or deadlock path is introduced. The broad update stays
untouched, because narrowing it to the addressed id would have stopped clearing
genuine legacy duplicates — the behavior this pass exists to provide.
The comment claiming the target's unrevoked state was re-decided by the update
predicate described a sequence the code did not implement, and is corrected.
Co-Authored-By: Claude Opus 5 <[email protected]>
Explicit-grant management could produce states an operator could not reason
about, and pushed request-shaped problems into the database.
Two identical createGrant calls produced two grants; five concurrent identical
calls produced five. Revoking one returned success while the equivalent kept
authorizing the same user — an operator could believe they had removed access
that was still live. Two issuances are now the same authorization source when
user_id, permission_id, scope, source_kind, and source_ref are all equal, with
NULL equal only to NULL; expiry, creation metadata, actor, and grant id do not
distinguish them. Creating one while an equivalent is currently effective fails
with AUTHORIZATION_GRANT_ALREADY_EXISTS (HTTP 409, no-store) naming nothing about
the grant that already exists, and writing no grant and no audit row. A revoked
or expired equivalent does not block a fresh issuance. Conflict rather than
idempotent return: the caller asked to record a new authorization with their own
actor and expiry, and answering "done" while recording neither would hide that
nothing happened.
Enforcement is database-backed, since nothing constrains the number of API
processes: every create and revoke touching one equivalence tuple takes a
transaction-scoped advisory lock keyed by a hash of that tuple, encoded as a JSON
array so NULL stays distinct from the empty string and a crafted scope cannot
forge a separator. A collision can only make unrelated tuples serialize, never
hide a conflict, because the decision is always an exact SQL predicate. Its
single-key lock space is verified disjoint from the two-key space the migration
runner and bootstrap use. A time-dependent partial unique index was rejected:
now() is not immutable and cannot be indexed.
One revocation now clears every unrevoked equivalent — including expired-but-
unrevoked legacy rows, so the history is left in one state — with one audit row
per grant actually revoked, each carrying its own id, in one transaction.
Differently scoped or sourced grants are untouched. Historical duplicates are not
silently revoked by migration; the store prevents new effective equivalents and
an operator's explicit revocation resolves the class.
The request boundary no longer leaks into PostgreSQL. GrantId accepts exactly the
canonical lowercase hyphenated UUID the database emits, with no trimming and no
case folding, so a malformed path parameter is a 400 decided after authorization
but before any store call instead of reaching the UUID comparison and returning
an indistinguishable 503; a canonical unknown id still returns 404. expiresAt
accepts exactly YYYY-MM-DDTHH:mm:ss.sssZ, byte-for-byte equal to the parsed
date's own toISOString, and must be strictly future — bare new Date silently
rewrote 2026-02-31 to March 3rd and accepted locale, date-only, offset, and
millisecond-free forms, and a past expiry was stored as a grant dead on arrival.
The store validates the same thing before any SQL so trusted internal callers get
the guarantee too.
The generic HTTP surface now rejects a scope field in every form, including null.
No route middleware supplies a scope to the evaluator, so a scope accepted here
produced a grant that persisted, listed, and authorized nothing. Core's scope
semantics are unchanged for internal callers; no resolver was added.
Listing was unbounded and returned a user's entire history. It is now keyset
pagination over created_at DESC, id DESC with a strict boundary on both values,
so rows sharing a timestamp are neither skipped nor repeated, at most limit + 1
rows read, explicit-source filtering server-side on every page, and an opaque
unsigned position cursor that is strictly decoded and reveals nothing on
rejection. Core's list method requires a validated finite bound with no
unbounded fallback anywhere.
Co-Authored-By: Claude Opus 5 <[email protected]>
Wires Core's identity and authorization domains into apps/api over one
shared, validated PostgreSQL pool; adds an explicit, single-use,
concurrency-safe platform-owner bootstrap CLI; adds a smallest-possible
immutable permission registry with one Core-owned restricted permission
(core.authorization.grants.manage); and adds reusable Hono authorization
enforcement plus a narrowly scoped /api/v1/grants HTTP API for managing
explicit grants only, gated by an exact trusted Origin on every mutation.