Query filters
Global query filters run for snapshot GET and QUERY requests and at admission for observable subscriptions
(direct SSE, WebSocket, and multiplexed hubs), including the opt-in authenticated QueryHealth.ObserveHealth operation. They do not run for every emission. Use an
emission guard if access needs to be checked throughout a live subscription.
AuthorizationQueryFilter runs first, before validator or performer dependencies are constructed.
QueryPipelineFilter runs next, before validation and the performer. Both expose onPerform(context: QueryContext),
which may return a QueryResult fragment or nothing. The context carries the bound query arguments, request identity,
query options, and an operationName equal to the namespace-qualified declaration identity shown in introspection. Framework-created contexts make this name non-writable and non-configurable; manually built contexts may omit it. A filter gating on the name must deny absent identity or explicitly handle a manual context, never infer identity from query payload. When schema parsing fails, authorization filters see raw input and ordinary filters do not run.
Transport argument coercion errors are rejected before the filter pipeline.
A denial answers 403 with no data or validation results. Query denials have no reason field.
import { ArcApplication, authorizationQueryFilter, queryPipelineFilter, queryFilterResult, unauthorizedQueryResult, validation, type AuthorizationQueryFilter, type QueryPipelineFilter, type QueryContext, type QueryResult } from '@cratis/arc.core';
@authorizationQueryFilter()class TenantGate implements AuthorizationQueryFilter { onPerform(context: QueryContext): QueryResult | void { if (!context.tenantId) return unauthorizedQueryResult(context); }}
@queryPipelineFilter()class QueryGate implements QueryPipelineFilter { onPerform(context: QueryContext): QueryResult | void { if (typeof context.query === 'object' && context.query !== null && 'value' in context.query && context.query.value === '') { return queryFilterResult(context, { validationResults: [validation('Value is required', ['value'])] }); } }}
export const builder = ArcApplication.createBuilder();builder.add(TenantGate, QueryGate);You can also register tokens using ArcOptions.authorizationQueryFilters and .queryPipelineFilters, or
builder.addAuthorizationQueryFilter(token) and .addQueryPipelineFilter(token). Register explicit tokens as scoped
or transient services. Within each group the order is ArcOptions tokens, then explicit builder calls, then decorated
classes in add() order. A token registered more than once runs once; a token in both groups fails at build.
An unsuccessful fragment stops that group and skips subsequent stages. As with global command filters, Arc applies
context.allowedSeverity to each normalized fragment before deciding whether to stop: a filtered-out warning cannot
skip a later authorization filter. Denial always stops the chain and exposes no validation details. This TypeScript
severity-before-short-circuit behavior intentionally differs from .NET global filters. Invalid fragments and thrown filters fail closed
with 500. Omitted fragment fields retain their defaults; supplied authorization and readiness must be boolean, and validation severity must be a Severity value. The existing per-definition QueryFilter<T> callbacks remain separate and collect validation results
at the validator stage. A pre-aborted direct query resolves or invokes no declared authorization policy. Arc checks cancellation before each named policy resolution and invocation and after each awaited resolution, as well as before each filter, validator dependency or callback, performer dependency, perform(), and renderer/interceptor resolution and invocation. If cancellation occurs while one of these stages is pending, Arc waits for it to settle, fails the request without starting the next policy or stage or the snapshot/observable query producer, and releases its operation scope. Already-running callbacks are not interrupted.
The admission-filter specs exercise in-process observable hub admission and direct SSE for health. The conformance suite pairs GET, QUERY, observable snapshots, and direct SSE filter denial and allow against .NET on Express, Fastify, and Hono. It also checks direct WebSocket denial and allow, and that the host keeps serving direct SSE/WebSocket subscriptions after an early disconnect, on each TypeScript adapter; the .NET HTTP fixture has no direct WebSocket upgrade. TypeScript fixture traces check authorization before ordinary filters before validation and verify that denial does not construct validator/performer dependencies or start the observable producer. See Query pipeline for the complete order and Authorizing commands and queries for declared authorization.