/** * Opt-in crash relay to a Sentry-compatible upstream. * * This is a **second, separate** egress channel from `gjc crash report`. The * issue flow keeps its per-invocation, digest-confirmed consent boundary; this * one is gated by configuration instead, and is therefore deliberately much * narrower in what it can ever emit: * * 1. `crashReport.upstream` must be `sentry`. The default is `off`, and while * it is off this module performs no IO at all — not even a state read. * 2. An operator must supply a DSN. No DSN literal is compiled into the binary, * so a build has no destination to fall back to; an unset DSN is a hard stop * and never means "use ours". * 3. Every crash-derived byte must pass `sanitizeExternalCrashV1`. A refusal * drops that signature entirely. There is no less-sanitized fallback path. * * The relay never runs on the fatal path. A crashing process still does exactly * one `O_APPEND` write and dies; relaying happens at the *next* startup, after * compaction, where blocking and failing are both safe. */ import { type CrashSignatureView, type CrashStatePaths } from "../index-store"; import { type SentryDsn } from "./dsn"; /** Version of the complete sanitizer/egress contract persisted in refusals. */ export declare const SANITIZER_EGRESS_CONTRACT_VERSION = "sanitize-external-crash-v1"; /** Trusted environment variable form of the DSN, for CI and one-off runs. */ export declare const CRASH_UPSTREAM_DSN_ENV = "GJC_CRASH_SENTRY_DSN"; export interface CrashRelayConfig { readonly upstream: "off" | "sentry"; readonly dsn: string; } /** * The only settings surface the relay is allowed to read. * * `Settings.get` merges project `.gjc` configuration into the answer, so using * it here would let merely opening a repository turn the relay on and choose * its destination — an untrusted checkout could redirect crash signatures that * were recorded long before it was cloned. Both keys are therefore read from * the user/global layer only, which is exactly what `getGlobal` documents * itself for. */ export interface TrustedRelaySettings { getGlobal(path: "crashReport.upstream" | "crashReport.upstreamDsn"): unknown; } /** * Resolve the relay configuration from the trusted layer. * * The values are re-validated rather than trusted by type. `getGlobal` reports * whatever the hand-editable global config file holds, and it returns * `undefined` instead of a schema default, so anything that is not literally * `"sentry"` lands on `off` and anything that is not a string lands on an empty * DSN. Both absent and malformed therefore fail closed. */ export declare function readTrustedRelayConfig(settings: TrustedRelaySettings): CrashRelayConfig; /** * The exact shape the relay uses. Narrower than `typeof fetch` on purpose: the * relay only ever issues one POST to a known URL, and depending on the full * runtime signature (Bun adds `preconnect`) would force every caller and test * double to fake surface this module never touches. */ export type CrashRelayFetch = (url: string, init: RequestInit) => Promise; export type CrashRelaySkip = "disabled" | "no-dsn" | "invalid-dsn" | "nothing-to-relay"; export type CrashRelayOutcome = { readonly status: "skipped"; readonly reason: CrashRelaySkip; } | { readonly status: "ran"; /** Signatures the upstream accepted and that are now stamped `relayedAt`. */ readonly sent: number; /** Signatures dropped because the sanitizer refused a field. */ readonly refused: number; /** Signatures the upstream rejected or that failed in transport. */ readonly failed: number; }; /** * Severity of the store being relayed. * * Fatal crashes and handled tool failures live in separate files on purpose -- * handled errors are high-volume and would otherwise evict the rare, precious * fatal records from a shared cap. They are relayed through the same code with * the same egress contract, and differ upstream only by `level`, so a single * project can hold both without the noisy class drowning the signal. */ export type CrashRelaySeverity = "fatal" | "error"; export interface CrashRelayOptions { readonly config: CrashRelayConfig; readonly paths?: CrashStatePaths; readonly handledPaths?: CrashStatePaths; readonly severity?: CrashRelaySeverity; readonly env?: Record; readonly fetchImpl?: CrashRelayFetch; readonly now?: () => number; readonly maxPerRun?: number; readonly platform?: string; readonly release?: string; readonly bunVersion?: string; } /** * Resolve the destination. Explicit config wins over the environment so a * machine-wide export cannot silently redirect a configured install. */ export declare function resolveRelayDsn(config: CrashRelayConfig, env?: Record): { ok: true; dsn: SentryDsn; } | { ok: false; reason: CrashRelaySkip; }; /** * A signature is due when new journal-append-order occurrences exist after the * last durable watermark. `lastSeen` is display-time only and must not hide a * backdated occurrence. Legacy indexes that only have `relayedAt` stay covered * when that wall-clock stamp still covers `lastSeen`. After a downgrade that * advanced `relayedAt` without rewriting `relayedRecordId`, the same lastSeen * coverage applies only when the latest append is also the lastSeen record. */ export declare function isRelayDue(signature: CrashSignatureView): boolean; /** * Automatic relay always reads the trusted agent stores. XDG-aware paths remain * available to ordinary crash state operations, but never select automatic-egress * input files. */ export declare function resolveTrustedRelayStatePaths(): CrashStatePaths; export declare function resolveTrustedHandledRelayStatePaths(): CrashStatePaths; /** * Relay every due signature once. Never throws: a broken upstream, an offline * machine or a corrupt crash log must not be able to take down startup. */ export declare function relayCrashSignatures(options: CrashRelayOptions): Promise; /** Paths of the handled-error store. Never XDG: a supplied directory is joined directly. */ export declare function resolveHandledErrorStatePaths(agentDir?: string): CrashStatePaths; /** * Relay both stores in one pass. * * Fatal crashes go first: they are rarer and more valuable, so when the * per-run cap binds they must not be starved by a noisy handled-error class. * The cap is shared across both stores. The gate is evaluated once by the * first call, and a skip there means the same skip applies to the second, so * `off` still performs no IO at all. */ export declare function relayAllSignatures(options: CrashRelayOptions): Promise;