/** * `Capture` — pure observation builder for a single `web.fetch` invocation. * The transport in `fetch-core.ts` feeds it DNS/TCP/TLS/header/redirect/ * cookie events; it accumulates them into the {@link WebFetchMetadata} * fields (timing, TLS info, headers, redirect chain, resolved IP/hostname, * cookies — each capped, see MAX_REDIRECT_HOPS/MAX_COOKIES_CAPTURED). * * Only the last hop's timing/DNS/TLS values are kept on redirect. No I/O * happens here — it's a pure data sink so the transport layer can be * tested independently. Redaction and the 64 KiB metadata budget are * applied later by `redact.ts` / `budget.ts`. */ import type { IncomingHttpHeaders } from "node:http"; import type { TLSSocket } from "node:tls"; import { type CookieInfo, type HeaderMap, type RedirectChain, type TimingInfo, type TlsInfo } from "./types.js"; /** * Snapshot of the structured fields the builder has accumulated when * `fetch-core.ts` finalises the response. The shape matches the slice of * {@link WebFetchMetadata} that depends on per-hop transport * observation; the fetch handler combines this with `requestedUrl`, * `finalUrl`, `mode`, `bytesReceived`, `truncated`, etc. before applying * `redact.applyToHeaders` / `redact.applyToCookies` and `budget.enforce`. */ export interface CapturedFields { /** IP address contacted on the final hop. */ resolvedIp: string; /** Hostname of the final hop (the one whose body is returned). */ finalHostname: string; /** Integer HTTP status of the final response. */ status: number; /** Lowercased response headers from the final hop, repeats joined. */ headers: HeaderMap; /** TLS session details from the final hop, when the scheme was https. */ tls?: TlsInfo; /** Per-phase timings; `tlsMs` is omitted on http:// requests. */ timing: TimingInfo; /** Up to {@link MAX_REDIRECT_HOPS} hops in chronological order. */ redirectChain: RedirectChain; /** Up to {@link MAX_COOKIES_CAPTURED} cookies parsed from `Set-Cookie`. */ cookies: CookieInfo[]; } /** * Pure builder that records the per-hop observations made by * `fetch-core.ts` and assembles them into a {@link CapturedFields} * snapshot via {@link Capture.finalize}. * * The builder is single-use: callers should construct one `Capture` per * `web.fetch` invocation and discard it after `finalize`. */ export declare class Capture { /** Final-hop DNS-resolution time in milliseconds. */ private dnsMs; /** Final-hop TCP-connect time in milliseconds. */ private tcpMs; /** Final-hop TLS-handshake time in milliseconds (https only). */ private tlsMs; /** Final-hop time-to-first-byte in milliseconds. */ private ttfbMs; /** Final-hop resolved IP, set by {@link markDnsResolved}. */ private resolvedIp; /** Final-hop hostname, set by {@link setHopContext}. */ private finalHostname; /** Final-hop HTTP status, set by {@link markResponse}. */ private status; /** Final-hop normalised headers, set by {@link markResponse}. */ private headers; /** Final-hop TLS info (https only), set by {@link markTlsHandshaked}. */ private tls; /** Redirect hops, capped at {@link MAX_REDIRECT_HOPS}. */ private readonly redirectChain; /** Captured cookies, capped at {@link MAX_COOKIES_CAPTURED}. */ private readonly cookies; /** * Whether the current invocation is an `https://` fetch. When `false`, * `tlsMs` is omitted from {@link TimingInfo} and `tls` from * {@link CapturedFields} per Requirements 2.16, 2.24, and 2.25. */ private readonly isHttps; /** * Construct a fresh builder. * * @param opts.isHttps - Whether the request URL used `https://`. * Controls whether `timing.tlsMs` and `tls` are populated. * @param opts.finalHostname - Optional initial hostname; the transport * will overwrite this via {@link setHopContext} as redirects are * followed. */ constructor(opts: { isHttps: boolean; finalHostname?: string; }); /** * Record the hostname of the hop the transport is about to issue. * * Called once per hop, before {@link markDnsResolved}. The most-recent * value becomes {@link CapturedFields.finalHostname}. */ setHopContext(hostname: string): void; /** * Record the DNS-resolution outcome for the current hop. * * The most-recent values overwrite any earlier ones so the values * surfaced in {@link TimingInfo.dnsMs} and * {@link CapturedFields.resolvedIp} correspond to the final hop. */ markDnsResolved(ms: number, ip: string): void; /** * Record the TCP-connect time for the current hop. The most-recent * value wins (see class-level docstring for per-hop semantics). */ markTcpConnected(ms: number): void; /** * Record the TLS handshake time and extract {@link TlsInfo} from the * final-hop `tls.TLSSocket`. * * - `protocol` ← `socket.getProtocol()` (`""` if null). * - `cipher` ← `socket.getCipher().name` (`""` if missing). * - `subjectCN/issuerCN` ← `cert.subject.CN` / `cert.issuer.CN`. * - `subjectAltNames` ← parsed from `cert.subjectaltname`. * - `notBefore/notAfter` ← `cert.valid_from` / `cert.valid_to` parsed * to ISO 8601 (falls back to the raw value). * - `fingerprintSha256` ← lowercase, colon-separated SHA-256 of * `cert.raw` via `node:crypto`. * * Calling this method on an http:// fetch is harmless but pointless — * the constructor's `isHttps=false` flag suppresses the field in * {@link finalize} regardless. */ markTlsHandshaked(ms: number, socket: TLSSocket): void; /** * Record the final-hop response: HTTP status, raw headers * (lowercased and joined into a {@link HeaderMap}), and TTFB. * * `Set-Cookie` is preserved in `headers` joined with `, ` like every * other repeated header so the audit/redact passes can act on it. * Per-cookie capture happens via {@link addSetCookieHeader}, which * `fetch-core.ts` calls once per `Set-Cookie` line observed. */ markResponse(status: number, rawHeaders: IncomingHttpHeaders, ttfbMs: number): void; /** * Append a redirect hop to the chronological chain. * * Hops in excess of {@link MAX_REDIRECT_HOPS} are silently dropped so * the array always satisfies the cap from Requirement 2.26. * `fetch-core.ts` is responsible for surfacing the * `redirect-limit` error when the cap is reached; this builder just * stops accumulating. */ addRedirectHop(url: string, status: number, location?: string): void; /** * Parse one `Set-Cookie` header value via {@link parseSetCookie} and * append the resulting {@link CookieInfo}, bounded at * {@link MAX_COOKIES_CAPTURED}. * * Cookies in excess of the cap are silently dropped, matching * Requirement 2.31. */ addSetCookieHeader(value: string): void; /** * Produce the {@link CapturedFields} snapshot. * * @param totalMs - Wall-clock duration of the whole invocation, in * milliseconds. Stored in {@link TimingInfo.totalMs}. * * Optional fields obey the design's "include flags applied at the * adapter, not at the builder" principle: this method always emits * every field it observed, including `tls` (when `isHttps=true` and a * handshake was captured). The fetch handler in `fetch.ts` is * responsible for honouring `includeTls` / `includeTiming` / * `includeRedirectChain` by stripping fields from the assembled * {@link WebFetchMetadata} *after* this snapshot is produced. */ finalize(totalMs: number): CapturedFields; }