Reference#
The public surface of @kontourai/forage 1.x. The package has three entry
points, defined by exports in package.json, and no CLI.
| Import | Purpose |
|---|---|
@kontourai/forage |
crawl(), snapshot stores, text decoding, and the pinned browser renderer. |
@kontourai/forage/egress |
The SSRF and DNS-rebinding guard for single requests. |
@kontourai/forage/fetch |
fetchSource() and the provenance reference helpers. |
@kontourai/forage#
crawl()#
crawl(seed: Seed, policy?: CrawlPolicy): Promise<CrawlManifest>
Throws only InvalidCrawlConfigError (a TypeError). Everything else is
reported in CrawlManifest.warnings.
Seed#
| Field | Type | Meaning |
|---|---|---|
url |
string |
Required. An absolute http: or https: URL. Its fragment is dropped. |
render |
boolean |
Render the seed page (depth 0) in a browser. Other pages follow policy.render. |
headers |
Record<string, string> |
Extra request headers. Sent only to URLs on the seed's origin. |
userAgent |
string |
The User-Agent for plain requests and robots.txt matching. The browser renderer always sends forage's own user agent and does not use userAgent or headers. The default is kontourai-forage-bot/0.x (...) with a placeholder contact, so set your own. |
CrawlPolicy#
| Field | Default | Meaning |
|---|---|---|
maxPages |
20 |
Maximum acquisition attempts, failed ones included. Capped at 500 and rounded down. |
maxDepth |
2 |
Maximum link depth; the seed is depth 0. Capped at 10 and rounded down. |
sameHost |
true |
Queue only links on the seed's origin. Sitemap page URLs are always limited to the seed's host, whatever this is set to. |
discovery |
"links" |
"links" follows <a href>, "sitemap" reads sitemaps and follows no links, "both" does both. |
render |
"never" |
"on-shell" renders pages that look like a JavaScript shell; "always" renders every page. |
politeness.delayMs |
1000 |
Minimum time between requests to one origin. |
politeness.concurrency |
1 |
Accepted, but the frontier is sequential. Any other value adds a warning. Must be at least 1. |
robots |
true |
Honor robots.txt. |
egress |
{ guarded: true } |
See EgressPolicy. |
mode |
"live" |
"replay" serves every page from store with no network. Requires store. |
store |
none | A SnapshotStore. In live mode every acquired page is put into it, and so are the robots.txt and sitemap documents that sitemap discovery reads. |
shouldFollow |
none | Accepted, but not used yet. Setting it adds a warning. |
Numbers must be finite and non-negative. Sitemap discovery runs only when
maxPages is above 1 and maxDepth is at least 1, and accepts at most
maxPages page URLs. crawl() has no response-size option.
CrawlManifest and Page#
CrawlManifest has seed (the normalized seed URL), pages, truncated
(true when URLs were still queued at the maxPages cap), warnings, and,
when discovery is "sitemap" or "both", sitemap: { documentsRead, urlsDiscovered }
(zeros when discovery was skipped).
Each Page has url (after redirects), status, body, snapshot,
sourceRef, depth, rendered, and its own warnings.
EgressPolicy#
| Field | Meaning |
|---|---|
guarded |
true routes every plain HTTP request through the guard. false uses globalThis.fetch with no SSRF protection for those requests. Browser rendering is always guarded. |
testOnlyAllowedLoopbackOrigins |
Exact http://127.0.0.1:<port> or http://[::1]:<port> origins the guard may reach, for local test servers. |
Snapshot stores#
createInMemorySnapshotStore(): ExactSnapshotStore & PrunableSnapshotStore
createFilesystemSnapshotStore(options: {
root: string;
maxHistoryFiles?: number; // 1 to 10,000; default 10,000
}): VerifiedHeadSnapshotStore & PrunableSnapshotStore
The SnapshotStore interface:
| Method | Meaning |
|---|---|
put(snapshot) |
Store one snapshot. |
latest(sourceId) |
The newest snapshot of a source, or undefined. |
get(sourceId, bodyHash) |
The newest snapshot whose body hash equals, or starts with, bodyHash (both built-in stores accept a prefix), or undefined. For an exact citation, use findExact or resolveSnapshotSourceRef. |
list(sourceId) |
Every snapshot of a source. |
findExact(lookup) |
Optional. Full-identity lookup: { kind: "found", snapshot }, { kind: "missing" }, or { kind: "mismatch" }. |
prune(sourceId, { keepLast, keep? }) |
Optional. Remove all but the newest keepLast snapshots and those matching a keep reference. The head is always kept. Returns { removed, retained }. |
Filesystem store limits and behaviour:
- At most
maxHistoryFilesrecords per source. At the cap,put()rejects withSnapshotHistoryFullError(code: "history-full") until youprune().maxHistoryFilescannot change after a source directory is initialized. - Each record is at most 96 MiB, and a durable body at most 64 MiB.
- Reads are fail-closed. A malformed, mismatched, or foreign record makes
list,get, andlatestreject withSnapshotStoreReadError, whosecodeis"snapshot-corrupt", or"snapshot-store-error"for storage and race failures.reasongives the detail, such as"malformed-record"or"store-busy". put()andprune()take a per-source lock file,source.lock. A writer waits up to 90 seconds for it, then fails with reasonstore-busy. Every process writing to one store must run a forage version that takes this lock.
isSnapshotStoreReadError() and isSnapshotHistoryFullError() are type
guards for the two error classes.
Verified source-head witnesses#
Only the filesystem store has these two methods:
readVerifiedHead(sourceId, limits?): Promise<ReadVerifiedHeadResult>
compareHeadWitness(witness, limits?): Promise<HeadWitnessComparisonResult>
readVerifiedHead fingerprints the source's record and index files,
authenticates the latest record, and fingerprints them again. On success it
returns { kind: "found", headSnapshotRef, witness }, where witness is a
forage.source-head-witness/v1 object with an opaque token. Otherwise it
returns missing, unavailable, corrupt, or unsupported.
compareHeadWitness reads only metadata and returns matches, changed,
missing, unavailable, corrupt, or unsupported. A changed result does
not name the new head.
limits can only lower the defaults: maxEntries (the store's
maxHistoryFiles), maxIndexBytes (8 MiB), and maxVerifiedBodyBytes
(96 MiB). The token is local to the physical store, so a copy or restore can
invalidate it.
Text bodies#
| Export | Meaning |
|---|---|
parseDeclaredCharset(contentType) |
The lower-cased charset parameter, or null, with warnings for a malformed value. |
decodeTextBody(bytes, charset) |
{ text, encoding, warnings }. An unknown charset falls back to UTF-8; invalid bytes become U+FFFD; a matching byte-order mark is dropped from text. |
snapshotHashBasis(snapshot) |
"bytes" or "decoded-utf8": what bodyHash was computed over. |
Rendering#
crawl() and fetchSource() load the renderer on their own when rendering is
requested. These exports are for callers that drive a browser themselves:
| Export | Meaning |
|---|---|
createForageRenderImpl(options?) |
A RenderImpl that launches a dedicated headless Chromium per call through the optional playwright peer. Options: timeoutMs (default 30,000), resolver, testOnlyAllowedLoopbackOrigins. |
preparePinnedBrowserNavigation(url, resolver?, origins?) |
Resolve a URL through the guard and return the pinned IP and a Chromium --host-resolver-rules value. |
installGuardedPageNetwork(page, pinned, origins?) |
Install a route handler that aborts every request to another hostname, and every https: to http: redirect. |
@kontourai/forage/egress#
createGuardedFetch(options?: {
resolver?: EgressResolver;
testOnlyAllowedLoopbackOrigins?: readonly string[];
maxRedirects?: number; // default 5
}): typeof fetch
A fetch-shaped function. For each request and each redirect hop it validates
the URL, resolves the hostname once, requires every answer to be a public
address, and connects to the first validated IP while keeping the hostname for
Host and TLS SNI. With the default redirect: "follow" it follows redirects
itself and refuses a redirect to another origin or from https: to http:.
With redirect: "manual" it returns the redirect response.
evaluateEgressUrl(url, deps?) runs the same checks without a request and
returns { url, addresses }. classifyAddress(address, safeHost?) checks one
IP literal and returns { address, family }. All three throw
EgressUrlPolicyError with one of these code values:
| Code | Cause |
|---|---|
INVALID_URL |
The URL does not parse. |
INVALID_SCHEME |
Not http: or https:. |
CREDENTIALS |
The URL has a user name or password. |
INVALID_HOST |
A non-canonical numeric host such as 0x7f.1, or an address that does not parse. |
DENIED_HOST |
localhost, *.localhost, or a metadata hostname such as metadata.google.internal. |
INVALID_PORT |
A port other than 80 for http: or 443 for https:, unless the origin is in testOnlyAllowedLoopbackOrigins. |
DNS_FAILURE |
The lookup failed or returned no answers. |
DENIED_ADDRESS |
An answer is private, loopback, link-local, metadata, carrier-grade NAT, multicast, documentation, reserved, or an IPv6 form embedding such an IPv4 address. |
REDIRECT_INVALID, REDIRECT_LIMIT, REDIRECT_LOOP |
A bad Location, more than maxRedirects hops, or a revisited URL. |
REDIRECT_CROSS_HOST, REDIRECT_DOWNGRADE |
A redirect to another origin, or from https: to http:. |
CONNECT_FAILED |
The connection to the validated address failed. |
@kontourai/forage/fetch#
fetchSource()#
fetchSource(config: SourceConfig, options?: FetchSourceOptions): Promise<FetchResult>
Returns { snapshot?, error?, warnings? } and never throws for an operational
failure. It reads options.store but never writes to it.
SourceConfig#
| Field | Default | Meaning |
|---|---|---|
id |
required | The sourceId of the snapshot, and the key for revalidation. |
url |
required | An http: or https: URL. |
egress |
required | An EgressPolicy. |
minDelayMs |
1000 |
Minimum time between requests to one origin. |
timeoutMs |
15000 |
Per-request timeout. The renderer gets the same value per navigation attempt, not its own 30 s default; a render that falls back from networkidle to domcontentloaded can take up to twice this. |
retries |
2 |
Retries for timeouts, network errors, 429, and 5xx. Capped at 5. |
headers |
none | Extra request headers. |
userAgent |
forage default | The User-Agent header. |
respectRobots |
true |
Check robots.txt. Unreachable, 429, and 5xx robots.txt responses are treated as allow-all, with a warning. Rules match by plain path prefix; * and $ wildcards are not supported. |
render |
false |
true always renders; "on-shell" renders a JavaScript shell. |
revalidate |
true |
Send If-None-Match and If-Modified-Since from the source's latest stored snapshot (read from options.store), when its URL is the one being requested. Set false when you need a body every time. |
FetchSourceOptions#
| Field | Meaning |
|---|---|
fetch |
A FetchLike to use instead of the guard. |
store |
A SnapshotStore read for revalidation. |
maxResponseBytes |
A positive integer ceiling on the body kept. A larger declared Content-Length fails early. Otherwise the body is checked as it is read, but the guard downloads the whole response before returning it, so with the guard this limits what is kept, not what is downloaded. Rendered HTML over the limit falls back to the plain snapshot with a warning. |
renderImpl |
A RenderImpl to use instead of loading playwright. |
now, clock, sleep, random, schedule |
Injectable time and randomness, for tests. |
politenessState, robotsCache |
Maps that share politeness timing and robots.txt rules across calls. They default to process-wide maps. |
FetchErrorKind#
| Kind | Cause |
|---|---|
invalid-config |
Missing id, or an invalid maxResponseBytes. |
invalid-url |
The URL is invalid or not HTTP, or a redirect Location does not parse. |
robots-denied |
robots.txt disallows the path for your user agent's product token. |
timeout |
The request did not finish within timeoutMs, after retries. |
network |
A transport failure, or a redirect to another host or from https: to http:. |
egress-denied |
The guard refused the target. The message carries the EgressPolicyErrorCode. |
http-error |
A non-2xx status (with status set), a redirect without Location, or an unexpected 304. |
response-too-large |
The body exceeded maxResponseBytes. |
too-many-redirects |
More than 5 redirects. |
no-snapshot |
Replay found no stored snapshot for the source. |
snapshot-corrupt, snapshot-store-error |
Replay could not read a trustworthy snapshot. |
Provenance references#
| Export | Meaning |
|---|---|
buildSnapshotSourceRef(snapshot) |
The forage-snapshot: reference for a snapshot. Throws if it would exceed 16 KiB. |
parseSnapshotSourceRef(ref) |
{ sourceId, url, bodyHash, fetchedAt, snapshotDigest? }, or undefined for anything that is not a reference. |
resolveSnapshotSourceRef(store, ref) |
Look the reference up with store.findExact and verify it. |
resolveSnapshotSourceRef returns { ok: true, reference, snapshot, integrity },
where integrity is "snapshot-envelope" for current references and
"body-and-identity" for references from the 0.3 grammar (no
snapshotSha256). On failure it returns { ok: false, error } with
error.kind one of invalid-reference, snapshot-not-found,
snapshot-mismatch, snapshot-corrupt, or snapshot-store-error. A store
without findExact gives snapshot-store-error.
The subpath also re-exports decodeTextBody, parseDeclaredCharset,
snapshotHashBasis, the snapshot and store types, and the store error classes.
Fixed limits#
| Limit | Value |
|---|---|
| Pages per crawl | 500 |
| Crawl depth | 10 |
| Redirects per fetch | 5 |
| Retries per request | 5 |
| robots.txt size | 256 KiB |
| Sitemap documents per crawl | 50 |
| Sitemap index nesting | 3 levels |
| Sitemap size, compressed or not | 5 MiB |
| Reference length | 16 KiB (1 MiB for 0.3 references) |
| Durable body | 64 MiB |
| Filesystem record | 96 MiB |
| Records per source | 10,000 |