edgeir

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package edgeir projects a compiled *pipeline.Pipeline into a versioned, serializable EdgeIR — the single, explicit JSON contract a small generic JavaScript worker (Cadish Edge) interprets at the edge. It is the FIRST slice of Cadish Edge (the management-plane foundation; no runtime, no Cloudflare API).

The projector is the ONE decision point for "edge-capable vs delegate" (design policy C, §2.6): anything the edge v1 cannot faithfully execute (body transforms via `replace`; regex BAN purge) is recorded in EdgeIR.Delegate with a reason — never silently dropped — so the worker `pass`es it to the Cadish server behind, and `cadish edge build` can surface a coverage report (and fail under --strict).

The IR is an EXPLICIT projection (not raw pipeline structs with json tags): the field names here are a STABLE contract the JS interpreter mirrors. Keep IRVersion in lockstep with the runtime that understands it.

Index

Constants

View Source
const IRVersion = 9

IRVersion is the EdgeIR schema version. The embedded JS runtime declares the version it understands; `cadish edge build` refuses a mismatch. Bump it on any breaking change to the field shape below.

v2 (D70, edge completion roadmap v1.1) batches three additive shape changes:

  • Key.Recipes: the FULL ordered scoped cache_key recipe list + selectors, so a scoped site (D65) keys byte-identically at the edge (was delegated in v1).
  • Device: the {device} User-Agent classifier ruleset, so the worker classifies from the User-Agent natively (was an X-Cadish-Device crutch / constant "").
  • TTL.MaxStale: the max_stale (D60) window, so the edge bounds its stale-on-error serving instead of serving unboundedly-old content.

v3 (D74) closes the edge open-redirect: Site.RedirectHosts (the normalized trusted-host allowlist) + Site.CanonicalHost let the worker resolve a redirect TARGET's {host} the SAME safe way the server does (reflect the inbound Host only when trusted, else the canonical host) instead of reflecting it verbatim. The version is bumped (not just batched into v2) deliberately: a stale runtime that ignored these fields would re-open the open redirect, so it must refuse the IR.

v4 (D75/D76, edge completion roadmap v1.2) makes two delegated directives edge-native:

  • Response.Transforms + Response.TransformMaxBytes: a SIZE-BOUNDED `replace` body transform (D75). The worker applies the literal substitution within the cap and passes an over-cap body through untransformed (unbounded `replace` stays server-only).
  • Response.OnError: the `respond on_error` outage synthetic (D76). The worker serves it on an origin hard-failure with no servable cached object, instead of a bare 502. A stale runtime ignoring these would silently NOT transform / NOT serve the friendly outage page, so the version is bumped and the runtime refuses a mismatch.

v5 (Finding 6) adds Response.ttl[].StripHeaders: the from_header-family control headers (X-Cache-Ttl/X-Cache-Grace/X-Cache-Max-Stale) a cache_ttl rule CONSUMES, so the worker strips them before store + deliver exactly as the server does. A stale runtime ignoring them would LEAK the internal origin↔cache contract headers to the client (the server never does), so the version is bumped and the runtime refuses a mismatch.

v6 (D101) adds CacheCredentialed: the `cache_credentialed @scope` set that makes caching ORIGIN-AUTHORITATIVE for the matching credentialed requests. The worker skips the credential bypass for them (caches under the SHARED key, forwards the original cookies to origin) and applies the in-scope EvalResponse precedence: a positive in-scope cache_ttl signal is the SOLE storage gate — it force-stores and FORCE-OVERRIDES + STRIPS the per-user Set-Cookie + weak no-store/private/no-cache/Pragma/Expires (matching the VCL); no signal ⇒ not stored (fail-closed). A stale runtime ignoring the field would credential-BYPASS those requests (caching LESS, never wrong) — but to keep Go==JS exact it is a versioned contract, so the runtime refuses a mismatch.

v7 (edge `all`+`query` matchers + `query_present` non-empty modifier) makes the last two pure-request-data Gateway matchers edge-native (they were serverOnly => forced-pass before) and adds the `+` non-empty-value modifier to `query_present`:

  • `query` reuses the already-projected Name/Values (one named param vs an OR set of exact values; empty set => presence) — only the serverOnly suppression is lifted and a JS `query` matchOne case added.
  • `all` adds Matcher.Subs: the AND-conjunction of (Ref, Negate) sub-terms. Each term references a named matcher XOR'd with Negate; request-phase, depth-1 (the compiler forbids all-of-all and response-phase subs). The worker recurses through matchOne over ir.matchers[ref], mirroring the Go `got == negate` short-circuit.
  • `query_present` adds Matcher.QueryNamesNonEmpty: the subset of QueryNames whose `+` modifier requires the matched param to have ≥1 non-empty value (`?t=` empty does NOT match `t+`, `?t=abc` does — Varnish `=[^&]+` parity for the "publi" marketing-param flag). Additive: a stale runtime ignoring it falls back to pure presence (strictly more permissive — may pass one fewer request than intended, never caches one that should be passed), so the version is NOT bumped solely for this field.
  • Redirect.NoStore is an additive v7 field (json:"noStore,omitempty"): when the `redirect … no_store` modifier is present the worker attaches Cache-Control: no-store, no-cache, must-revalidate, private to the short-circuit Response so no shared cache or browser caches a personalized (cookie/Accept-Language-driven) redirect. A stale runtime that misses it simply omits the header (the pre-modifier behavior — strictly less safe but not wrong), so the version is NOT bumped for this field.
  • Deliver.CacheAgeHeader is an additive v7 field (json:"cacheAgeHeader,omitempty"): the target header name for `header +cache_age NAME`; the worker emits the object's age in whole seconds on a cache HIT (Math.floor((now − storedAt) / 1000)), omitted on MISS/bypass. A stale runtime ignoring it simply omits the header (observability gap, not a correctness regression), so the version is NOT bumped for this field.

