testing/surface_invariants.ts

Surface invariant assertions for AppSurface data.

Two categories: - Structural — validate internal consistency (error schema presence, descriptions, duplicates, middleware propagation, error schema validity, error code consistency). No options needed. - Policy — enforce security policy (sensitive routes rate-limited, no public mutations, mutation method conventions). Configurable with sensible defaults.

Structural invariants catch schema/surface generation bugs. Policy invariants catch security misconfigurations. Both propagate automatically to consumers via assert_surface_invariants.

Declarations
#

24 declarations

view source

assert_404_schemas_use_specific_errors
#

testing/surface_invariants.ts view source

(surface: AppSurface): void

Routes declaring 404 error schemas should use specific z.literal() or z.enum() error codes, not generic z.string().

A generic 404 schema (ApiError with z.string()) means the error code is unconstrained — the handler could return any string, making client error handling fragile. Routes with params (:id) are the primary 404 producers; their error schemas should use specific constants like ERROR_ACCOUNT_NOT_FOUND.

Only flags routes that have params_schema (param-driven resource lookup) — routes declaring 404 for other reasons (e.g., bootstrap not configured) may legitimately use generic schemas.

surface

returns

void

assert_descriptions_present
#

assert_error_code_status_consistency
#

testing/surface_invariants.ts view source

(surface: AppSurface): void

The same z.literal() error code should not appear at different HTTP status codes across routes.

Extracts const values from error schema error properties (which correspond to z.literal() in the Zod source). Flags when the same literal appears at different status codes — e.g., ERROR_INVALID_CREDENTIALS at both 401 and 403 would be a bug.

Only checks schemas with const values (literal schemas). Generic z.string() schemas (which produce {type: 'string'} in JSON Schema) are ignored.

surface

returns

void

assert_error_schema_tightness
#

testing/surface_invariants.ts view source

(surface: AppSurface, options?: ErrorSchemaTightnessOptions | undefined): void

Assert that all error schemas meet a minimum specificity threshold.

Calls audit_error_schema_tightness and fails on any entry below the configured threshold. Use allowlist and ignore_statuses to exclude known exceptions during progressive tightening.

surface

the app surface to check

options?

threshold and exclusion configuration

type ErrorSchemaTightnessOptions | undefined
optional

returns

void

assert_error_schemas_structurally_valid
#

testing/surface_invariants.ts view source

(surface: AppSurface): void

Every route's declared error schemas must have an error field at the top level (conforming to the ApiError base shape {error: string}).

Catches typos in error schema definitions and ensures consumers can always read .error from error responses.

surface

returns

void

assert_input_routes_declare_400
#

assert_keeper_routes_under_prefix
#

testing/surface_invariants.ts view source

(surface: AppSurface, prefixes?: string[]): void

Keeper-protected routes must be under expected path prefixes.

Catches keeper routes accidentally placed outside the API namespace (e.g., a keeper route at /health or /admin/ instead of /api/...).

surface

prefixes

type string[]
default DEFAULT_KEEPER_ROUTE_PREFIXES

returns

void

assert_middleware_errors_propagated
#

assert_mutation_routes_use_post
#

testing/surface_invariants.ts view source

(surface: AppSurface): void

Routes with non-null input schemas should use POST (or other mutation methods), not GET.

GET routes with request bodies are technically allowed by HTTP but semantically suspicious — they bypass browser security assumptions about GET being idempotent. Query-string-driven filtering (audit log, list endpoints) should use params schemas or query string parsing, not input schemas.

surface

returns

void

assert_no_duplicate_routes
#

assert_no_unexpected_public_mutations
#

testing/surface_invariants.ts view source

(surface: AppSurface, allowlist?: string[]): void

Public mutation routes (auth: none + is_mutation) must be in the allowlist.

Catches accidentally unprotected POST/PUT/DELETE routes. Routes like login and bootstrap are public mutations by design — they go in the allowlist.

surface

allowlist

type string[]
default []

returns

void

assert_params_routes_declare_400
#

assert_protected_routes_declare_401
#

assert_query_routes_declare_400
#

assert_role_routes_declare_403
#

assert_sensitive_routes_rate_limited
#

testing/surface_invariants.ts view source

(surface: AppSurface, sensitive_patterns?: (string | RegExp)[]): void

Sensitive routes must declare rate limiting (rate_limit_key is non-null) or have 429 in their error schemas.

Matches routes against sensitive patterns and flags any that lack rate limit declarations. Catches forgotten rate limiting on credential-handling routes.

surface

sensitive_patterns

type (string | RegExp)[]
default DEFAULT_SENSITIVE_PATTERNS

returns

void

assert_surface_invariants
#

assert_surface_security_policy
#

testing/surface_invariants.ts view source

(surface: AppSurface, options?: SurfaceSecurityPolicyOptions): void

Run security policy invariants. Configurable with sensible defaults.

Checks: - Sensitive routes are rate-limited - No unexpected public mutation routes - Input schemas use mutation methods (not GET) - Keeper routes under expected prefixes

surface

options

default {}

returns

void

audit_error_schema_tightness
#

testing/surface_invariants.ts view source

(surface: AppSurface): ErrorSchemaAuditEntry[]

Audit error schema tightness across all routes in a surface.

Reports which route x status code combinations use generic ApiError (z.string()) vs specific z.literal() or z.enum() error codes. Use the output to prioritize progressive tightening of error schemas.

surface

the app surface to audit

returns

ErrorSchemaAuditEntry[]

audit entries for every route x status combination

DEFAULT_ERROR_SCHEMA_TIGHTNESS
#

testing/surface_invariants.ts view source

ErrorSchemaTightnessOptions

Recommended baseline error schema tightness for consumer projects.

Uses min_specificity: 'enum' (the assertion default) with ignore_statuses for middleware-derived status codes that are commonly generic (auth middleware produces multiple error codes at 401/403, and 429 comes from rate limiters). Consumers can extend with project-specific allowlist entries.

ErrorSchemaAuditEntry
#

ErrorSchemaSpecificity
#

ErrorSchemaTightnessOptions
#

SurfaceSecurityPolicyOptions
#

testing/surface_invariants.ts view source

SurfaceSecurityPolicyOptions

Configuration for security policy invariants.

All fields have sensible defaults. Pass overrides for project-specific needs.

sensitive_route_patterns

Path patterns for routes that should be rate-limited. Default: common sensitive patterns (login, password, bootstrap, tokens/create).

type Array<string | RegExp>

public_mutation_allowlist

Routes explicitly allowed to be public mutations (e.g., webhooks, bootstrap). Format: 'METHOD /path' (e.g., 'POST /api/account/login').

type Array<string>

keeper_route_prefixes

Allowed path prefixes for keeper-protected routes. Default: ['/api/']. Catches keeper routes outside expected namespaces.

type Array<string>

Depends on
#

Imported by
#