/** * The identity tier a row is written at (§4). Decided AT EMIT and recorded — never * inferred later, because auth restore is asynchronous and resolves after boot, so an * event emitted before it lands is honestly T1 even for a signed-in user. * * - `T0` — unkeyed. Not "anonymous": the controller terminates TLS on the beacon and * writes T0 and T2 into the same sink, so a T0 row is singled-out-able within a * session under Recital 26. §6's bucketing reduces identifiability; nothing here * depends on it conferring anonymity. * - `T1` — an ephemeral in-memory session id, lost on reload. * - `T2` — `HMAC(uid, subkey)`, derived SERVER-SIDE (§4.1). Never client-side. */ export type TelemetryTier = "T0" | "T1" | "T2"; /** * Which sink table a row lands in, and therefore its retention (§8). * * - `product` — the §6 table. 13-month raw retention (the §10 exemption ceiling). * - `error` — §9 operator error/crash rows. Same ceiling. * - `boundary`— §6.2 dispatch observation. **30 days, no pseudonym, operator-only, * and excluded from the §13 analysis surface.** Held to the security-events * discipline deliberately: a per-pseudonym record of which repositories ran and * which apps were invoked is the server-side equivalent of the `mounts:registry` * cross-app activity oracle that `UI_AS_APPS_SPEC` §8.9 rules permanently * ungrantable. * - `app` — APP_ANALYTICS_SPEC events. Separate table, separate transport, * validated host-side per event; never share a batch with host events. */ export type TelemetryEventClass = "product" | "error" | "boundary" | "app"; export interface TelemetryEventDef { /** * The per-name declared key set (§5). An UNKNOWN KEY REJECTS THE EVENT — it is * never stripped, so a leak fails a test rather than passing quietly. */ readonly props: readonly string[]; /** * The highest tier this event may ever carry. The emitter computes * `min(maxTier, tier available at emit)`; ingest rejects anything above the * ceiling. `boot.*` is T0 forever — boot health must never become keyed. */ readonly maxTier: TelemetryTier; readonly class: TelemetryEventClass; /** * Whether a symbolicated HOST-ORIGIN frame list may ride this event (§9). Only * error rows set it. A frame is not a `prop` — the §5 scalar discipline does not * reach it — so carrying frames is an explicit, registry-gated exception rather * than an accident of the payload shape. */ readonly frames?: true; /** The §6 product question this answers. An entry that cannot name one is not collected. */ readonly question: string; } /** * The closed event vocabulary. Adding a name here is a spec amendment to §6's table. * * Dimension props are BUCKETED, normatively (§6): viewport to breakpoint buckets, * browser and OS to families, **region not city** (a resolved city is far more * identifying than the JS timezone the earlier draft dropped as "the high-entropy * bit"), and **no JS timezone at all**. The bucketing is applied host-side before * emit; the key names below are what the wire may carry. */ export declare const TELEMETRY_EVENTS: { readonly "session.start": { readonly props: readonly ["country", "region", "browser", "browserMajor", "os", "formFactor", "viewport", "referrerClass"]; readonly maxTier: "T2"; readonly class: "product"; readonly question: "Daily users, visit frequency, retention; where users are and on what devices. `referrerClass` answers the sessions-with-no-auth-and-an-external-referrer row — which is NOT a first-time-visitor rate (§6)."; }; readonly "boot.ok": { readonly props: readonly ["ms", "cold", "routeKind", "provider", "signedIn"]; readonly maxTier: "T0"; readonly class: "product"; readonly question: "Does the site boot — and on which route kind, provider and auth state?"; }; readonly "boot.fail": { readonly props: readonly ["failureClass", "ms", "routeKind", "provider", "signedIn"]; readonly maxTier: "T0"; readonly class: "product"; readonly question: "Does the site boot — and when it does not, in which class does it fail, on which route kind, provider and auth state?"; }; readonly "app.run": { readonly props: readonly ["coordinate", "coordinateClass", "cold", "contract"]; readonly maxTier: "T2"; readonly class: "product"; readonly question: "Which repositories are run?"; }; readonly "app.contribute": { readonly props: readonly ["coordinate", "coordinateClass"]; readonly maxTier: "T2"; readonly class: "product"; readonly question: "Which repositories are contributed to, and the share of users who contribute (over a stated denominator)."; }; readonly "project.create": { readonly props: readonly ["template"]; readonly maxTier: "T1"; readonly class: "product"; readonly question: "New-project creation. Rides an authenticated request; identity is discarded at the sink."; }; readonly "dispatch.observed": { readonly props: readonly ["kind", "method", "outcome", "targetIndex", "streaming"]; readonly maxTier: "T0"; readonly class: "boundary"; readonly question: "What do apps do at the dispatch boundary — operator-only, 30 days, no pseudonym, and excluded from the analysis surface."; }; readonly "perf.load": { readonly props: readonly ["coordinate", "coordinateClass", "cold", "cacheHit", "loadSource", "interactiveMs", "transpileMs", "depsMs"]; readonly maxTier: "T0"; readonly class: "product"; readonly question: "Per-app load time in the field — how long a real app takes to become interactive on a real network."; }; readonly "migration.bareRead": { readonly props: readonly ["collection"]; readonly maxTier: "T0"; readonly class: "product"; readonly question: "How many reads still resolve the bare (pre-tenant) layout during the tenancy migration window — the Phase 3 drain signal (R3-678/R3-680)."; }; readonly "error.host": { readonly props: readonly ["name", "fingerprint", "scope", "message", "sandboxFrames", "externalFrames", "repeated"]; readonly maxTier: "T0"; readonly class: "error"; readonly frames: true; readonly question: "Is the host failing, where, and how often — the content-free half of `does the site boot` that would have caught the post-deploy chunk-404 incident."; }; readonly "error.app": { readonly props: readonly ["app", "name", "scope", "sandboxFrames", "repeated"]; readonly maxTier: "T0"; readonly class: "error"; readonly question: "Are apps failing, and which — the content-free operator row. The full trace goes to the user's screen, never to us."; }; readonly "gate.invalidParamsExtra": { readonly props: readonly ["method", "key"]; readonly maxTier: "T0"; readonly class: "boundary"; readonly question: "Do callers send param keys the method's paramsSchema does not declare — the pre-enforcement signal for R3-1083's additionalProperties flip (a hit names a real caller, so the flip would break it)."; }; readonly "gate.invalidResult": { readonly props: readonly ["method", "path"]; readonly maxTier: "T0"; readonly class: "boundary"; readonly question: "Do handlers' success values fail their resultSchema — the R3-1087 report-only check made visible before any enforcement."; }; readonly "csp.violation": { readonly props: readonly ["directive", "disposition", "blockedOrigin", "sourceFile", "line", "severity"]; readonly maxTier: "T0"; readonly class: "error"; readonly question: "Is anything being injected into the host origin, and which directive caught it?"; }; readonly "llm.call": { readonly props: readonly ["provider", "model", "promptTokens", "completionTokens", "latencyMs", "errorClass", "byoKey"]; readonly maxTier: "T2"; readonly class: "product"; readonly question: "LLM intensity — which models, at what volume and latency, and with what error classes."; }; readonly "store.usage": { readonly props: readonly ["docReads", "docWrites", "listenerMinutes", "mountedSpaces"]; readonly maxTier: "T2"; readonly class: "product"; readonly question: "Per-user store usage — document reads/writes, live-listener minutes, mounted spaces per window (STORE_PORTABILITY §12 signals 2/3: the migration trigger's evidence)."; }; readonly "space.stats": { readonly props: readonly ["fileCount", "totalSizeBytes"]; readonly maxTier: "T1"; readonly class: "product"; readonly question: "The distribution of mounted-space sizes (file count, total bytes per space per window — never a space id) — what sizes the migration must move (STORE_PORTABILITY §12 signal 3)."; }; readonly "space.backingBound": { readonly props: readonly ["leg"]; readonly maxTier: "T0"; readonly class: "product"; readonly question: "Does the backing-store containment fire in the field — a stuck space read answering the picker's fallback or refusing a picker write at the bound (R3-938), or the mount handshake expiring (R3-947) — and on which leg?"; }; }; export type TelemetryEventName = keyof typeof TELEMETRY_EVENTS; /** All registered names, sorted — the drift check's "registry side". */ export declare const telemetryEventNames: () => string[]; /** The definition for `name`, or `null` if unregistered (→ reject). */ export declare const telemetryEventDef: (name: string) => TelemetryEventDef | null; export interface TelemetryEvent { /** Registry-resolved, `.`. */ name: string; /** ISO timestamp, stamped on emit. */ at: string; /** Decided at emit (§4), never inferred later. */ tier: TelemetryTier; props?: Record; /** * Host-minted (§7). The app never chooses, reads or influences it — an app-visible * correlation id is a covert channel for linking one user across apps. Each side * records only its own leg; the join happens in the sink, never on the wire. */ causationId?: string; /** Symbolicated HOST-ORIGIN frames (§9). Only for events whose def sets `frames`. */ frames?: string[]; } export interface TelemetryBatch { events: TelemetryEvent[]; /** Count of events dropped since the last batch (drop-cap, never grow). */ dropped?: number; } /** At most 8 keys per event. */ export declare const TELEMETRY_MAX_PROP_KEYS = 8; /** Strings capped at 256 characters. */ export declare const TELEMETRY_MAX_STR = 256; /** Batch size cap — mirrors the security-events forwarder's `DEFAULT_MAX_BATCH`. */ export declare const TELEMETRY_MAX_BATCH = 50; /** Frame-list cap for §9 error rows: at most this many, each ≤ {@link TELEMETRY_MAX_STR}. */ export declare const TELEMETRY_MAX_FRAMES = 32; /** * Validate ONE event against the registry and the §5 discipline. Returns `null` when * valid, or a human-readable reason. * * FAIL-CLOSED, and it REJECTS rather than strips: silently stripping an offending key * turns a leak into a green test. 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. */ export declare const validateTelemetryEvent: (e: unknown) => string | null; export type TelemetryBatchResult = { ok: true; events: TelemetryEvent[]; dropped?: number; } | { ok: false; reason: string; name?: string; }; /** * Validate a POSTed batch. * * GRANULARITY, decided deliberately (and recorded in the spec rather than left to the * reader — R3-343's fourth deliverable): **an unregistered NAME rejects only its own * event; a props-discipline violation rejects the whole batch.** The two failures are * not the same failure. Whole-batch rejection exists so a LEAK fails a test (SE-2/3); * applied to a vocabulary drift it converts a rename into a total outage of the * stream, which is precisely what R3-343 measured — a single routinely-emitted * unregistered kind destroying up to 49 valid co-batched events per flush. * * Both outcomes are OBSERVABLE: the caller logs `rejected` with the offending names. * A fail-closed validator whose rejections are logged nowhere is indistinguishable * from a healthy empty stream. */ export declare const validateTelemetryBatch: (body: unknown) => TelemetryBatchResult & { rejected?: { name: string; reason: string; }[]; };