A stale runtime would mis-evaluate `all`/`query` (silent wrong cache/route decision), so the version was bumped to 7 and the runtime refuses a mismatch.

v8 (D105, edge pass-route exclusion) adds RouteExclusions: the host+prefix CF route globs the worker would ONLY ever `pass` (a path-only unconditional pass with NO other edge directive touching the path), populated ONLY when the site opts in with `edge { bypass_passes }`. The deploy plane turns each into a Cloudflare route that matches but runs no worker (script omitted) so CF proxies it straight to origin, skipping a wasted worker invocation. This is purely a DEPLOY-PLANE concern — the worker RUNTIME never reads RouteExclusions (a request that reaches the worker is still passed correctly), so a stale runtime ignoring the field is unaffected. The version is bumped (not silently batched) so the management plane treats the field as a versioned contract; the runtime mismatch check is unchanged in behaviour for it.

v9 (F-A1/F-B) adds UsesGeo and SECURITY-relevant request-phase neutralization: the worker must blank an UNKEYED {device}/{geo*} (raw OR class-derived {classify.NAME}) reflected in a REQUEST-phase header (forwarded to origin) so it never forwards a class the SELECTED cache-key recipe does not segment on (cross-class cache leak) — the worker derives the keyed set from the recipe it selects per request — and it injects the CF geo headers ONLY when UsesGeo. The version bump ensures a stale runtime (which would not neutralize) is refused.

