/** Producer-declared severity of one event (unchanged from the §5.2 wire shape). */ export type SecuritySeverity = "low" | "medium" | "high"; /** * The §5.4 routing class. It decides the Cloud Logging severity, and therefore which * bucket the event lands in and whether it can page. * * - `integrity` — an integrity breach. ERROR unconditionally; ~never fires in healthy * production, so it is safe to page on. * - `abuse` — abuse telemetry. WARNING unconditionally, because these are EXPECTED * under attack and a paging alert on them is a denial-of-service against the operator * (SE-6). Counters and rate alerts read them; nothing pages. * - `other` — host-internal signal, mapped by the producer's own severity * (low→INFO, medium→WARNING, high→ERROR). */ export type SecurityEventClass = "integrity" | "abuse" | "other"; export interface SecurityEventDef { readonly class: SecurityEventClass; /** * The §6 mitigation or threat this kind serves, and the producer that emits it. * An entry that cannot name one does not belong here — that is the property that * keeps the registry from re-growing spare slots. */ readonly why: string; } /** * The closed kind vocabulary. Adding a kind here is an amendment to * `SECURITY_EVENTS_STREAM_SPEC` Appendix A, and the two-way drift check in each repo * requires it to have a producer before it can land. */ export declare const SECURITY_EVENT_KINDS: { readonly "zip-integrity:coordinate-mismatch": { readonly class: "integrity"; readonly why: "A zip's sidecar names a different repo than the load request — the PR-redirection / baseline-poisoning shape (RL-1 / CT-1)."; }; readonly "zip-integrity:blob-mismatch": { readonly class: "integrity"; readonly why: "A file's content does not hash to the blob SHA its manifest entry claims."; }; readonly "zip-integrity:tree-mismatch": { readonly class: "integrity"; readonly why: "The manifest's tree does not reconstruct the commit's tree SHA."; }; readonly "zip-integrity:extra-entries": { readonly class: "integrity"; readonly why: "The zip carries entries the (untruncated) manifest does not list — content smuggled past the manifest."; }; readonly "zip-integrity:commit-tree-mismatch": { readonly class: "integrity"; readonly why: "The commit object does not point at the tree the manifest reconstructs."; }; readonly "artifact-integrity:distrust": { readonly class: "integrity"; readonly why: "A pre-transpiled artifact for this commit failed spot-verification, so every artifact for the commit is distrusted (PRETRANSPILED_ARTIFACTS_SPEC §5.7)."; }; readonly "registry:layer2-chrome-moving-ref-refused": { readonly class: "integrity"; readonly why: "A deployment-layer override of a CHROME region named a moving ref instead of a SHA pin and was dropped for the build default (UI_AS_APPS_SPEC §3.3 / T3)."; }; readonly "registry:principal-claim-refused": { readonly class: "integrity"; readonly why: "A non-build-default binding tried to claim a broad-elevated / first-party principal — attempted privilege escalation via the principal sugar (R3-98 S2)."; }; readonly "registry:region-claim-refused": { readonly class: "integrity"; readonly why: "A registry layer (deployment/synced/device/dev-override) named a region no build default declares — an attempt to mint a region the kernel never declared; dropped and reported (WORKBENCH_MODES_SPEC §10 G-MODE-1 / R-MODE-1)."; }; readonly "release:unpinned-refused": { readonly class: "integrity"; readonly why: "A production config named a UI release with no `sha256` pin. The pin is the only integrity anchor not living on the registry origin being defended against (UI_RELEASES_SPEC §4.1)."; }; readonly "release:digest-mismatch": { readonly class: "integrity"; readonly why: "A fetched release lock did not match its index digest or its deployment pin."; }; readonly "release:entry-invalid": { readonly class: "integrity"; readonly why: "A release lock entry failed structural validation — a malformed or hostile lock on the shared registry origin."; }; readonly "release:first-party-caps-stripped": { readonly class: "integrity"; readonly why: "A release repointed a region and its first-party-only capabilities were stripped (UI_RELEASES_SPEC §6.1b) — a release carries code, never authority."; }; readonly "secrets:dev-prf-bypass-active": { readonly class: "integrity"; readonly why: "The dev PRF unseal bypass is armed. It cannot arm in a production bundle (the §5.3 sentinel gate); if this is ever seen from production, the guardrail failed."; }; readonly "capability:forbidden": { readonly class: "abuse"; readonly why: "The §8.4 gate refused an app→host call. The T24 probing-detection signal — high traffic by design, so it must never page. Renamed from `gate:forbidden`, which is the rename R3-343 caught."; }; readonly "oauth:state-invalid": { readonly class: "abuse"; readonly why: "An OAuth callback carried a `state` whose UNSIGNED nonce was not a plain name (traversal, separators, NUL, over-long) and was refused before any pending-flow read — a crafted callback link steering `loadPending` outside the pending dir (R3-366; bounded today by the backend's signature check, recorded so the attempt is visible)."; }; readonly "net-fetch:ssrf-refused": { readonly class: "abuse"; readonly why: "A `net:fetch` named an SSRF-blocked host and was refused before any request left the browser (§5.11 / T40)."; }; readonly "security-stream:app-flooding": { readonly class: "abuse"; readonly why: "An app exceeded its per-window event budget and is itself a flagged anomaly (T46) — an app must not be able to bury a real signal."; }; readonly "security-stream:forward-dropped": { readonly class: "abuse"; readonly why: "The host forwarder dropped events at its buffer cap before this batch. Recorded so a drop-capped stream is visible rather than merely shorter — the count was previously sent and discarded."; }; readonly "contribute:rate-limited": { readonly class: "abuse"; readonly why: "A contribute PR creation was refused at the backend chokepoint for exceeding the per-(uid, appKey) or per-uid PR-creation budget (TRUST_AND_SAFETY_SPEC §6 TS-18 — identity abuse, fail-closed). Expected under spam, so WARNING, never pages."; }; readonly "focus:denied-no-activation": { readonly class: "abuse"; readonly why: "A column-focus transfer was requested without a real user gesture and was refused (FT-1 / FT-6)."; }; readonly "focus:rate-limited": { readonly class: "abuse"; readonly why: "Column-focus transfers exceeded the flicker budget (FT-3, capacity-class and fail-open)."; }; readonly "focus:cross-activity-denied": { readonly class: "abuse"; readonly why: "An `editor:reveal` cross-activity move was refused because no activity owning the editor could be resolved from host state (TOOLS_ACTIVITY_SPEC §5.2, R3-389)."; }; readonly "space-write:throttled": { readonly class: "abuse"; readonly why: "A frame's write through a space export was refused at the per-(reader, space) token-bucket budget after the bounded wait (SPACES_UI_SPEC §6.6, TS-13's fail-closed quota precedent). Emitted at most once per second per key — the flood must not flood. Expected under the presenting heartbeat class of load; counted, never paged."; }; readonly "opener:open-suppressed": { readonly class: "abuse"; readonly why: "A marker-initiated `open-declared` was refused by the §4b.3 open budget: the carrying resource's or the calling app's allowance was spent, or the meter's key table was full — the bound on app-planted markers relaunching a bound opener (BUNDLE_EMBEDDING §4b.3/§4b.7 SA-7, R3-547). Expected under attack; counted, never paged. NOT emitted for an unwired budget, which is a host defect, nor for the refused-probe ledger."; }; readonly "device:capture": { readonly class: "other"; readonly why: "A device read SUCCEEDED (a geolocation fix delivered, a camera/microphone capture handed over) — the attribution BROWSER_CAPABILITIES_SPEC §2 promises: geolocation has no on-screen indicator, so without this a successful read leaves no trace. Carries the app, the capability and a coarse count — never payload content (no coordinates, no frames)."; }; readonly "overlay:heal": { readonly class: "other"; readonly why: "The overlay sweep healed phantom entries or evicted a legacy migration (R3-28). Renamed from `overlay-sweep:`, the second rename R3-343 caught."; }; readonly "bundle-extent:outside-write": { readonly class: "other"; readonly why: "A write landed outside its bundle's declared extent. C0 OBSERVES — nothing was refused, and the row is about the declaration mismatch, not an incident."; }; readonly "release:fetch-failed": { readonly class: "other"; readonly why: "The release index or lock could not be fetched or timed out; the build-in fallback was used."; }; readonly "release:unknown-region": { readonly class: "other"; readonly why: "A release lock named a region this build does not know — a forward-compatible lock, or a typo."; }; readonly "release:region-missing": { readonly class: "other"; readonly why: "A release lock omitted a region this build expects, which resolves from the build default."; }; readonly "chrome:activity-dropped": { readonly class: "other"; readonly why: "An activity referenced a region no layer binds, so it was dropped from the rail rather than rendering a dead affordance."; }; readonly "csp:violation": { readonly class: "other"; readonly why: "Something was blocked by the host-origin CSP. HOST_ORIGIN_HARDENING_SPEC §2.1 requires these reports reach a backend-owned endpoint; R3-343 measured them reaching it and being discarded."; }; }; export type SecurityEventKind = keyof typeof SECURITY_EVENT_KINDS; /** All registered kinds, sorted — the drift check's "registry side". */ export declare const securityEventKinds: () => string[]; /** The definition for `kind`, or `null` if unregistered (→ reject that event). */ export declare const securityEventDef: (kind: string) => SecurityEventDef | null; /** The §5.4 class of a kind, or `null` if the kind is not registered. */ export declare const classifySecurityKind: (kind: string) => SecurityEventClass | null; /** * Cloud Logging severity (the SE-6 split): `integrity`→ERROR, `abuse`→WARNING, * `other`→by the producer's own severity. An UNREGISTERED kind maps as `other` so a * caller that logs one anyway (the rejection record) still gets a sane severity — it * is never a path to acceptance, which `validateSecurityEvent` owns. */ export declare const classifySecuritySeverity: (kind: string, severity: SecuritySeverity) => "ERROR" | "WARNING" | "INFO"; export interface SecurityEvent { /** Registry-resolved, `:`. Typed as the union at the producer seam, * so an unregistered kind is a COMPILE error in `site-main` and the runtime check * below is the backstop for anything that reaches the wire another way. */ kind: SecurityEventKind; severity: SecuritySeverity; detail?: Record; /** * Attributing app when the event was caused by a specific app's call. Recorded ONLY * as an UNVERIFIED label unless the batch was App-Check-attested (SE-5) — never * trusted for alerting or auto-mitigation on its own. */ app?: string; /** Dedup count: identical repeats coalesced into this entry (T46). */ repeated?: number; /** ISO timestamp, stamped on emit. */ at: string; } export interface SecurityEventBatch { events: SecurityEvent[]; /** Opaque session id — no identity, no cross-session linkage (SE-7). */ session?: string; /** Count of events the host forwarder dropped at its buffer cap since the last * batch (drop-cap, never grow). */ dropped?: number; } /** Batch size cap — mirrors the host forwarder's `DEFAULT_MAX_BATCH`. */ export declare const SECURITY_MAX_BATCH = 50; /** At most 8 keys per `detail`. */ export declare const SECURITY_DETAIL_MAX_KEYS = 8; /** Detail strings capped at 256 characters. */ export declare const SECURITY_DETAIL_MAX_STR = 256; /** * Validate ONE event against the registry and the §5.5 discipline. Returns `null` * when valid, or a human-readable reason. * * FAIL-CLOSED, and it REJECTS rather than strips: silently stripping an offending * detail key turns a leak into a green test (SE-2/3). Shared by the host hygiene pass * and the backend ingest gate so the two can never drift into disagreeing about the * same payload — which is the whole lesson of R3-343. * * The reason string for an unregistered kind is load-bearing: `validateSecurityBatch` * matches on the `unknown kind:` prefix to decide granularity, so keep it stable. */ export declare const validateSecurityEvent: (e: unknown) => string | null; /** One rejected event, named so an operator query can group by it. */ export interface SecurityRejection { kind: string; reason: string; } export type SecurityBatchResult = { ok: true; events: SecurityEvent[]; session?: string; dropped?: number; rejected?: SecurityRejection[]; } | { ok: false; reason: string; kind?: string; rejected?: SecurityRejection[]; }; /** * Validate a POSTed batch (§5.5). * * GRANULARITY, decided deliberately and recorded here rather than left to the reader * (R3-343's fourth deliverable): **an unregistered KIND rejects only its own event; a * DETAIL-discipline violation rejects the whole batch.** * * The two failures are not the same failure, and the original rule treated them as * one. Whole-batch rejection exists so a LEAK fails a test (SE-2/3) — an unknown * `detail` key might be the leak, and stripping it quietly is exactly what SE-2/3 * forbids, so the whole batch still goes. Applied to a VOCABULARY DRIFT the same rule * converts a rename into a total outage of the stream: the host forwarder buffers up * to 50 events per POST, so one routinely-emitted unregistered kind * (`capability:forbidden`, on every refused app→host call) destroyed up to 49 valid * co-batched events per flush. That is not a leak defence, it is an availability bug * wearing one. * * Both outcomes are OBSERVABLE — the caller logs `rejected` with the offending kinds. * A fail-closed validator whose rejections are logged nowhere is indistinguishable * from a healthy empty stream, which is precisely how R3-343 presented for a month. */ export declare const validateSecurityBatch: (body: unknown) => SecurityBatchResult;