Cross-backend parity suite for token scoping's non-RPC surface rule —
*a narrowed api token is RPC-only*.
Why this suite exists
Every existing cross-impl token-scope assertion is the rpc:<method> arm
(the token_scope_cases batch in the conformance security slate). The
surface:<name> arm had none — and it is the load-bearing half of the
design. Both spines implement it, at the same named surfaces, returning a
denial body that is an explicit cross-impl contract
(token_scope_denied_body ↔ fuz_auth::token_scope_surface_denied_response),
and nothing compared the two.
That gap has the shape this control family keeps finding: the per-method arm
was the one everybody looked at, so it was the one that got a gate. The
surface arm is where a divergence could sit indefinitely — the schema gate
can't see it (no column), the action-manifest gate can't see it (no method),
and the spec-derived suites can't see it (these are REST routes, not the
declared RPC surface).
What it pins
- The denial body, byte for byte — status,
error, and the exact
required_scope capability string, per surface. A spine that renamed a
surface, dropped the surface: prefix, or emitted the JSON-RPC envelope
from a REST route fails here. - The audit stream's channel denial — that surface is gated to
credential_types: ['session'] on both spines, so a bearer never reaches
its scope gate and the observable is credential_type_required instead.
The case is kept here rather than moved: it is still the bearer-vs-surface
probe, and what it now pins is the coarser-gate-first ordering both spines
run (a spine that checked scope first would answer token_scope_required).
surface:audit_stream's own capability string is consequently unreachable
on the default mount and has no cross-impl pin — each spine's census holds
its declaration, the way the WS upgrade's is held. - Scope outranks role — a narrowed token whose account *also* lacks the
gating role must hear
token_scope_required, not
insufficient_permissions. This is the observable the TS↔Rust ordering
divergence produced before both spines converged on running the surface
gate ahead of the role gate; it was explicitly *invisible* to the
cross-backend suites, because every scoped-token probe used a token whose
account happened to hold the role.
Two non-vacuity controls hold the negatives honest: the narrowed token must
still reach the RPC method it *does* name (otherwise a spine that rejected
the credential outright would pass every denial), and a full bearer must
reach fact_bare (otherwise a route broken for all bearers would pass too).
The second has no audit-stream twin by construction — refusing every bearer
is exactly what that route's channel gate is for.
Why the WS upgrade isn't here
Rule 3's third probeable surface is the WebSocket upgrade, and it is
deliberately omitted: create_ws_transport threads cookies, not bearer
headers, and a refused upgrade surfaces as a thrown connection error whose
body — the thing this suite exists to compare — isn't readable. The HTTP
surfaces are where a denial body is observable, which is what the parity
gate needs. The WS gate keeps its per-spine coverage (fuz_app's surface
census pins the call site; the Rust census pins its own).
$lib-free by contract (relative specifiers only), like the sibling
cross-backend suites.