testing/cross_backend/conformance_rpc_auth.ts

Per-method RPC auth rows for the conformance table, generated from the declared surface and, when given, the methods a backend live-mounts off it.

describe_rpc_attack_surface_tests walks every declared method's auth, but only in-process against stub apps with no database, so it never reaches a spawned backend. The hand-written conformance rows reach every backend but cover a chosen few methods. A spine that hand-implements a method's gate can drift on any method nobody wrote a row for, and the action-manifest parity gate only compares what the spines *declare*. These rows close that: each method's declared RouteAuth becomes ConformanceCase data that describe_conformance_table_tests fires at the live gate, in-process and cross-process alike.

Rows per method, from its declared auth:

  • account or actor required — anonymous → 401 authentication_required. The 401 body is method-independent, so every such row joins one equivalence group: a caller with no credential learns nothing that tells one method from another.
  • credential-gated (credential_types) — each builtin channel the gate does not admit (keeper for session, token for api_token, daemon for daemon_token) → 403 credential_type_required, echoing the declared required_credential_types. Each of those principals is the keeper.
  • role-gated (roles) and admitting a session — fresh_non_admin → 403 insufficient_permissions, echoing required_roles. A role gate behind a credential gate that refuses sessions is skipped: every role-less principal would hit the credential gate first.
  • public — no rows.

Acting. On both spines the authorization phase resolves the acting actor before the credential gate runs, so a keeper with a second actor (extra_actors) would answer actor_required (400) to a request with no acting on an actor-bound method. Every credential and role row on such a method therefore binds acting to its principal's own actor at run time (request.acting: 'principal'): a single-actor account resolves the same actor with or without it, a multi-actor keeper resolves the one holding its roles, and either way the gate under test is the first that can refuse. The anonymous rows send none — the 401 precedes actor resolution.

Every refusal row sends schema-valid params where they can be generated (generate_valid_body, its generated acting dropped for the binding), so the gate is the only thing that can refuse. A backend whose gate is missing reaches its handler and answers something else. The rows therefore pin each gate, not the gate order against validation — the 401-before-400 rows of the security slate pin that.

Off the declared surface. live_rpc_endpoints adds the methods a backend mounts beyond the declared surface — cell verbs, opt-in resolvers, the _testing_* backdoors — read from their action specs the same way, a method the declared surface already has taken from there. The rows then need the same endpoints to resolve: pass them to the table runner's live_rpc_endpoints too.

Skipped: the protocol actions (PROTOCOL_ACTION_METHODS): their auth is fixed by fuz_app's shared specs rather than declared per consumer, they are WebSocket-scoped (cancel is a notification with no reply to assert, peer/ping is public, heartbeat rides an admitted socket), and the WS suites cover them.

view source

Declarations
#

3 declarations

generate_rpc_auth_conformance_cases
#

testing/cross_backend/conformance_rpc_auth.ts view source

(options: RpcAuthConformanceOptions): { name: string; request: { method: string; as: "token" | "keeper" | "daemon" | "invalid_daemon" | "daemon_browser" | ... 7 more ... | "expired_session"; params?: unknown; params_json?: string | undefined; acting?: "principal" | ... 2 more ... | undefined; verb?: "GET" | ... 1 more ... | undefined; }; expect: { ...; }; note?: string | undefined; xfail?: { ...; } | undefined; }[] import {generate_rpc_auth_conformance_cases} from '@fuzdev/fuz_app/testing/cross_backend/conformance_rpc_auth.js';

Generate the per-method auth rows for every declared RPC method, then every live-mounted one the declared surface lacks.

options

the declared surface, the live mount, and any methods to skip

returns

ConformanceCase[]

the conformance rows, grouped by method in declaration order, then live-mount order

RPC_AUTH_ANONYMOUS_EQUIVALENCE_GROUP
#

testing/cross_backend/conformance_rpc_auth.ts view source

"generated_rpc_auth_anonymous_401" import {RPC_AUTH_ANONYMOUS_EQUIVALENCE_GROUP} from '@fuzdev/fuz_app/testing/cross_backend/conformance_rpc_auth.js';

The equivalence group every generated anonymous 401 row joins — the unauthenticated refusal must be byte-identical whatever the method.

RpcAuthConformanceOptions
#

testing/cross_backend/conformance_rpc_auth.ts view source

RpcAuthConformanceOptions import type {RpcAuthConformanceOptions} from '@fuzdev/fuz_app/testing/cross_backend/conformance_rpc_auth.js';

surface_source

The declared surface; its rpc_endpoints supply each method's auth and input schema.

type AppSurfaceSpec

readonly

live_rpc_endpoints?

The endpoints the backend live-mounts, for rows on the methods the declared surface lacks (resolved — resolve_rpc_endpoints_for_setup turns the factory form into this). Omitted, only declared methods get rows.

type readonly RpcEndpointSpec[]

readonly

skip_methods?

Methods to leave out, beyond the protocol actions always skipped.

type readonly string[]

readonly

Depends on
#