Additive v9 fields (version NOT bumped — same discipline as Redirect.NoStore in v7):

  • Respond.Headers / Redirect.Headers (CAD-3): the `respond`/`redirect` `{ header … }` block — literal operator header ops (set|append|remove) the worker applies to the short-circuit Response (after the synthetic's base Content-Type, resp. after Location + the no_store Cache-Control). A stale runtime that misses them serves the short-circuit WITHOUT the operator headers — the pre-block behavior; a header-surface gap (e.g. a missing Cache-Control: no-store hint), never a wrong status/Location/body or a wrong cache decision — so the version is not bumped. Location/Content-Length can never appear in the ops (compile-rejected, runtime-skipped), preserving the open-redirect authority guard on the redirect TARGET.

Variables

This section is empty.

Functions

func Project

Project builds the EdgeIR + CoverageReport for a compiled pipeline. It never errors today (the pipeline is already valid), but returns an error to keep the contract stable for future validation (e.g. an IR size/cardinality ceiling).

func ServerOnlyEdgeKinds added in v0.2.1

func ServerOnlyEdgeKinds() map[string]bool

ServerOnlyEdgeKinds returns a COPY of the set of matcher kinds that have no edge JavaScript runtime case (the projector marks them serverOnly + delegates; the worker fails closed). Exported so the conformance coverage gate can assert its own mirror set EQUALS this one, making any future drift fail loudly (Finding 7).

Types

type CORS

type CORS struct {
	Scope           Scope    `json:"scope"`
	AllowAllOrigins bool     `json:"allowAllOrigins,omitempty"`
	Origins         []string `json:"origins,omitempty"`
	Methods         []string `json:"methods,omitempty"`
	Headers         []string `json:"headers,omitempty"`
}

CORS projects a `cors` directive.

type Classifier

type Classifier struct {
	Rows    []ClassifyRow `json:"rows"`
	Default string        `json:"default"`
	// DerivesFrom names the request cookies this axis consumes (`derives_from cookie
	// NAME…`). The worker keeps them through cookie_allow so the classifier reads the
	// original value and the key is built from it, then strips them post-key before the
	// credential bypass + origin fetch (the same derive→strip the server does, so the two
	// runtimes collapse cardinality identically). Omitted when none are declared.
	DerivesFrom []string `json:"derivesFrom,omitempty"`
	// DerivesForward is the SUBSET of DerivesFrom declared `forward` (alias `keep`): the
	// worker FORWARDS these to origin (does NOT strip) and treats them as covered by
	// {TOKEN} in the credential bypass — the loud opt-in for cookie-reading backends, the
	// same behavior the server applies. Omitted when no row uses the modifier.
	DerivesForward []string `json:"derivesForward,omitempty"`
}

Classifier is `{rows:[{conj:[matcherId], value}], default}` — first-match over the rows' AND-conjunctions, else default (identical to classifier.resolve).

type ClassifyRow

type ClassifyRow struct {
	Conj  []string `json:"conj"`
	Value string   `json:"value"`
}

ClassifyRow is one row: an AND-conjunction of matcher ids yielding a value.

type CoverageReport

type CoverageReport struct {
	// EdgeNative is the count of edge-expressible directives projected into the IR.
	EdgeNative int `json:"edgeNative"`
	// Delegated is the count of directives recorded in EdgeIR.Delegate.
	Delegated int `json:"delegated"`
	// Items is the per-directive delegate detail (mirrors EdgeIR.Delegate).
	DelegatedItems []DelegatedItem `json:"delegatedItems,omitempty"`
	// Warnings are non-fatal advisories surfaced by `cadish edge build` (e.g. the KV
	// 25 MB hard-cap notice). They are visibility, not a gate, and never fail the
	// build — UNLESS they are also counted in SecurityGate/ValueExposed below, which
	// are gating signals that fail `--strict`.
	Warnings []string `json:"warnings,omitempty"`
	// SecurityGate is non-zero when the site configured a security gate
	// (allow/deny/block/rate_limit). Those rules are SERVER-ONLY and are NOT
	// projected into / enforced by the edge worker (Cloudflare's own layer must
	// enforce them). The projector also records a `security` entry in Delegate so the
	// gate is visible; this dedicated counter lets `--strict` fail loudly even if the
	// Delegate accounting ever changes. Fix A.
	SecurityGate int `json:"securityGate,omitempty"`
	// ValueExposed counts header/cookie matcher literal values that ship verbatim in
	// the IR to the PUBLIC worker bundle (a potential baked-in secret). It mirrors the
	// value-exposure warnings; `--strict` fails when it is non-zero so a CI pipeline
	// catches a secret in the bundle. Fix B.
	ValueExposed int `json:"valueExposed,omitempty"`
	// ForcedPass counts SELECTING directives (pass / route / redirect / cache_key selector or
	// token / cache_ttl / storage / edge-tier / upgrade) that could NOT be faithfully projected
	// because they reference a matcher the edge fails closed on (a ServerOnly Gateway/lb/`ip`
	// matcher, or an untranslatable RE2 regex), so the projector forced the SAFE fallback —
	// a site-wide fail-open `pass` (the edge caches nothing) or a delegated redirect chain.
	// The runtime direction is safe, but the operator's PRECISE intent is silently coarsened, so
	// `cadish edge build` fails NON-ZERO on a non-zero ForcedPass EVEN WITHOUT -strict (R02/R16) —
	// the operator must consciously keep that directive on the Cadish server behind, not discover
	// at runtime that their whole site is being passed. Distinct from Delegated (server-only
	// directives like rewrite/encode/purge/security that have no SELECTING/cache effect).
	ForcedPass int `json:"forcedPass,omitempty"`
	// RouteExcludable is the host+prefix CF route globs the edge worker would ONLY ever
	// `pass` and that no other edge directive touches (D105) — the set the deploy plane
	// would carve out of the worker route as origin-direct (no-worker) routes. It is
	// ALWAYS computed and reported (even when `edge { bypass_passes }` is off) so the
	// operator can review the opportunity; it is only PROJECTED into the IR (and turned
	// into real CF routes) when the toggle is on. Never a gate — exclusions are an
	// optimization, not a coverage gap, so they do not affect `-strict`.
	RouteExcludable []string `json:"routeExcludable,omitempty"`
	// RouteExcludableExplicit is the operator-DECLARED `edge { bypass PATTERN… }` set
	// (D105 explicit companion), host-crossed into CF route globs. Distinct from
	// RouteExcludable (the auto-derived set): these are taken at the operator's word (no
	// excludability gate) and are ALWAYS projected into the IR (declaring `bypass …` is
	// itself the opt-in). The coverage report prints them under the route-excludable
	// section marked `bypass` (vs `~` for auto-derived). Omitted when none are declared.
	RouteExcludableExplicit []string `json:"routeExcludableExplicit,omitempty"`
	// BypassOverlapWarnings are the loud advisories for an explicit `bypass` pattern that
	// OVERLAPS a path the edge would CACHE (a scoped cache_ttl/cache_key/storage rule): the
	// bypass forgoes POP caching for that path. Operator-declared ⇒ a WARNING, never a gate;
	// the pattern is still projected. Printed in the route-excludable section. Omitted when
	// no explicit bypass shadows a cached path.
	BypassOverlapWarnings []string `json:"bypassOverlapWarnings,omitempty"`
}

CoverageReport summarizes what the projection covers edge-natively vs delegates. It is the edge equivalent of the `cadish check` report: `cadish edge build` prints it, and --strict fails when anything is delegated.

func (CoverageReport) AllRouteExcludable added in v0.2.2

func (r CoverageReport) AllRouteExcludable() []string

AllRouteExcludable returns the EXACT-deduped union of EVERY no-script route the deploy plane could have created for this site under any `bypass_passes` toggle state — the auto-derived excludable set (RouteExcludable) AND the operator-declared `edge { bypass … }` set (RouteExcludableExplicit). `cadish edge disable` tears down this union so the kill switch fully reverts (F-D4).

It deliberately does NOT cross-collapse the two sources (no coversGlob merge): `enable` projects ir.RouteExclusions = mergeRouteExclusions(auto, explicit), and with `bypass_passes` OFF the auto input is nil — so enable can create the NARROW explicit `/api/v2*` while disable's always-computed auto set contains the BROADER `/api/*`. A cross-collapse would drop `/api/v2*` under `/api/*` and leave the created route orphaned (F-D1-r2). Every pattern enable could emit is one of these exact strings (merge only ever substitutes a broader INPUT pattern), so the exact union is a superset of what was created; deploy.Disable matches by exact pattern, so any pattern here that was never created is a harmless no-op.

type Delegated

type Delegated struct {
	Directive string `json:"directive"`
	Reason    string `json:"reason"`
	Scope     *Scope `json:"scope,omitempty"`
}

Delegated records one non-edge-capable directive the worker must pass to the Cadish server behind, with a human reason and (when known) the scope it applies to.

type DelegatedItem

type DelegatedItem struct {
	Directive string `json:"directive"`
	Reason    string `json:"reason"`
}

DelegatedItem is one entry in the coverage report's delegate list.

type Deliver

type Deliver struct {
	CacheStatusHeader string `json:"cacheStatusHeader,omitempty"`
	// CacheKeyHeader is the `header +cache_key NAME` target header name (""=none).
	// The worker computes the SAME 12-hex sha256 prefix over the cache key it builds
	// per request (or the raw key when CacheKeyRaw) and sets this header — Go↔JS
	// identical (a conformance fixture asserts it).
	CacheKeyHeader string `json:"cacheKeyHeader,omitempty"`
	// CacheKeyRaw selects the raw projected key string (the `raw` modifier) instead
	// of its hash for CacheKeyHeader.
	CacheKeyRaw bool `json:"cacheKeyRaw,omitempty"`
	// CacheAgeHeader is the `header +cache_age NAME` target header name (""=none).
	// The worker emits the object's age in whole seconds on a cache HIT (fresh or
	// stale), computed as Math.floor((now − storedAt) / 1000). OMITTED on MISS/bypass.
	// Additive v7 field (omitempty): a stale runtime ignoring it simply omits the
	// header (the pre-directive behavior — observability gap, not a correctness
	// regression), so IRVersion stays 7.
	CacheAgeHeader string `json:"cacheAgeHeader,omitempty"`
	// CacheTTLRemainingHeader is the `header +cache_ttl_remaining NAME` target header
	// name (""=none). The worker emits the object's remaining fresh TTL in whole seconds
	// on a cache HIT (fresh or stale), computed as Math.floor((storedAt + ttlMs − now) /
	// 1000) floored at 0 — the Varnish X-TTL complement to CacheAgeHeader's X-Age.
	// OMITTED on MISS/bypass. Additive field (omitempty): a stale runtime ignoring it
	// simply omits the header (observability gap, not a correctness regression), so
	// IRVersion is unchanged.
	CacheTTLRemainingHeader string `json:"cacheTtlRemainingHeader,omitempty"`
}

Deliver is the DELIVER-phase projection. Body transforms (`replace`) are NOT here — they are delegated; CacheStatusHeader is the `header +cache_status` target.

type DeviceClassifier

type DeviceClassifier struct {
	Rules   []DeviceRule `json:"rules"`
	Default string       `json:"default"`
	Folds   []DeviceFold `json:"folds,omitempty"`
}

DeviceClassifier is the projected {device} User-Agent ruleset (D70): an ordered first-match-wins rule list + default class + optional folds. The worker ports the identical scan so the same User-Agent yields the same {device} bucket as the Go server's classify pre-pass.

type DeviceFold

type DeviceFold struct {
	From string `json:"from"`
	Into string `json:"into"`
}

DeviceFold remaps a classified class onto another after rule matching (FROM->INTO).

type DeviceRule

type DeviceRule struct {
	Class      string   `json:"class"`
	Substrings []string `json:"substrings"`
	Exclude    []string `json:"exclude,omitempty"`
}

DeviceRule is one UA→class rule: ANY of Substrings present (OR) AND NONE of Exclude present selects Class. Substrings/Exclude are already lower-cased; matching is case-insensitive (the worker lower-cases the UA once).

type Edge

type Edge struct {
	Policies []EdgePolicy `json:"policies,omitempty"`
	Default  string       `json:"default"` // local|distribute|skip

	// KVTTLSeconds caps KV retention (the KV `expirationTtl`) independently of the
	// object's ttl+grace. Zero => unset: KV retention defaults to ceil((ttl+grace)).
	// The runtime computes expirationTtl = clamp(ttl+grace, 60s, kv_ttl).
	KVTTLSeconds int `json:"kvTtlSeconds,omitempty"`
	// KVMaxBytes is the hard size bound for the KV tier: a response body larger than
	// this is written to L1 only, never KV (regardless of its distribute tier).
	// Defaults to 1 MiB; the runtime reads this value.
	KVMaxBytes int64 `json:"kvMaxBytes,omitempty"`
}

Edge is the edge cache policy block: per-scope tier policies + a default tier (local | distribute | skip), plus the KV (L2) guardrails. Projected from the `edge {}` Cadishfile block.

type EdgeIR

type EdgeIR struct {
	IRVersion   int                   `json:"irVersion"`
	Site        Site                  `json:"site"`
	Upstream    Upstream              `json:"upstream"`
	Matchers    map[string]Matcher    `json:"matchers"`
	Classifiers map[string]Classifier `json:"classifiers,omitempty"`
	Normalizers map[string]Normalizer `json:"normalizers,omitempty"`
	Tenant      *Tenant               `json:"tenant,omitempty"`

	// Device is the {device} User-Agent classifier ruleset (D70). Present only when the
	// cache key uses the {device} token (zero-cost-when-unused). The worker classifies
	// the User-Agent natively from this ruleset — no X-Cadish-Device header crutch.
	Device *DeviceClassifier `json:"device,omitempty"`

	Recv     Recv     `json:"recv"`
	Key      Key      `json:"key"`
	Response Response `json:"response"`
	Deliver  Deliver  `json:"deliver"`

	// UsesGeo mirrors pipeline.UsesGeoToken (v9): the site varies on or reflects geo. The
	// worker injects the CF geo headers to origin (applyGeoHeaders) ONLY when true, so a
	// site that does not use geo never forwards an unkeyed geo signal an origin could
	// vary on under a geo-independent edge key (F-B compounding path). NOT omitempty:
	// the worker reads usesGeo===false to SUPPRESS injection, so false must be emitted.
	UsesGeo bool `json:"usesGeo"`

	// CacheUnsafe mirrors the site-level `cache_unsafe` opt-out flag. When true the
	// edge interpreter skips the safe-by-default downgrade (Set-Cookie / private
	// Cache-Control / uncovered Vary) that mirrors Go's EvalResponse behaviour.
	// Omitted (false) when the site does not set `cache_unsafe`.
	CacheUnsafe bool `json:"cacheUnsafe,omitempty"`

	// CacheCredentialed is the projected `cache_credentialed @scope` set (D101): the
	// request scopes for which caching is ORIGIN-AUTHORITATIVE. The worker (a) does NOT
	// credential-bypass a matching request (it caches under the SHARED key and forwards the
	// original cookies to origin for auth) and (b) applies the in-scope EvalResponse
	// precedence — a positive in-scope cache_ttl signal is the SOLE storage gate: it force-
	// stores and FORCE-OVERRIDES + STRIPS the per-user Set-Cookie + weak no-store/private/
	// no-cache/Pragma/Expires (matching the VCL); absence of the signal does NOT store
	// (fail-closed). A scope referencing a
	// ServerOnly/untranslatable matcher is NEVER projected here: it fails CLOSED (the whole
	// site fail-open passes + ForcedPass++ so `cadish edge build` fails loud). Omitted when
	// the site declares no `cache_credentialed`.
	CacheCredentialed []Scope `json:"cacheCredentialed,omitempty"`

	// CookieAllow is the `cookie_allow` request-cookie allowlist (name patterns, globs
	// ok). When CookieAllowSet is true the edge worker strips every request cookie not
	// matching a pattern before the cache decision (an EMPTY list strips all cookies),
	// mirroring the server's FilterRequestCookies, so the controlled cookies are exempt
	// from the edge credential bypass and cookie-bearing traffic can cache at the edge.
	CookieAllow    []string `json:"cookieAllow,omitempty"`
	CookieAllowSet bool     `json:"cookieAllowSet,omitempty"`

	// KeyHeaderNames is the lower-cased set of request header names included in the
	// cache key (from every `header:NAME` cache_key token). The edge interpreter uses
	// this to decide whether a `Vary` field is covered by the cache key, mirroring
	// Go's varyCovered helper. Omitted when the key contains no header tokens.
	KeyHeaderNames []string `json:"keyHeaderNames,omitempty"`

	// Edge is the `edge {}` cache-tier policy block (L1 Cache API / L2 KV): a default
	// tier plus per-scope overrides (local | distribute | skip). The worker resolves
	// each response's tier from these. Deploy identity (account/zone/worker/routes/kv)
	// is NOT here — it is management-plane metadata that must not ship to the public
	// worker (the CLI reads it from pipeline.ViewDeployConfig()).
	Edge Edge `json:"edge"`

	// Delegate lists every non-edge-capable directive, with a reason, that the
	// worker must `pass` to the Cadish server behind. Materializes policy C.
	Delegate []Delegated `json:"delegate,omitempty"`

	// RouteExclusions is the set of host+prefix CF route globs the edge worker would
	// ONLY ever `pass` (D105): a path covered by an unconditional path-only `pass` with
	// NO other edge-emitted directive touching it, reducible to a `<host>/<prefix>*`
	// glob. Populated ONLY when the site sets `edge { bypass_passes }`; the deploy plane
	// (edgedeploy.Enable) creates a no-script Cloudflare route per pattern so CF proxies
	// those paths straight to origin, skipping a wasted worker invocation. A DEPLOY-PLANE
	// concern only — the worker runtime never reads it (a request that still reaches the
	// worker is passed correctly), so a stale runtime ignoring it is unaffected. Omitted
	// when empty / when the toggle is off. The coverage report ALWAYS lists the excludable
	// set (CoverageReport.RouteExcludable) even when the toggle is off, so the operator can
	// review it before opting in.
	RouteExclusions []string `json:"routeExclusions,omitempty"`
}

EdgeIR is the versioned, serializable projection of one compiled site. The JSON field names are the contract; the JS interpreter is a faithful port of the matcher switch + the EvalRequest/EvalResponse/EvalDeliver phase walk over this structure.

type EdgePolicy

type EdgePolicy struct {
	Scope Scope  `json:"scope"`
	Tier  string `json:"tier"` // local|distribute|skip
}

EdgePolicy is one per-scope edge cache-tier policy.

type Header struct {
	Scope Scope      `json:"scope"`
	Ops   []HeaderOp `json:"ops"`
}

Header is a scoped group of header ops.

type HeaderOp

type HeaderOp struct {
	Op          string `json:"op"`
	Name        string `json:"name"`
	Value       string `json:"value,omitempty"`
	ValueIsTmpl bool   `json:"valueIsTmpl,omitempty"`
}

HeaderOp is one header edit. Op is set|append|remove|cache_status. ValueIsTmpl flags a value carrying a template placeholder the worker must expand.

type Key

type Key struct {
	Tokens  []KeyToken  `json:"tokens"`
	Recipes []KeyRecipe `json:"recipes,omitempty"`
}

Key is the cache-key recipe set. Tokens is the catch-all (default/unscoped) recipe, kept for backward compatibility and as the worker's fallback. Recipes is the FULL ordered scoped recipe list (D70): the worker evaluates it first-match-wins (mirroring pipeline.selectKeyTokens) and builds the matching recipe, so a scoped cache_key site keys byte-identically at the edge. When Recipes is empty the worker uses Tokens (a single-recipe site behaves exactly as v1).

type KeyRecipe

type KeyRecipe struct {
	Selector Scope      `json:"selector"`
	Tokens   []KeyToken `json:"tokens"`
}

KeyRecipe is one scoped cache_key recipe: a request-phase selector + its tokens. The catch-all recipe has Selector.Always=true. Evaluated first-match-wins by the worker, exactly like the Go pipeline.

type KeyToken

type KeyToken struct {
	Kind  string   `json:"kind"`
	Arg   string   `json:"arg,omitempty"`
	Ref   string   `json:"ref,omitempty"`
	Allow []string `json:"allow,omitempty"`
	Deny  []string `json:"deny,omitempty"`
}

KeyToken is one cache-key component. Kind is the stable token name the JS interpreter switches on; Arg/Ref/Allow/Deny carry its parameters (Allow for the query_allow allowlist, Deny for the query_strip denylist). For "sticky", Arg is the site-level sticky cookie name the worker must read; for "header" it is the header name; for "tenant"/"literal" it is the constant text.

type Matcher

type Matcher struct {
	Kind string `json:"kind"`

	Patterns []string `json:"patterns,omitempty"` // path/host
	Regex    string   `json:"regex,omitempty"`    // path_regex/host_regex/header_regex
	// Flags is the JS-equivalent RegExp flag string ("i"/"is"/…) lifted from a RE2
	// inline flag group (e.g. `(?i)^/cams`) on Regex so the worker compiles
	// `new RegExp(regex, flags)` instead of crashing on the inline `(?i)`. Empty when
	// the source carried no inline flags (BUG-1).
	RegexFlags string `json:"regexFlags,omitempty"`

	Name   string   `json:"name,omitempty"`   // header/cookie name
	Values []string `json:"values,omitempty"` // header/cookie accepted values
	Glob   bool     `json:"glob,omitempty"`   // cookie name-prefix glob

	Methods      []string `json:"methods,omitempty"`
	Upstreams    []string `json:"upstreams,omitempty"`
	ContentTypes []string `json:"contentTypes,omitempty"`
	CookieNames  []string `json:"cookieNames,omitempty"` // set_cookie

	ClassifyToken  string `json:"classifyToken,omitempty"`
	ClassifyValue  string `json:"classifyValue,omitempty"`
	ClassifyNegate bool   `json:"classifyNegate,omitempty"`

	GeoGranularity string   `json:"geoGranularity,omitempty"`
	GeoValues      []string `json:"geoValues,omitempty"`

	QueryNames []string `json:"queryNames,omitempty"`

	// QueryNamesNonEmpty is the subset of QueryNames that carry the `+` modifier
	// (`query_present adult_content+ ff-*+`): a matched param must have ≥1 non-empty
	// value. Absent/empty => all names are presence-only (the previous behavior).
	// Additive under v7: a v7 runtime that does not read it falls back to pure
	// presence, which is strictly more permissive than the intended behavior but never
	// caches something a correct runtime would pass — fail-safe rather than fail-closed.
	QueryNamesNonEmpty []string `json:"queryNamesNonEmpty,omitempty"`

	// Subs is the AND-conjunction of an `all` matcher: each entry references a named
	// matcher (Ref) XOR'd with Negate. Edge-evaluated request-phase; depth-1 (the compiler
	// forbids all-of-all and response-phase subs), so the worker walks ir.matchers[ref] with
	// no cycle guard. Empty slice ⇒ matches (mirrors the Go empty-conjunction). v7.
	Subs []SubMatcher `json:"subs,omitempty"`

	// JSONPath is the dotted PATH of a cookie_json/header_json matcher (D54), e.g.
	// "user.verified" / "flags.0.kind". Name carries the cookie/header name and
	// Values the OR set of accepted scalar string forms (empty => presence). The JS
	// runtime splits JSONPath the same way and applies the same 8 KiB/depth-32 caps.
	JSONPath string `json:"jsonPath,omitempty"`

	// ResponsePhase marks content_type/set_cookie matchers (need the origin
	// response): the JS interpreter only evaluates them in the response/deliver walk.
	ResponsePhase bool `json:"responsePhase,omitempty"`

	// RegexUntranslatable marks a path_regex/host_regex/header_regex whose RE2 source
	// uses a construct with no faithful JS RegExp equivalent (e.g. ungreedy `(?U)`, a scoped
	// `(?i:…)` group, or a mid-pattern inline flag). The source is stripped (never
	// ship a pattern that would crash or silently mismatch); the runtime treats the
	// matcher as a non-match (fail-closed) and the projector delegates every directive
	// that references it. See BUG-1 / regexflags.go.
	RegexUntranslatable bool `json:"regexUntranslatable,omitempty"`

	// Redacted is set when this matcher's literal value(s) were a purge-guard secret
	// (a token compared by `purge when …`) and were stripped from the IR so they never
	// ship to the public edge worker. The name/kind survive (for the coverage report);
	// the values do not. See DECISIONS.md D34.
	Redacted bool `json:"redacted,omitempty"`

	// ServerOnly marks a matcher kind that has no JavaScript runtime case — currently
	// `ip` (trusted-proxy real-IP ACL, no edge analogue, D.R02) and `upstream_healthy`
	// (live lb-pool liveness, no edge analogue, D49). The projector delegates every
	// directive that references such a matcher to the Cadish server behind (Fix #4);
	// the runtime treats it as a non-match (fail-closed) so a site that slipped one
	// through never silently mis-projects. Distinct from RegexUntranslatable (which is
	// regex-specific). `all` and `query` were server-only before IR v7; they became
	// edge-native in v7 (pure functions of request data the worker already parses).
	ServerOnly bool `json:"serverOnly,omitempty"`
	// contains filtered or unexported fields
}

Matcher is the `{kind, fields}` projection of one named matcher. Only the fields relevant to Kind are populated; the JS matcher switch mirrors this 1:1.

type Normalizer

type Normalizer struct {
	Source     string            `json:"source"` // header|cookie|query
	SourceName string            `json:"sourceName"`
	Map        map[string]string `json:"map"`
	Default    string            `json:"default,omitempty"`
}

Normalizer projects a `normalize` bucket map: read a request value, map it to a bounded bucket, else the default.

type OnError

type OnError struct {
	Scope       Scope  `json:"scope"`
	Status      int    `json:"status"`
	Body        string `json:"body"`
	ContentType string `json:"contentType"`
}

OnError is one `respond on_error [@scope] STATUS BODY` origin-error synthetic (D57/D76). Scope is the request-phase selector (Always for an unscoped rule); Status/Body/ContentType are fixed at compile. The worker serves the FIRST rule whose scope matches, on the outage path only.

type Purge

type Purge struct {
	Guard Scope `json:"guard"`
}

Purge is a purge guard scope. NOTE: as of D34 every `purge` directive is delegated to the Cadish server behind (the guard compares a SECRET token that must never live on a public edge worker), so this type is retained for the IR contract but the projector emits no edge-native purge — see Delegated.

type Recv

type Recv struct {
	Pass      []Scope    `json:"pass,omitempty"`
	Respond   []Respond  `json:"respond,omitempty"`
	Redirect  []Redirect `json:"redirect,omitempty"`
	Purge     []Purge    `json:"purge,omitempty"`
	Route     []Route    `json:"route,omitempty"`
	HeaderReq []Header   `json:"headerReq,omitempty"`
}

Recv is the RECV-phase projection: the terminal + header directives evaluated before the cache key.

type Redirect

type Redirect struct {
	Regex string `json:"regex,omitempty"`
	// RegexFlags is the JS RegExp flag string lifted from a RE2 inline flag group on
	// Regex (e.g. `redirect (?i)^/cams/?$`). The worker compiles
	// `new RegExp(regex, flags)`. Empty when the source carried no inline flags (BUG-1).
	RegexFlags string `json:"regexFlags,omitempty"`
	Scope      *Scope `json:"scope,omitempty"`
	Status     int    `json:"status"`
	Target     string `json:"target"`
	// NoStore, when true, means the directive carried a trailing `no_store` modifier.
	// The worker attaches Cache-Control: no-store, no-cache, must-revalidate, private
	// to the redirect Response so no shared cache or browser caches the redirect.
	// Additive v7 field (omitted when false — a stale runtime that misses it simply
	// sends no Cache-Control header, which is the pre-modifier behavior).
	NoStore bool `json:"noStore,omitempty"`
	// Headers is the directive's optional `{ header … }` block (CAD-3): literal
	// operator ops the worker applies to the 3xx Response AFTER Location and the
	// no_store Cache-Control. Location/Content-Length never appear (the compiler
	// rejects them; the runtime ALSO skips them defensively, preserving the
	// open-redirect authority guard on the TARGET). Additive v9 field: a stale
	// runtime that misses it omits the operator headers (a header-surface gap,
	// never a wrong Location/status), so the version is NOT bumped for this field.
	Headers []HeaderOp `json:"headers,omitempty"`
}

Redirect is a computed 3xx. Regex and/or Scope select: the combined scope+regex form carries BOTH (the worker requires the scope AND the regex to match).

type Respond

type Respond struct {
	Path    string     `json:"path"`
	Status  int        `json:"status"`
	Body    string     `json:"body"`
	Headers []HeaderOp `json:"headers,omitempty"`
}

Respond is a synthetic-response rule (`respond PATH STATUS BODY`).

Headers is the directive's optional `{ header … }` block (CAD-3): literal operator ops (set|append|remove only — never a template value or a delivery-phase special, compile-guaranteed) the worker applies to the synthetic Response after its base Content-Type. Additive v9 field (omitted when absent): a stale runtime that misses it serves the synthetic WITHOUT the operator headers — the pre-block behavior, a header-surface gap, never a wrong status/body/cache decision — so the version is NOT bumped for this field (same rationale as Redirect.NoStore in v7).

type Response

type Response struct {
	TTL          []TTL     `json:"ttl,omitempty"`
	Storage      []Storage `json:"storage,omitempty"`
	StripCookies []Scope   `json:"stripCookies,omitempty"`
	HeaderResp   []Header  `json:"headerResp,omitempty"`
	CORS         *CORS     `json:"cors,omitempty"`

	// HasStripHeaders is true iff at least one cache_ttl rule declares StripHeaders
	// (the from_header-family control headers X-Cache-Ttl/Grace/Max-Stale it consumes).
	// It is a build-time hint so the worker can SKIP the deliver/store-phase evalResponse
	// walk + delete loop entirely on the common config that has no from_header rule —
	// the walk runs only when it can actually strip something. Derivable from
	// TTL[].StripHeaders; surfaced here so the runtime need not scan to learn it.
	HasStripHeaders bool `json:"hasStripHeaders,omitempty"`

	// Transforms is the ordered `replace OLD NEW` deliver-phase body-substitution
	// rule list (D75). Each is edge-native within TransformMaxBytes: the worker
	// applies the literal substitution post-cache, on delivery, to a within-cap body,
	// skipping Range/HEAD/already-encoded responses — mirroring the server's V2e
	// gating (internal/server/transform.go). A body LARGER than the cap passes through
	// UNTRANSFORMED (same as the server's large-object behavior, and a separate
	// permanent server-only non-goal for unbounded/streaming `replace`). Omitted when
	// the site declares no `replace`.
	Transforms []Transform `json:"transforms,omitempty"`

	// TransformMaxBytes is the body-size ceiling (bytes) for edge-native `replace`:
	// a response body at or below it is buffered and transformed; a larger one streams
	// through untransformed. Mirrors the server's maxTransformBody (1 MiB). Present
	// only when Transforms is non-empty (zero-cost-when-unused).
	TransformMaxBytes int64 `json:"transformMaxBytes,omitempty"`

	// OnError is the ordered `respond on_error [@scope] STATUS BODY` rule list (D57,
	// edge-native in D76). On an origin HARD-failure with NO servable cached object
	// (no fresh/grace copy and no stale copy within the max_stale window), the worker
	// serves the first matching synthetic (status + body + content_type) instead of a
	// bare 502 — mirroring the server precedence: serve-stale-within-grace/max_stale >
	// cacheable negative cache > respond on_error > bare 502. Omitted when the site
	// declares no `respond on_error`.
	OnError []OnError `json:"onError,omitempty"`
}

Response is the ORIGIN/store-phase projection.

type Route

type Route struct {
	Scope    Scope  `json:"scope"`
	Upstream string `json:"upstream"`
}

Route is a `route @scope -> upstream` rule.

type Scope

type Scope struct {
	Always bool      `json:"always,omitempty"`
	Names  []string  `json:"names,omitempty"`
	Inline []Matcher `json:"inline,omitempty"`
}

Scope is an OR set of matcher ids (+ inline anonymous matchers). Always=true is an unconditional directive (a nil pipeline scope).

type Site

type Site struct {
	Hosts []string `json:"hosts"`

	// RedirectHosts is the trusted-host allowlist a `redirect` TARGET's {host}
	// placeholder is checked against — the normalized projection of the server's
	// trustedHosts (pipeline.normalizeRedirectHost over the site addresses: scheme/
	// port/path stripped, lower-cased, `*.suffix` wildcards preserved). The worker
	// echoes the inbound request Host into a redirect Location ONLY when it matches one
	// of these (exact, or HasSuffix for a `*.suffix` wildcard — the apex is NOT trusted
	// by a wildcard, mirroring hostSet.Match); otherwise it substitutes CanonicalHost.
	// This closes the edge open-redirect (parity with the server F12 fix). When Hosts
	// is empty the site declared no address (server trustedHosts == nil) and the worker
	// reflects the request Host verbatim, exactly as the server does. Declared hostnames
	// are already public config — no secret is added to the IR. Omitted when empty.
	RedirectHosts []string `json:"redirectHosts,omitempty"`

	// CanonicalHost is the site's primary host (first non-wildcard configured address,
	// scheme/port stripped, lower-cased) — the safe {host} value the worker substitutes
	// into a redirect Location when the inbound Host is NOT in RedirectHosts. Mirrors
	// pipeline.Pipeline.canonicalHost. Omitted when the site declares no address.
	CanonicalHost string `json:"canonicalHost,omitempty"`
}

Site is the host set the worker routes for.

type Storage

type Storage struct {
	SelKind    string   `json:"selKind"`
	Codes      []int    `json:"codes,omitempty"`
	Scope      *Scope   `json:"scope,omitempty"`
	Tier       string   `json:"tier"`
	RespHeader *Matcher `json:"respHeader,omitempty"` // optional response-phase resp_header AND-term (see TTL.RespHeader)
}

Storage is a storage tier rule (ram|disk). The edge maps tiers to its own L1/L2 policy later (the `edge {}` block); this preserves the server intent.

type SubMatcher added in v0.2.2

type SubMatcher struct {
	Ref    string `json:"ref"`
	Negate bool   `json:"negate,omitempty"`
}

SubMatcher is one term of an `all` AND-conjunction: a reference to a named matcher (Ref, always resolvable — ViewMatchers projects every named matcher) XOR'd with Negate (`!@x`). The worker evaluates matchOne(ir.matchers[Ref]) and compares it to Negate (`got == negate ⇒ fail`), the same per-term semantics the server's secTerm uses.

type TTL

type TTL struct {
	SelKind    string `json:"selKind"`
	Codes      []int  `json:"codes,omitempty"`
	Scope      *Scope `json:"scope,omitempty"`
	TTL        string `json:"ttl,omitempty"`
	Grace      string `json:"grace,omitempty"`
	HitForMiss string `json:"hitForMiss,omitempty"`
	IsHFM      bool   `json:"isHFM,omitempty"`
	// MaxStale is the max_stale (D60) window as a Go duration string: the additional
	// span beyond ttl+grace during which the edge may serve a stored copy ONLY as a
	// stale-on-error fallback (origin failed). "" => no error-fallback window; the edge
	// must NOT serve a copy older than ttl+grace. Bounds the worker's salvage path so
	// the edge stops serving unboundedly-old content (D70).
	MaxStale string `json:"maxStale,omitempty"`
	// FromHeader names an origin response header the edge reads the TTL from
	// (`cache_ttl from_header HEADER`); "" => the static TTL above. A bare integer
	// value is seconds (Cache-Control max-age style); a unit spelling is a cadish
	// duration. Absent/unparseable => the rule does not apply (fall through).
	FromHeader string `json:"fromHeader,omitempty"`
	// GraceFromHeader / MaxStaleFromHeader name origin response headers the edge reads
	// the grace / max_stale window from (`grace_from_header NAME` /
	// `max_stale_from_header NAME`); "" => the static Grace / MaxStale above. Same
	// parse convention as FromHeader; an absent/unparseable value falls back to the
	// literal (it does NOT make the rule fall through). The worker mirrors the server's
	// resolveGraceMaxStale, including the resolved max_stale >= grace clamp.
	GraceFromHeader    string `json:"graceFromHeader,omitempty"`
	MaxStaleFromHeader string `json:"maxStaleFromHeader,omitempty"`
	// RespHeader is an optional RESPONSE-phase `resp_header NAME VALUE` matcher ANDed in
	// front of the selector: the rule applies only when the origin response carries the
	// named header value AND the selector matches. nil for every non-resp_header rule. The
	// worker evaluates it first (selectorMatches), so Go and JS branch freshness on the
	// origin response identically (a conformance fixture asserts it).
	RespHeader *Matcher `json:"respHeader,omitempty"`
	// StripHeaders names the from_header-family control headers this rule CONSUMES from
	// the origin response (X-Cache-Ttl/X-Cache-Grace/X-Cache-Max-Stale). The worker
	// removes them before store + deliver, mirroring the server (handler.go), so the
	// internal origin↔cache contract never leaks to the client. Omitted for a plain rule.
	StripHeaders []string `json:"stripHeaders,omitempty"`
}

TTL is a cache_ttl rule. SelKind is status_in|status_not_in|scope|default. TTL, Grace, HitForMiss are Go duration strings (e.g. "1m0s") so the JS side parses them with the same semantics.

type Tenant

type Tenant struct {
	FromHeader string      `json:"fromHeader,omitempty"` // "" => derive from Host
	Rules      []TenantMap `json:"rules"`
	Default    string      `json:"default,omitempty"`
}

Tenant projects a request-derived `tenant { … }` resolver.

type TenantMap

type TenantMap struct {
	Pattern string `json:"pattern"`
	Name    string `json:"name"`
}

TenantMap is one tenant pattern->name rule.

type Transform

type Transform struct {
	Scope Scope  `json:"scope"`
	Old   string `json:"old"`
	New   string `json:"new"`
}

Transform is one `replace OLD NEW` deliver-phase literal body substitution (D75). The worker applies these in order with String.replaceAll semantics (every occurrence of Old becomes New), identical to the server's applyReplacements.

type Upstream

type Upstream struct {
	To string `json:"to,omitempty"`
}

Upstream is the origin the worker fetches. To is the default upstream `to` name (best-effort projection of the site default; the worker resolves the concrete URL from its binding). Empty when the site declares no default upstream.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL