import type { IncomingMessage, ServerResponse } from 'node:http'; import { PayloadTooLargeError } from '@origintrail-official/dkg-core'; import type { DKGAgent, ContextGraphWritePreflightProbe } from '@origintrail-official/dkg-agent'; import type { DkgConfig } from '../config.js'; import type { CorsAllowlist } from './state.js'; export declare function isPayloadTooLargeError(err: unknown): err is PayloadTooLargeError; export declare function payloadTooLargeResponseBody(err: unknown): Record; /** * True iff `err` is (or looks like) the funded-wallet-selection failure * (`InsufficientPublisherFundsError`, code `NO_FUNDED_PUBLISHER_WALLET`) — * code-first, with a message-marker fallback for a re-wrap that dropped `.code`. * Code + marker are the shared dkg-core contract so the daemon, publisher * classifier, chain, and node-ui cannot drift. Shared by the `/vm/publish` route * catch and the top-level daemon handler. */ export declare function isNoFundedPublisherWalletLike(err: unknown): boolean; /** The HTTP-400 response body for a no-funded-wallet publish failure: the * structured `code` plus the actionable message (which lists per-wallet * balances). Single source of truth for both publish routes. */ export declare function noFundedPublisherWalletBody(message: string): { code: string; error: string; }; /** * Map a thrown request error to the daemon's top-level HTTP response — the * single neutral place that rethrowing lifecycle publish routes * and the lifecycle catch agree on status codes: 413 payload-too-large; 400 for * SyntaxError / reserved-namespace / NO_FUNDED_PUBLISHER_WALLET; otherwise a 500 * with the EVM-decoded message. Unit-testable in isolation. */ export declare function respondWithDaemonError(res: ServerResponse, err: any): void; export declare function oversizedRdfLiteralResponseBody(err: unknown): Record; export declare function resolveNameToPeerId(agent: DKGAgent, nameOrId: string): Promise; /** * GH #306 / #787 — shape guard for the WRITE routes (wm/write, * shared-memory/write). The `graph` term is OPTIONAL here: those routes * legitimately accept `{subject,predicate,object}` * and fill the graph internally. Without this guard, a string-shaped quad * (e.g. an N-Quad line `"

."`) slips past a bare `Array.isArray` * check and crashes the agent write path with a TypeError → HTTP 500 instead * of an actionable 4xx. */ export declare function isWritableQuad(value: unknown): boolean; export declare function validateWritableQuadLiteralSizes(label: string, quads: Array<{ subject: string; predicate: string; object: string; graph?: string; }>): { ok: true; } | { ok: false; body: Record; }; /** * GH #306 / #787 (follow-up) — validate each quad's `object` term is either a * quoted RDF literal (`"…"`) or an absolute IRI. Shared by lifecycle write * routes and other quad-accepting validation paths: the shape guard * ({@link isWritableQuad}) only checks that fields * are strings, so an object that is neither a literal nor an IRI (e.g. a bare * word `hello` or a number `123`) slips past them and crashes the RDF parser * with an uncaught "No scheme found in an absolute IRI" → HTTP 500 instead of an * actionable 400. */ export declare function validateQuadObjectTerms(label: string, quads: ReadonlyArray<{ object: string; }>): string | null; /** * KA-number-floor reconcile resilience (follow-up to the "KA create 500-on-429" * fix). If `e` is a **transient** reconcile failure — the chain RPC couldn't serve * the one-time-per-author floor read (e.g. a 429 after the bounded retry in * `allocator.ts` is exhausted) — send a retryable **503** and return true; * otherwise return false so the caller falls through to its normal mapping. * * The transient-vs-deterministic verdict comes from `retryable` (derived from * `isTransientChainError`): the typed `KaFloorReconcileError` carries it, and the * finalize/selection re-wrap sites in `dkg-agent-publish.ts` tag the same marker. * A deterministic failure (`retryable === false`, e.g. a revert) is NOT a 503 — * advertising a retry would be pointless — so it falls through. The legacy * message-text match is honored ONLY when the error explicitly marks itself * retryable, so a bare re-wrapped message can never force a deterministic error * into a retryable 503 (PR #1319 review). Used by every route that can trigger the * reconcile (named create, one-shot publish, shared-memory publish, and the * WM-verb routes via `respondAssertionError`) so they answer consistently. */ export declare function respondIfReconcileUnavailable(res: ServerResponse, e: any): boolean; /** * Strip http(s) URLs from a chain error message before it is returned in an * HTTP response body. The adapter's multi-provider `RPC_ENDPOINTS_EXHAUSTED` * message embeds `this.rpcUrls.join(', ')`, and with default-backup inheritance * an operator-set private `chain.rpcUrl` may carry an API key — so a response * body must never echo raw RPC URLs (the failover logger is already host-only). */ export declare function sanitizeRpcMessage(msg: string): string; /** * Maps a TRANSPORT-level chain RPC failure to a RETRYABLE HTTP status, * keyed STRICTLY on `err.code` (never message text): * - `RPC_ENDPOINTS_EXHAUSTED` → 503 (all configured endpoints failed over) * - `RPC_RECEIPT_LOOKUP_FAILED` → 503 (receipt lookup failed on every endpoint) * - `TIMEOUT` → 504 (receipt wait / RPC request timed out) * * Returns `undefined` for anything else. On-chain reverts (`CALL_EXCEPTION`), * `INSUFFICIENT_FUNDS`, and application errors carry NO `RPC_*`/`TIMEOUT` * transport code (the chain adapter only stamps these on the multi-RPC * failover loops), so they fall through to each route's own mapping — which * preserves the #988 contract that a genuine publish/on-chain failure stays * 5xx/4xx and is NEVER down-classified by message text. * * Shared chokepoint for `/api/context-graph/register`, the `/vm/publish` * catch, `respondAssertionError` (WM-verb writes), the SWM→VM publish * auto-register leg, and the top-level daemon catch, so EVERY chain-write * surface answers a transient RPC outage with the SAME retryable status * instead of a generic 500 (or, in the auto-register leg, a misleading 400). * Mirrors the failover engine's own multi-RPC awareness at the HTTP boundary. */ export declare function classifyChainRpcTransportStatus(err: unknown): { status: number; body: Record; } | undefined; /** * Single responder for a transient chain-RPC transport failure: maps it to a * retryable 503/504 (via {@link classifyChainRpcTransportStatus}), writes the * response, and returns true. Returns false (writing nothing) for any * non-transport error so the caller falls through to its own mapping. * `extraBody` adds route-specific fields to the response body (e.g. the identity * route's `{ identityId, hasIdentity }`). The canonical transport fields * (`error`, `code`, `txHash`) are merged LAST so a caller can never shadow them * — `extraBody` may only ADD fields, keeping this responder the single source of * truth for the transport response shape. Use this instead of repeating the * classify→jsonResponse branch in every chain-write catch. */ export declare function respondIfChainRpcTransportError(res: ServerResponse, err: unknown, extraBody?: Record): boolean; export declare function jsonResponse(res: ServerResponse, status: number, data: unknown, corsOrigin?: string | null, extraHeaders?: Record): void; export declare function safeDecodeURIComponent(encoded: string, res: ServerResponse): string | null; export declare function safeParseJson(body: string, res: ServerResponse): Record | null; export declare function validateOptionalSubGraphName(subGraphName: unknown, res: ServerResponse): boolean; export declare function validateRequiredContextGraphId(contextGraphId: unknown, res: ServerResponse): boolean; export declare function normalizeContextGraphIdOrUri(contextGraphId: string): string; type ExistingContextGraphRow = { id?: unknown; uri?: unknown; creator?: unknown; curator?: unknown; accessPolicy?: unknown; onChainId?: unknown; isSystem?: unknown; subscribed?: unknown; synced?: unknown; }; /** Bound for the last-resort on-chain rescue eth_call, so an RPC stack that * hangs on connect cannot stall a write route that is already degraded. * Exported so tests can drive the timeout branch without waiting the full * production window; production callers never pass an override so the * default behaviour is unchanged. */ export declare const WRITE_PREFLIGHT_CHAIN_RESCUE_TIMEOUT_MS = 5000; /** * Resolve a write target to a known, canonical context graph id. * * The storage layer auto-materializes named graphs on first insert, so syntax * validation is not enough for mutation routes. This helper fail-closes before * callers reach agent/publisher/storage code that would create a shadow CG. */ export declare function resolveRequiredWriteContextGraphId(agent: { listContextGraphs(opts?: { callerAgentAddress?: string | null; }): Promise; contextGraphHasLocalContent?: (contextGraphId: string) => Promise; contextGraphExists?: (contextGraphId: string) => Promise; probeContextGraphWritePreflight?: (contextGraphId: string, opts?: { callerAgentAddress?: string | null; }) => Promise; /** * High-level store-outage rescue decision (owns the on-chain * active+public policy semantics). The daemon calls ONLY this method for * the both-legs-failed rescue — it never assembles chain-policy meaning * locally. */ validateWriteTargetDuringStoreOutage?: (contextGraphId: string) => Promise; }, contextGraphId: unknown, res: ServerResponse, opts?: { callerAgentAddress?: string | null; requireLocalWritable?: boolean; /** * Trusted local / node-admin routes may accept an exact, store-backed * private graph without first enumerating all visible context graphs. * Scoped agent routes must leave this false so their per-caller private * authorization remains authoritative. */ allowLocalExactFallback?: boolean; /** * Test-only seam: override the store-outage rescue eth_call timeout so the * timeout branch can be exercised without waiting the full production * window. Production callers never pass this, so the default is unchanged. */ chainRescueTimeoutMs?: number; }): Promise; export declare function validateEntities(entities: unknown, res: ServerResponse): boolean; export declare function validateConditions(conditions: unknown, res: ServerResponse): boolean; export declare const MAX_BODY_BYTES: number; export declare const SMALL_BODY_BYTES: number; export declare const MAX_UPLOAD_BYTES: number; /** * In-memory extraction job tracking record. Populated at import-file time * and queried by the extraction-status endpoint. Records are kept in a * bounded, TTL-pruned map keyed by the target assertion URI (which is * unique per agent × contextGraph × assertionName × subGraphName). */ export interface ImportFileExtractionPayload { status: "completed" | "skipped" | "failed"; tripleCount: number; pipelineUsed: string | null; mdIntermediateHash?: string; error?: string; code?: string; limitBytes?: number; actualBytes?: number; subject?: string; predicate?: string; graph?: string; skipReason?: string; } export declare function buildImportFileResponse(args: { assertionUri: string; fileHash: string; rootEntity?: string; detectedContentType: string; extraction: ImportFileExtractionPayload; }): { detectedContentType: string; extraction: { skipReason?: string | undefined; graph?: string | undefined; predicate?: string | undefined; subject?: string | undefined; actualBytes?: number | undefined; limitBytes?: number | undefined; code?: string | undefined; error?: string | undefined; mdIntermediateHash?: string | undefined; status: "failed" | "skipped" | "completed"; tripleCount: number; pipelineUsed: string | null; }; rootEntity?: string | undefined; assertionUri: string; fileHash: string; }; export declare function unregisteredSubGraphError(contextGraphId: string, subGraphName: string): string; export declare function readBody(req: IncomingMessage, maxBytes?: number): Promise; /** * Buffer variant of `readBody` that returns raw bytes. Use for binary payloads * like multipart/form-data uploads where `.toString()` would corrupt content. */ export declare function readBodyBuffer(req: IncomingMessage, maxBytes?: number): Promise; export declare function buildCorsAllowlist(config: DkgConfig, boundPort: number): CorsAllowlist; export declare function resolveCorsOrigin(req: IncomingMessage, allowlist: CorsAllowlist): string | undefined; export declare function corsHeaders(origin?: string | null): Record; export declare class HttpRateLimiter { private _max; private _exempt; private _hits; private _timer; constructor(requestsPerMinute: number, exemptPaths?: string[]); isAllowed(ip: string, pathname: string): boolean; destroy(): void; } /** * Read-only view of {@link InFlightLimiter}'s admission-control counters. This * is what `/api/status` (and the plugin-facing `RequestContext`) consume, so * route/plugin code can read inFlight/max/rejectedTotal without gaining access * to the mutating `tryAcquire()`/`release()` and corrupting slot accounting. */ export interface AdmissionStatsView { /** Requests currently holding a slot. */ readonly inFlight: number; /** Effective concurrency cap; 0 disables the limiter (always admits). */ readonly max: number; /** Monotonic count of requests shed (503) since boot. */ readonly rejectedTotal: number; } /** * Bounds the number of HTTP requests being processed concurrently by the * daemon, independent of client IP. This is admission control, not rate * limiting: the single-process daemon funnels every request onto one event * loop (and, on the embedded store backend, one Oxigraph worker thread), so a * burst of concurrent in-flight requests — including local/loopback traffic * that bypasses {@link HttpRateLimiter} — can pile pending work onto the heap * and stall the node. When the cap is reached, callers should shed load with a * 503 + Retry-After rather than queue unboundedly. * * `tryAcquire()` must be paired with exactly one `release()` in a `finally`. */ export declare class InFlightLimiter implements AdmissionStatsView { private _inFlight; private _rejectedTotal; private readonly _max; constructor(max: number); get inFlight(): number; get max(): number; /** Monotonic count of requests shed (tryAcquire returned false) — for metrics/logging. */ get rejectedTotal(): number; /** Returns true and reserves a slot, or false if at capacity (shed load). */ tryAcquire(): boolean; release(): void; } /** * Resolve an integer setting from an env var (highest precedence), then a * config value, then a fallback. Malformed / empty / NaN inputs are IGNORED * (fall through) rather than silently disabling the setting — a typo like * `DKG_MAX_INFLIGHT=abc` or an empty string yields the documented default, not * `NaN`/`0`. * * Pass `allowNonPositive` when `<= 0` is a meaningful value (e.g. "disable the * cap"): then ANY integer is accepted, so `0` or a negative flows through to * disable rather than falling back to the default. Without it the minimum * accepted value is 1. (Named for what it does — it admits negatives too, not * just zero.) */ export declare function resolveIntSetting(envValue: string | undefined, configValue: number | undefined, fallback: number, opts?: { allowNonPositive?: boolean; }): number; /** Minimal shape of the bits of `http.Server` that {@link applyServerLimits} sets. */ export interface ServerLimitsTarget { maxConnections: number; headersTimeout: number; } /** * Resolve and APPLY the socket-level limits to an HTTP server: `maxConnections` * (cap simultaneous sockets) and `headersTimeout` (kill slow-header * connections). `requestTimeout` is intentionally left at the Node default so * legitimately long publishes / SPARQL queries aren't truncated. Extracted from * the daemon so the resolution precedence AND the assignment are unit-testable. */ export declare function applyServerLimits(server: ServerLimitsTarget, opts: { maxConnectionsEnv?: string; maxConnectionsConfig?: number; headersTimeoutEnv?: string; }): void; /** * Whether a request bypasses admission control. METHOD-AWARE on purpose: only * `OPTIONS` (CORS preflight, any path) and safe `GET`/`HEAD` reads of the cheap * liveness/doc/SSE paths bypass. A `POST`/`PUT`/etc. to those same paths is NOT * exempt — it falls through to the router/route-plugins and so must still take * a slot, otherwise a buggy/authenticated local client could run real work * outside the cap via e.g. `POST /api/status`. */ export declare function isAdmissionExempt(method: string | undefined, pathname: string): boolean; /** * Apply concurrency admission control to one request. Exempt requests (see * {@link isAdmissionExempt}) always pass and take no slot. Otherwise a slot is * reserved; on capacity it writes `503` + `Retry-After` (with CORS headers so * browsers surface it) and returns `{ admitted: false }`. * * Ownership model: the slot is released automatically when the RESPONSE * completes (`res` `close`), NOT when the request handler returns — route * plugins and SSE can return with the response still streaming, and releasing * on handler return would free the slot mid-stream and let that work run * outside the cap. The release is registered here so callers cannot get it * wrong; they only need to honor `admitted`. */ export declare function admitRequest(limiter: InFlightLimiter, method: string | undefined, pathname: string, res: ServerResponse, corsOrigin: string | null): { admitted: boolean; }; export declare function isLoopbackClientIp(ip: string): boolean; export declare function isLoopbackRateLimitExemptPath(pathname: string): boolean; export declare function shouldBypassRateLimitForLoopbackTraffic(ip: string, pathname: string): boolean; export declare function isValidContextGraphId(id: string): boolean; /** * CLI-9 ( * scrub raw chain-revert payloads from error messages before they * reach the HTTP body. Providers (ethers, viem, hardhat) serialise * the same revert data under multiple keys: `data="0x…"`, `data=0x…`, * `errorData="0x…"`, `errorData=0x…`, and JSON `"data":"0x…"`. The * matching set here mirrors `enrichEvmError()` in * `packages/chain/src/evm-adapter.ts` so any selector that survived * decoding still gets redacted before reaching the operator. Note * that we redact AFTER `enrichEvmError` has had a chance to splice * the decoded custom-error name in — so the operator still sees the * human-readable error, just without the raw selector blob. */ export declare function sanitizeRevertMessage(raw: string): string; /** * CLI-7/9 helper: classify a thrown error as a "client mistake" (4xx) * vs an "infrastructure failure" (5xx). The vocabulary is conservative * — only well-known not-found / invalid-input / unreachable-peer * patterns map to 4xx; everything else stays 5xx so a real internal * problem still surfaces via the top-level catch. */ export declare function classifyClientError(msg: string): { status: 404; sanitized: string; } | { status: 403; sanitized: string; } | { status: 400; sanitized: string; } | { status: 504; sanitized: string; } | null; export declare function shortId(peerId: string): string; export declare function sleep(ms: number): Promise; export declare function deriveBlockExplorerUrl(chainId?: string): string | undefined; export {}; //# sourceMappingURL=http-utils.d.ts.map