polite-crawler 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +28 -121
  2. package/build/{browser-CKRNbIDT.mjs → browser-56dBBLec.mjs} +7 -3
  3. package/build/browser-56dBBLec.mjs.map +1 -0
  4. package/build/classify.d.mts +50 -2
  5. package/build/{cli-daemon-ZYRdy6EW.mjs → cli-daemon-qS4PH2bM.mjs} +2 -2
  6. package/build/{cli-daemon-ZYRdy6EW.mjs.map → cli-daemon-qS4PH2bM.mjs.map} +1 -1
  7. package/build/{cli-recipes-Bci6HREA.mjs → cli-recipes-qbLRVThF.mjs} +2 -2
  8. package/build/{cli-recipes-Bci6HREA.mjs.map → cli-recipes-qbLRVThF.mjs.map} +1 -1
  9. package/build/cli.mjs +41 -16
  10. package/build/cli.mjs.map +1 -1
  11. package/build/{daemon-DK6U51_v.mjs → daemon-BXBbUrni.mjs} +11 -7
  12. package/build/daemon-BXBbUrni.mjs.map +1 -0
  13. package/build/{diff-BUM91Rev.mjs → diff-CpKN0Jdc.mjs} +7 -3
  14. package/build/{diff-BUM91Rev.mjs.map → diff-CpKN0Jdc.mjs.map} +1 -1
  15. package/build/{extract-CefB5Vfn.mjs → extract-DqeqiNHb.mjs} +12 -4
  16. package/build/extract-DqeqiNHb.mjs.map +1 -0
  17. package/build/{html-BACljlkf.d.mts → html-DMgIeQfJ.d.mts} +16 -2
  18. package/build/html.d.mts +1 -2
  19. package/build/index.d.mts +4227 -40
  20. package/build/index.mjs +17 -17
  21. package/build/{log-Dx-wXjJl.mjs → log-CmBWcyQm.mjs} +5 -1
  22. package/build/log-CmBWcyQm.mjs.map +1 -0
  23. package/build/mcp-package-Y0bREnOS.mjs +40 -0
  24. package/build/mcp-package-Y0bREnOS.mjs.map +1 -0
  25. package/build/{ollama-BJxkjny4.mjs → ollama-D1xRh3hY.mjs} +7 -3
  26. package/build/ollama-D1xRh3hY.mjs.map +1 -0
  27. package/build/{open-CbpSCEZc.mjs → open-CEo22Hpu.mjs} +78 -18
  28. package/build/open-CEo22Hpu.mjs.map +1 -0
  29. package/build/politeness.d.mts +366 -2
  30. package/build/{recipe-GiFQojmo.mjs → recipe-B2SkepDZ.mjs} +10 -4
  31. package/build/recipe-B2SkepDZ.mjs.map +1 -0
  32. package/build/{recipe-product-Dellqudu.mjs → recipe-product-CyZE3Ukd.mjs} +29 -7
  33. package/build/recipe-product-CyZE3Ukd.mjs.map +1 -0
  34. package/build/{render-C1CUraWF.mjs → render-CKE2LILH.mjs} +10 -3
  35. package/build/render-CKE2LILH.mjs.map +1 -0
  36. package/build/robots.d.mts +143 -2
  37. package/build/robots.mjs +1 -1
  38. package/build/robots.mjs.map +1 -1
  39. package/build/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-8H4AJuhK.mjs} +1 -0
  40. package/build/{runtime-D_r2SYge.mjs → runtime-C_2BQibH.mjs} +84 -18
  41. package/build/runtime-C_2BQibH.mjs.map +1 -0
  42. package/build/signals.d.mts +45 -2
  43. package/build/{transformers-BQMlaGj4.mjs → transformers-BNiuiLxT.mjs} +3 -3
  44. package/build/transformers-BNiuiLxT.mjs.map +1 -0
  45. package/build/{version-BPem-XZd.mjs → version-Djva7JkN.mjs} +7 -2
  46. package/build/version-Djva7JkN.mjs.map +1 -0
  47. package/build/{wacz-QyzLp4mC.mjs → wacz-DKk2HBmx.mjs} +4 -4
  48. package/build/{wacz-QyzLp4mC.mjs.map → wacz-DKk2HBmx.mjs.map} +1 -1
  49. package/build/{warc-BHcaRKHw.mjs → warc-C_ew76bV.mjs} +4 -4
  50. package/build/{warc-BHcaRKHw.mjs.map → warc-C_ew76bV.mjs.map} +1 -1
  51. package/build/{webbotauth-CjJdTAfE.mjs → webbotauth-Dq_dA_u3.mjs} +2 -2
  52. package/build/{webbotauth-CjJdTAfE.mjs.map → webbotauth-Dq_dA_u3.mjs.map} +1 -1
  53. package/package.json +5 -11
  54. package/build/browser-CKRNbIDT.mjs.map +0 -1
  55. package/build/classify-CrfyXcwo.d.mts +0 -51
  56. package/build/daemon-DK6U51_v.mjs.map +0 -1
  57. package/build/daemon-wwIWsUmU.d.mts +0 -3777
  58. package/build/extract-CefB5Vfn.mjs.map +0 -1
  59. package/build/log-Dx-wXjJl.mjs.map +0 -1
  60. package/build/loopback-C_QYowSf.mjs +0 -22
  61. package/build/loopback-C_QYowSf.mjs.map +0 -1
  62. package/build/mcp.d.mts +0 -199
  63. package/build/mcp.mjs +0 -1555
  64. package/build/mcp.mjs.map +0 -1
  65. package/build/ollama-BJxkjny4.mjs.map +0 -1
  66. package/build/open-CbpSCEZc.mjs.map +0 -1
  67. package/build/politeness-yI3xBEx9.d.mts +0 -367
  68. package/build/recipe-GiFQojmo.mjs.map +0 -1
  69. package/build/recipe-product-Dellqudu.mjs.map +0 -1
  70. package/build/render-C1CUraWF.mjs.map +0 -1
  71. package/build/robots-R-qaEBOb.d.mts +0 -144
  72. package/build/runtime-D_r2SYge.mjs.map +0 -1
  73. package/build/signals-BHpy2xb2.d.mts +0 -46
  74. package/build/transformers-BQMlaGj4.mjs.map +0 -1
  75. package/build/urls-BTDSgDLC.d.mts +0 -16
  76. package/build/version-BPem-XZd.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"robots.mjs","names":[],"sources":["../src/robots.ts"],"sourcesContent":["/**\n * This module reads robots.txt as RFC 9309 (September 2022) specifies it. It is for a crawler that\n * tells who it is and obeys what a site says. The important rules, each with a test:\n *\n * - A group is one or more `user-agent` lines and the rules after them. A `user-agent` line after\n * an `allow` or `disallow` line starts a new group, as the parser of Google does. A different\n * line (`crawl-delay`) between two `user-agent` lines does not split the group. A rule before\n * the first `user-agent` line is in no group.\n * - The product token matches with no attention to case. The matching groups merge. The group\n * `*` applies only when no group names the crawler.\n * - The longest matching rule wins, and `allow` wins a tie. A `*` matches any sequence, and a `$`\n * at the end anchors the end. The matcher uses no regular expression, and its time is linear.\n * - `/robots.txt` is always permitted, and an empty `disallow` matches nothing.\n * - The parser reads at least the first 500 KiB.\n * - A usage line (`content-signal`, `content-usage`) belongs to its group, with an optional path\n * before its value. A line before the first group applies to each crawler.\n *\n * The function `robotsFromStatus` gives the meaning of the status of the fetch. A 4xx is\n * \"unavailable\", so all paths are permitted. A 5xx or no response is \"unreachable\", so no path is\n * permitted. A 429 is unreachable too, as Google does it. A site that limits the rate of its robots\n * file does not ask for a crawl.\n */\n\nexport const ROBOTS_MAX_BYTES: number = 500 * 1024;\n\nexport interface RobotsRule {\n readonly allow: boolean;\n /** This value is the path pattern, normalized for comparison. */\n readonly pattern: string;\n /** This value is the number of the line in robots.txt, from 1. A rule that the code makes has none. */\n readonly line?: number | undefined;\n}\n\n/** A usage line states a preference about the use of the content (`content-signal`, `content-usage`). */\nexport interface RobotsSignal {\n /** This value is the name of the line, in lowercase. */\n readonly key: string;\n /** This text is the value of the line, after the path. */\n readonly value: string;\n /** This value is the normalized path pattern of the line. A line with no path applies to each path. */\n readonly path: string | undefined;\n /** This value is the number of the line in robots.txt, from 1. */\n readonly line: number;\n}\n\nexport interface RobotsGroup {\n /** These values are the user agents of the group, in lowercase. */\n readonly agents: readonly string[];\n /** This value is the number of the first `user-agent` line of the group, from 1. */\n readonly line?: number | undefined;\n readonly rules: readonly RobotsRule[];\n /** This value is the nonstandard `crawl-delay` in seconds, when the group sets one. */\n readonly crawlDelaySeconds: number | undefined;\n /** These usage lines are in the group. They apply when the group applies. */\n readonly signals?: readonly RobotsSignal[] | undefined;\n}\n\nexport interface Robots {\n readonly groups: readonly RobotsGroup[];\n readonly sitemaps: readonly string[];\n /** These lines state usage preferences (`content-signal: …`, `content-usage: …`), as written, from each group. */\n readonly signals: readonly string[];\n /** These usage lines come before the first group, so they apply to each crawler. */\n readonly fileSignals?: readonly RobotsSignal[] | undefined;\n /**\n * This text tells why the crawler could not read robots.txt: a dead domain, a broken certificate\n * or a server error. Then no path is permitted, and the note of a skip gives this reason. It does\n * not name a rule of the site.\n */\n readonly unreachable?: string | undefined;\n}\n\nexport const ALLOW_ALL: Robots = { groups: [], sitemaps: [], signals: [] };\n\nexport const DISALLOW_ALL: Robots = {\n groups: [\n { agents: [\"*\"], rules: [{ allow: false, pattern: \"/\" }], crawlDelaySeconds: undefined },\n ],\n sitemaps: [],\n signals: [],\n};\n\nconst UNRESERVED = /^[A-Za-z0-9\\-._~]$/;\nconst URL_ENCODED = new Set(\"\\\"'<>\\\\^`{|}\");\n/**\n * The URL parser encodes these ASCII characters in a path or a query: the controls, a space, DEL,\n * and `\"'<>\\^`{|}`. A rule can contain them as written.\n */\nconst encodedAscii = (char: string): boolean => {\n const code = char.charCodeAt(0);\n return code <= 0x20 || code === 0x7f || URL_ENCODED.has(char);\n};\nconst HEX = /^[0-9A-Fa-f]$/;\n\nconst escapeOf = (char: string): string => {\n const code = char.codePointAt(0)!;\n if (code <= 0x7f) return `%${code.toString(16).toUpperCase().padStart(2, \"0\")}`;\n try {\n return encodeURIComponent(char);\n } catch {\n // a lone surrogate: the URL parser writes the replacement character\n return \"%EF%BF%BD\";\n }\n};\n\n/**\n * This function normalizes a path, or the pattern of a rule, for comparison, as §2.2.2 asks. It\n * encodes the characters outside US-ASCII, and the ASCII characters that the URL parser encodes\n * (a space, `\"`, `<`, `{`). It decodes an escape of an unreserved character, so `/%7Efoo` and\n * `/~foo` are equal. The other escapes stay, with uppercase hexadecimal digits.\n */\nexport const normalizePath = (value: string): string => {\n let out = \"\";\n const chars = Array.from(value);\n for (let index = 0; index < chars.length; index += 1) {\n const char = chars[index]!;\n if (char === \"%\" && HEX.test(chars[index + 1] ?? \"\") && HEX.test(chars[index + 2] ?? \"\")) {\n const hex = `${chars[index + 1]!}${chars[index + 2]!}`;\n const decoded = String.fromCharCode(Number.parseInt(hex, 16));\n out += UNRESERVED.test(decoded) ? decoded : `%${hex.toUpperCase()}`;\n index += 2;\n continue;\n }\n out += char.charCodeAt(0) > 0x7f || encodedAscii(char) ? escapeOf(char) : char;\n }\n return out;\n};\n\n/** This function tells if a text is a product token: letters, `_` and `-` only, and one or more (RFC 9309, section 2.2.1). */\nexport const isProductToken = (value: string): boolean => /^[A-Za-z_-]+$/.test(value);\n\n/** This function gives the first product token of a user agent: `Googlebot/2.1` gives `googlebot`. */\nconst tokenOf = (value: string): string =>\n (/^[A-Za-z_-]+/.exec(value.trim())?.[0] ?? \"\").toLowerCase();\n\n/** These names are parts of the header of a browser, not the name of a crawler. */\nconst BROWSER_PARTS: ReadonlySet<string> = new Set([\n \"mozilla\",\n \"applewebkit\",\n \"khtml\",\n \"gecko\",\n \"chrome\",\n \"chromium\",\n \"safari\",\n \"version\",\n \"mobile\",\n \"firefox\",\n \"edg\",\n \"opr\",\n \"like\",\n]);\n\n/**\n * This function gives the product token that robots.txt rules match, in lowercase. `MyBot/1.0`\n * gives `mybot`. The full header `Mozilla/5.0 (compatible; MyBot/1.0; +https://…)` also gives\n * `mybot`. The token is the name after `compatible;`, or else the first product that is not a part\n * of a browser.\n */\nexport const productToken = (value: string): string => {\n const text = value.trim();\n const first = tokenOf(text);\n if (first !== \"mozilla\") return first;\n const compatible = /compatible;\\s*([A-Za-z_-]+)/i.exec(text)?.[1];\n if (compatible !== undefined) return compatible.toLowerCase();\n const products = [...text.matchAll(/([A-Za-z][A-Za-z_-]*)\\/[\\w.]+/g)].map((match) =>\n (match[1] ?? \"\").toLowerCase(),\n );\n return products.find((product) => !BROWSER_PARTS.has(product)) ?? first;\n};\n\nconst truncateUtf8 = (text: string, maxBytes: number): string => {\n const bytes = new TextEncoder().encode(text);\n if (bytes.length <= maxBytes) return text;\n // cut at the last line break inside the limit, so no rule is half-read\n const head = new TextDecoder().decode(bytes.slice(0, maxBytes));\n const lastBreak = Math.max(head.lastIndexOf(\"\\n\"), head.lastIndexOf(\"\\r\"));\n return lastBreak === -1 ? \"\" : head.slice(0, lastBreak);\n};\n\n/** This function reads a usage line: an optional path, then the value (`/images/ train-ai=n`). */\nconst signalOf = (key: string, value: string, line: number): RobotsSignal => {\n const path = /^(\\/\\S*)\\s+(.*)$/.exec(value);\n return path === null\n ? { key, value, path: undefined, line }\n : { key, value: path[2]!.trim(), path: normalizePath(path[1]!), line };\n};\n\nexport const parseRobots = (input: string): Robots => {\n const groups: Array<{\n agents: string[];\n line: number;\n rules: RobotsRule[];\n crawlDelaySeconds: number | undefined;\n signals: RobotsSignal[];\n }> = [];\n const sitemaps: string[] = [];\n const signals: string[] = [];\n const fileSignals: RobotsSignal[] = [];\n let current: (typeof groups)[number] | undefined;\n let collectingAgents = false;\n for (const [index, raw] of truncateUtf8(input, ROBOTS_MAX_BYTES)\n .split(/\\r\\n|\\r|\\n/)\n .entries()) {\n const line = raw.replace(/#.*$/, \"\").trim();\n const colon = line.indexOf(\":\");\n if (colon <= 0) continue;\n const key = line.slice(0, colon).trim().toLowerCase();\n const value = line.slice(colon + 1).trim();\n if (key === \"user-agent\") {\n if (current === undefined || !collectingAgents) {\n current = {\n agents: [],\n line: index + 1,\n rules: [],\n crawlDelaySeconds: undefined,\n signals: [],\n };\n groups.push(current);\n }\n current.agents.push(value.toLowerCase());\n collectingAgents = true;\n continue;\n }\n // records that belong to no group\n if (key === \"sitemap\") {\n if (value.length > 0) sitemaps.push(value);\n continue;\n }\n // an RSL `License` line names the license of the content: the crawler records it with the other usage lines\n if (key === \"content-signal\" || key === \"content-usage\" || key === \"license\") {\n signals.push(`${key}: ${value}`);\n (current?.signals ?? fileSignals).push(signalOf(key, value, index + 1));\n continue;\n }\n if (current === undefined) continue;\n if (key === \"allow\" || key === \"disallow\") {\n // only a rule ends the list of agents, so a crawl-delay between two agents keeps one group\n collectingAgents = false;\n if (value.length > 0)\n current.rules.push({\n allow: key === \"allow\",\n pattern: normalizePath(value),\n line: index + 1,\n });\n } else if (key === \"crawl-delay\") {\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds >= 0) current.crawlDelaySeconds = seconds;\n }\n }\n return { groups, sitemaps, signals, fileSignals };\n};\n\n/**\n * This function tells if a normalized path matches the pattern of a rule. A `*` matches any\n * sequence, and a `$` at the end anchors the end. The function finds the parts between the stars\n * in sequence, each at its first position. No pattern makes it slow: its time is linear in the\n * length of the path for each part, and it uses no regular expression.\n */\nexport const matchesPattern = (pattern: string, path: string): boolean => {\n const anchored = pattern.endsWith(\"$\");\n const parts = (anchored ? pattern.slice(0, -1) : pattern).split(\"*\");\n const first = parts[0]!;\n if (!path.startsWith(first)) return false;\n if (parts.length === 1) return !anchored || path.length === first.length;\n let at = first.length;\n const last = parts.length - 1;\n for (let index = 1; index < last; index += 1) {\n const part = parts[index]!;\n // two stars in sequence give an empty part, which matches at once\n if (part.length === 0) continue;\n const found = path.indexOf(part, at);\n if (found === -1) return false;\n at = found + part.length;\n }\n const tail = parts[last]!;\n if (!anchored) return tail.length === 0 || path.indexOf(tail, at) !== -1;\n return path.length - tail.length >= at && path.endsWith(tail);\n};\n\ninterface Chosen {\n readonly rules: readonly RobotsRule[];\n readonly groups: readonly RobotsGroup[];\n readonly crawlDelaySeconds: number | undefined;\n /** This value is true when a group names this token, and `*` did not apply. */\n readonly explicit: boolean;\n}\n\nconst groupsFor = (robots: Robots, token: string): Chosen => {\n const wanted = token === \"*\" ? \"*\" : productToken(token);\n const named = robots.groups.filter((group) =>\n group.agents.some((agent) => agent !== \"*\" && tokenOf(agent) === wanted),\n );\n const chosen =\n named.length > 0 ? named : robots.groups.filter((group) => group.agents.includes(\"*\"));\n const delays = chosen.flatMap((group) =>\n group.crawlDelaySeconds === undefined ? [] : [group.crawlDelaySeconds],\n );\n return {\n rules: chosen.flatMap((group) => group.rules),\n groups: chosen,\n crawlDelaySeconds: delays.length > 0 ? Math.max(...delays) : undefined,\n explicit: named.length > 0,\n };\n};\n\n/** This function gives the rule that decides for the path: the longest match, and `allow` in a tie. */\nconst decidingRule = (rules: readonly RobotsRule[], path: string): RobotsRule | undefined => {\n let best: RobotsRule | undefined;\n for (const rule of rules) {\n if (!matchesPattern(rule.pattern, path)) continue;\n if (\n best === undefined ||\n rule.pattern.length > best.pattern.length ||\n (rule.pattern.length === best.pattern.length && rule.allow && !best.allow)\n ) {\n best = rule;\n }\n }\n return best;\n};\n\nexport interface RobotsDecision {\n readonly allowed: boolean;\n /** This value is the product token whose rules refused the path. */\n readonly by: string | undefined;\n readonly crawlDelaySeconds: number | undefined;\n /** This rule decided. When no rule matches the path, the path is permitted and this value is empty. */\n readonly rule?: RobotsRule | undefined;\n /** These groups applied to the decision: the groups that name the token, or the group `*`. */\n readonly groups?: readonly RobotsGroup[] | undefined;\n}\n\n/**\n * This function tells if these tokens can fetch `pathAndQuery`. Each token becomes its product\n * token (`productToken`), so `MyBot/1.0` matches the group `mybot`. The first token is the token of\n * the crawler, and `*` applies when no group names it. A further token, for example `Claude-User`,\n * applies only when the site names it. A site that writes rules for an agent means them for the\n * fetches of that agent.\n */\nexport const decide = (\n robots: Robots,\n tokens: readonly string[],\n pathAndQuery: string,\n): RobotsDecision => {\n const path = normalizePath(pathAndQuery.length === 0 ? \"/\" : pathAndQuery);\n const primary = groupsFor(robots, tokens[0] ?? \"*\");\n // the crawl delay of a group that names a further agent applies to the fetches of that agent too\n const delays = [primary.crawlDelaySeconds];\n for (const token of tokens.slice(1)) {\n const extra = groupsFor(robots, token);\n if (extra.explicit) delays.push(extra.crawlDelaySeconds);\n }\n const known = delays.filter((delay): delay is number => delay !== undefined);\n const crawlDelaySeconds = known.length > 0 ? Math.max(...known) : undefined;\n if (path === \"/robots.txt\") {\n return { allowed: true, by: undefined, crawlDelaySeconds, rule: undefined, groups: [] };\n }\n const rule = decidingRule(primary.rules, path);\n if (rule !== undefined && !rule.allow) {\n return { allowed: false, by: tokens[0], crawlDelaySeconds, rule, groups: primary.groups };\n }\n for (const token of tokens.slice(1)) {\n const extra = groupsFor(robots, token);\n const refusal = extra.explicit ? decidingRule(extra.rules, path) : undefined;\n if (refusal !== undefined && !refusal.allow) {\n return { allowed: false, by: token, crawlDelaySeconds, rule: refusal, groups: extra.groups };\n }\n }\n return { allowed: true, by: undefined, crawlDelaySeconds, rule, groups: primary.groups };\n};\n\n/**\n * This function gives the usage lines that apply to these tokens and this path, as\n * `key: value` texts. The lines come from three places:\n *\n * - the lines before the first group\n * - the groups of the first token, or `*`\n * - the groups that name a further token\n *\n * In one group, the line with the longest matching path wins for each key.\n */\nexport const signalsFor = (\n robots: Robots,\n tokens: readonly string[],\n pathAndQuery: string,\n): string[] => {\n const path = normalizePath(pathAndQuery.length === 0 ? \"/\" : pathAndQuery);\n const groups = new Set(groupsFor(robots, tokens[0] ?? \"*\").groups);\n for (const token of tokens.slice(1)) {\n const extra = groupsFor(robots, token);\n if (extra.explicit) for (const group of extra.groups) groups.add(group);\n }\n const chosen: RobotsSignal[] = [];\n const pick = (lines: readonly RobotsSignal[]): void => {\n // for each key: the length of the longest matching path, and the lines with that length\n const best = new Map<string, { length: number; lines: RobotsSignal[] }>();\n for (const line of lines) {\n if (line.path !== undefined && !matchesPattern(line.path, path)) continue;\n const length = line.path?.length ?? 0;\n const before = best.get(line.key);\n if (before === undefined || length > before.length)\n best.set(line.key, { length, lines: [line] });\n else if (length === before.length) before.lines.push(line);\n }\n for (const { lines: kept } of best.values()) chosen.push(...kept);\n };\n pick(robots.fileSignals ?? []);\n for (const group of groups) pick(group.signals ?? []);\n return chosen.sort((a, b) => a.line - b.line).map((line) => `${line.key}: ${line.value}`);\n};\n\n/** This function gives the robots of a site whose robots.txt the crawler cannot read. No path is permitted, and the reason stays. */\nexport const unreachableRobots = (reason: string): Robots => ({\n ...DISALLOW_ALL,\n unreachable: reason,\n});\n\n/** This text is the reason when a robots.txt that answered 200 is a web page, not a robots.txt file. */\nexport const HTML_ROBOTS =\n \"robots.txt is a web page, not a robots.txt file (a login or challenge page)\";\n\n/**\n * This function tells if the body of a robots.txt is a web page. A server can send a challenge, a\n * login or an error page with a 200. Such a body starts with `<!doctype`, `<html`, `<head` or\n * `<body`. With the content type `text/html`, a tag of a page in the first 4 KB is enough. A plain\n * robots.txt that a server sends as `text/html` stays a file.\n */\nexport const isHtmlRobots = (contentType: string | null | undefined, text: string): boolean => {\n const head = (text.charCodeAt(0) === 0xfeff ? text.slice(1) : text).slice(0, 4096);\n if (/^\\s*<(!doctype|html|head|body)[\\s>]/i.test(head)) return true;\n return (\n /html/i.test(contentType ?? \"\") &&\n /<(html|head|body|script|meta|title|div|form)[\\s>/]/i.test(head)\n );\n};\n\n/**\n * This function reads the answer of a robots.txt fetch: the status, the content type and the body.\n * A 2xx with a web page in place of the file is unreadable. Thus no path is permitted (fail\n * closed), as for a body that does not decode. The other statuses follow `robotsFromStatus`.\n */\nexport const robotsFromResponse = (\n status: number,\n contentType: string | null | undefined,\n body: Uint8Array | string,\n): Robots => {\n if (status < 200 || status >= 300) return robotsFromStatus(status, undefined);\n const text = typeof body === \"string\" ? body : new TextDecoder(\"utf-8\").decode(body);\n if (isHtmlRobots(contentType, text)) return unreachableRobots(HTML_ROBOTS);\n return parseRobots(text);\n};\n\n/** This function gives the meaning of the status of a robots.txt fetch (RFC 9309 §2.3.1). */\nexport const robotsFromStatus = (status: number, body: string | undefined): Robots => {\n if (status >= 200 && status < 300) return parseRobots(body ?? \"\");\n if (status === 429) return unreachableRobots(\"status 429, rate-limited\");\n if (status >= 400 && status < 500) return ALLOW_ALL;\n return unreachableRobots(status === 0 ? \"no response\" : `status ${String(status)}`);\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"robots.mjs","names":[],"sources":["../src/robots.ts"],"sourcesContent":["/**\n * This module reads robots.txt as RFC 9309 (September 2022) specifies it. It is for a crawler that\n * tells who it is and obeys what a site says. The important rules, each with a test:\n *\n * - A group is one or more `user-agent` lines and the rules after them. A `user-agent` line after\n * an `allow` or `disallow` line starts a new group, as the parser of Google does. A different\n * line (`crawl-delay`) between two `user-agent` lines does not split the group. A rule before\n * the first `user-agent` line is in no group.\n * - The product token matches with no attention to case. The matching groups merge. The group\n * `*` applies only when no group names the crawler.\n * - The longest matching rule wins, and `allow` wins a tie. A `*` matches any sequence, and a `$`\n * at the end anchors the end. The matcher uses no regular expression, and its time is linear.\n * - `/robots.txt` is always permitted, and an empty `disallow` matches nothing.\n * - The parser reads at least the first 500 KiB.\n * - A usage line (`content-signal`, `content-usage`) belongs to its group, with an optional path\n * before its value. A line before the first group applies to each crawler.\n *\n * The function `robotsFromStatus` gives the meaning of the status of the fetch. A 4xx is\n * \"unavailable\", so all paths are permitted. A 5xx or no response is \"unreachable\", so no path is\n * permitted. A 429 is unreachable too, as Google does it. A site that limits the rate of its robots\n * file does not ask for a crawl.\n */\n\nexport const ROBOTS_MAX_BYTES: number = 500 * 1024;\n\nexport interface RobotsRule {\n readonly allow: boolean;\n /** This value is the path pattern, normalized for comparison. */\n readonly pattern: string;\n /** This value is the number of the line in robots.txt, from 1. A rule that the code makes has none. */\n readonly line?: number | undefined;\n}\n\n/** A usage line states a preference about the use of the content (`content-signal`, `content-usage`). */\nexport interface RobotsSignal {\n /** This value is the name of the line, in lowercase. */\n readonly key: string;\n /** This text is the value of the line, after the path. */\n readonly value: string;\n /** This value is the normalized path pattern of the line. A line with no path applies to each path. */\n readonly path: string | undefined;\n /** This value is the number of the line in robots.txt, from 1. */\n readonly line: number;\n}\n\nexport interface RobotsGroup {\n /** These values are the user agents of the group, in lowercase. */\n readonly agents: readonly string[];\n /** This value is the number of the first `user-agent` line of the group, from 1. */\n readonly line?: number | undefined;\n readonly rules: readonly RobotsRule[];\n /** This value is the nonstandard `crawl-delay` in seconds, when the group sets one. */\n readonly crawlDelaySeconds: number | undefined;\n /** These usage lines are in the group. They apply when the group applies. */\n readonly signals?: readonly RobotsSignal[] | undefined;\n}\n\nexport interface Robots {\n readonly groups: readonly RobotsGroup[];\n readonly sitemaps: readonly string[];\n /** These lines state usage preferences (`content-signal: …`, `content-usage: …`), as written, from each group. */\n readonly signals: readonly string[];\n /** These usage lines come before the first group, so they apply to each crawler. */\n readonly fileSignals?: readonly RobotsSignal[] | undefined;\n /**\n * This text tells why the crawler could not read robots.txt: a dead domain, a broken certificate\n * or a server error. Then no path is permitted, and the note of a skip gives this reason. It does\n * not name a rule of the site.\n */\n readonly unreachable?: string | undefined;\n}\n\nexport const ALLOW_ALL: Robots = { groups: [], sitemaps: [], signals: [] };\n\nexport const DISALLOW_ALL: Robots = {\n groups: [\n { agents: [\"*\"], rules: [{ allow: false, pattern: \"/\" }], crawlDelaySeconds: undefined },\n ],\n sitemaps: [],\n signals: [],\n};\n\nconst UNRESERVED = /^[A-Za-z0-9\\-._~]$/;\nconst URL_ENCODED = new Set(\"\\\"'<>\\\\^`{|}\");\n/**\n * The URL parser encodes these ASCII characters in a path or a query: the controls, a space, DEL,\n * and `\"'<>\\^`{|}`. A rule can contain them as written.\n */\nconst encodedAscii = (char: string): boolean => {\n const code = char.charCodeAt(0);\n return code <= 0x20 || code === 0x7f || URL_ENCODED.has(char);\n};\nconst HEX = /^[0-9A-Fa-f]$/;\n\nconst escapeOf = (char: string): string => {\n const code = char.codePointAt(0)!;\n if (code <= 0x7f) return `%${code.toString(16).toUpperCase().padStart(2, \"0\")}`;\n try {\n return encodeURIComponent(char);\n } catch {\n // a lone surrogate: the URL parser writes the replacement character\n return \"%EF%BF%BD\";\n }\n};\n\n/**\n * This function normalizes a path, or the pattern of a rule, for comparison, as §2.2.2 asks. It\n * encodes the characters outside US-ASCII, and the ASCII characters that the URL parser encodes\n * (a space, `\"`, `<`, `{`). It decodes an escape of an unreserved character, so `/%7Efoo` and\n * `/~foo` are equal. The other escapes stay, with uppercase hexadecimal digits.\n */\nexport const normalizePath = (value: string): string => {\n let out = \"\";\n const chars = Array.from(value);\n for (let index = 0; index < chars.length; index += 1) {\n const char = chars[index]!;\n if (char === \"%\" && HEX.test(chars[index + 1] ?? \"\") && HEX.test(chars[index + 2] ?? \"\")) {\n const hex = `${chars[index + 1]!}${chars[index + 2]!}`;\n const decoded = String.fromCharCode(Number.parseInt(hex, 16));\n out += UNRESERVED.test(decoded) ? decoded : `%${hex.toUpperCase()}`;\n index += 2;\n continue;\n }\n out += char.charCodeAt(0) > 0x7f || encodedAscii(char) ? escapeOf(char) : char;\n }\n return out;\n};\n\n/** This function tells if a text is a product token: letters, `_` and `-` only, and one or more (RFC 9309, section 2.2.1). */\nexport const isProductToken = (value: string): boolean => /^[A-Za-z_-]+$/.test(value);\n\n/** This function gives the first product token of a user agent: `Googlebot/2.1` gives `googlebot`. */\nconst tokenOf = (value: string): string =>\n (/^[A-Za-z_-]+/.exec(value.trim())?.[0] ?? \"\").toLowerCase();\n\n/** These names are parts of the header of a browser, not the name of a crawler. */\nconst BROWSER_PARTS: ReadonlySet<string> = new Set([\n \"mozilla\",\n \"applewebkit\",\n \"khtml\",\n \"gecko\",\n \"chrome\",\n \"chromium\",\n \"safari\",\n \"version\",\n \"mobile\",\n \"firefox\",\n \"edg\",\n \"opr\",\n \"like\",\n]);\n\n/**\n * This function gives the product token that robots.txt rules match, in lowercase. `MyBot/1.0`\n * gives `mybot`. The full header `Mozilla/5.0 (compatible; MyBot/1.0; +https://…)` also gives\n * `mybot`. The token is the name after `compatible;`, or else the first product that is not a part\n * of a browser.\n */\nexport const productToken = (value: string): string => {\n const text = value.trim();\n const first = tokenOf(text);\n if (first !== \"mozilla\") return first;\n const compatible = /compatible;\\s*([A-Za-z_-]+)/i.exec(text)?.[1];\n if (compatible !== undefined) return compatible.toLowerCase();\n // One pass in linear time: each match is a name that starts with a letter, with an optional `/` and\n // version after it, or a run of other characters. A run of other characters takes no version, so\n // `_/a/a` gives the product `a`, as the scan before 0.2.0 did.\n const products = [...text.matchAll(/([A-Za-z][A-Za-z_-]*)(\\/[\\w.]+)?|[^A-Za-z]+/g)].flatMap(\n (match) => (match[1] !== undefined && match[2] !== undefined ? [match[1].toLowerCase()] : []),\n );\n return products.find((product) => !BROWSER_PARTS.has(product)) ?? first;\n};\n\nconst truncateUtf8 = (text: string, maxBytes: number): string => {\n const bytes = new TextEncoder().encode(text);\n if (bytes.length <= maxBytes) return text;\n // cut at the last line break inside the limit, so no rule is half-read\n const head = new TextDecoder().decode(bytes.slice(0, maxBytes));\n const lastBreak = Math.max(head.lastIndexOf(\"\\n\"), head.lastIndexOf(\"\\r\"));\n return lastBreak === -1 ? \"\" : head.slice(0, lastBreak);\n};\n\n/** This function reads a usage line: an optional path, then the value (`/images/ train-ai=n`). */\nconst signalOf = (key: string, value: string, line: number): RobotsSignal => {\n const path = /^(\\/\\S*)\\s+(.*)$/.exec(value);\n return path === null\n ? { key, value, path: undefined, line }\n : { key, value: path[2]!.trim(), path: normalizePath(path[1]!), line };\n};\n\nexport const parseRobots = (input: string): Robots => {\n const groups: Array<{\n agents: string[];\n line: number;\n rules: RobotsRule[];\n crawlDelaySeconds: number | undefined;\n signals: RobotsSignal[];\n }> = [];\n const sitemaps: string[] = [];\n const signals: string[] = [];\n const fileSignals: RobotsSignal[] = [];\n let current: (typeof groups)[number] | undefined;\n let collectingAgents = false;\n for (const [index, raw] of truncateUtf8(input, ROBOTS_MAX_BYTES)\n .split(/\\r\\n|\\r|\\n/)\n .entries()) {\n const line = raw.replace(/#.*$/, \"\").trim();\n const colon = line.indexOf(\":\");\n if (colon <= 0) continue;\n const key = line.slice(0, colon).trim().toLowerCase();\n const value = line.slice(colon + 1).trim();\n if (key === \"user-agent\") {\n if (current === undefined || !collectingAgents) {\n current = {\n agents: [],\n line: index + 1,\n rules: [],\n crawlDelaySeconds: undefined,\n signals: [],\n };\n groups.push(current);\n }\n current.agents.push(value.toLowerCase());\n collectingAgents = true;\n continue;\n }\n // records that belong to no group\n if (key === \"sitemap\") {\n if (value.length > 0) sitemaps.push(value);\n continue;\n }\n // an RSL `License` line names the license of the content: the crawler records it with the other usage lines\n if (key === \"content-signal\" || key === \"content-usage\" || key === \"license\") {\n signals.push(`${key}: ${value}`);\n (current?.signals ?? fileSignals).push(signalOf(key, value, index + 1));\n continue;\n }\n if (current === undefined) continue;\n if (key === \"allow\" || key === \"disallow\") {\n // only a rule ends the list of agents, so a crawl-delay between two agents keeps one group\n collectingAgents = false;\n if (value.length > 0)\n current.rules.push({\n allow: key === \"allow\",\n pattern: normalizePath(value),\n line: index + 1,\n });\n } else if (key === \"crawl-delay\") {\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds >= 0) current.crawlDelaySeconds = seconds;\n }\n }\n return { groups, sitemaps, signals, fileSignals };\n};\n\n/**\n * This function tells if a normalized path matches the pattern of a rule. A `*` matches any\n * sequence, and a `$` at the end anchors the end. The function finds the parts between the stars\n * in sequence, each at its first position. No pattern makes it slow: its time is linear in the\n * length of the path for each part, and it uses no regular expression.\n */\nexport const matchesPattern = (pattern: string, path: string): boolean => {\n const anchored = pattern.endsWith(\"$\");\n const parts = (anchored ? pattern.slice(0, -1) : pattern).split(\"*\");\n const first = parts[0]!;\n if (!path.startsWith(first)) return false;\n if (parts.length === 1) return !anchored || path.length === first.length;\n let at = first.length;\n const last = parts.length - 1;\n for (let index = 1; index < last; index += 1) {\n const part = parts[index]!;\n // two stars in sequence give an empty part, which matches at once\n if (part.length === 0) continue;\n const found = path.indexOf(part, at);\n if (found === -1) return false;\n at = found + part.length;\n }\n const tail = parts[last]!;\n if (!anchored) return tail.length === 0 || path.indexOf(tail, at) !== -1;\n return path.length - tail.length >= at && path.endsWith(tail);\n};\n\ninterface Chosen {\n readonly rules: readonly RobotsRule[];\n readonly groups: readonly RobotsGroup[];\n readonly crawlDelaySeconds: number | undefined;\n /** This value is true when a group names this token, and `*` did not apply. */\n readonly explicit: boolean;\n}\n\nconst groupsFor = (robots: Robots, token: string): Chosen => {\n const wanted = token === \"*\" ? \"*\" : productToken(token);\n const named = robots.groups.filter((group) =>\n group.agents.some((agent) => agent !== \"*\" && tokenOf(agent) === wanted),\n );\n const chosen =\n named.length > 0 ? named : robots.groups.filter((group) => group.agents.includes(\"*\"));\n const delays = chosen.flatMap((group) =>\n group.crawlDelaySeconds === undefined ? [] : [group.crawlDelaySeconds],\n );\n return {\n rules: chosen.flatMap((group) => group.rules),\n groups: chosen,\n crawlDelaySeconds: delays.length > 0 ? Math.max(...delays) : undefined,\n explicit: named.length > 0,\n };\n};\n\n/** This function gives the rule that decides for the path: the longest match, and `allow` in a tie. */\nconst decidingRule = (rules: readonly RobotsRule[], path: string): RobotsRule | undefined => {\n let best: RobotsRule | undefined;\n for (const rule of rules) {\n if (!matchesPattern(rule.pattern, path)) continue;\n if (\n best === undefined ||\n rule.pattern.length > best.pattern.length ||\n (rule.pattern.length === best.pattern.length && rule.allow && !best.allow)\n ) {\n best = rule;\n }\n }\n return best;\n};\n\nexport interface RobotsDecision {\n readonly allowed: boolean;\n /** This value is the product token whose rules refused the path. */\n readonly by: string | undefined;\n readonly crawlDelaySeconds: number | undefined;\n /** This rule decided. When no rule matches the path, the path is permitted and this value is empty. */\n readonly rule?: RobotsRule | undefined;\n /** These groups applied to the decision: the groups that name the token, or the group `*`. */\n readonly groups?: readonly RobotsGroup[] | undefined;\n}\n\n/**\n * This function tells if these tokens can fetch `pathAndQuery`. Each token becomes its product\n * token (`productToken`), so `MyBot/1.0` matches the group `mybot`. The first token is the token of\n * the crawler, and `*` applies when no group names it. A further token, for example `Claude-User`,\n * applies only when the site names it. A site that writes rules for an agent means them for the\n * fetches of that agent.\n */\nexport const decide = (\n robots: Robots,\n tokens: readonly string[],\n pathAndQuery: string,\n): RobotsDecision => {\n const path = normalizePath(pathAndQuery.length === 0 ? \"/\" : pathAndQuery);\n const primary = groupsFor(robots, tokens[0] ?? \"*\");\n // the crawl delay of a group that names a further agent applies to the fetches of that agent too\n const delays = [primary.crawlDelaySeconds];\n for (const token of tokens.slice(1)) {\n const extra = groupsFor(robots, token);\n if (extra.explicit) delays.push(extra.crawlDelaySeconds);\n }\n const known = delays.filter((delay): delay is number => delay !== undefined);\n const crawlDelaySeconds = known.length > 0 ? Math.max(...known) : undefined;\n if (path === \"/robots.txt\") {\n return { allowed: true, by: undefined, crawlDelaySeconds, rule: undefined, groups: [] };\n }\n const rule = decidingRule(primary.rules, path);\n if (rule !== undefined && !rule.allow) {\n return { allowed: false, by: tokens[0], crawlDelaySeconds, rule, groups: primary.groups };\n }\n for (const token of tokens.slice(1)) {\n const extra = groupsFor(robots, token);\n const refusal = extra.explicit ? decidingRule(extra.rules, path) : undefined;\n if (refusal !== undefined && !refusal.allow) {\n return { allowed: false, by: token, crawlDelaySeconds, rule: refusal, groups: extra.groups };\n }\n }\n return { allowed: true, by: undefined, crawlDelaySeconds, rule, groups: primary.groups };\n};\n\n/**\n * This function gives the usage lines that apply to these tokens and this path, as\n * `key: value` texts. The lines come from three places:\n *\n * - the lines before the first group\n * - the groups of the first token, or `*`\n * - the groups that name a further token\n *\n * In one group, the line with the longest matching path wins for each key.\n */\nexport const signalsFor = (\n robots: Robots,\n tokens: readonly string[],\n pathAndQuery: string,\n): string[] => {\n const path = normalizePath(pathAndQuery.length === 0 ? \"/\" : pathAndQuery);\n const groups = new Set(groupsFor(robots, tokens[0] ?? \"*\").groups);\n for (const token of tokens.slice(1)) {\n const extra = groupsFor(robots, token);\n if (extra.explicit) for (const group of extra.groups) groups.add(group);\n }\n const chosen: RobotsSignal[] = [];\n const pick = (lines: readonly RobotsSignal[]): void => {\n // for each key: the length of the longest matching path, and the lines with that length\n const best = new Map<string, { length: number; lines: RobotsSignal[] }>();\n for (const line of lines) {\n if (line.path !== undefined && !matchesPattern(line.path, path)) continue;\n const length = line.path?.length ?? 0;\n const before = best.get(line.key);\n if (before === undefined || length > before.length)\n best.set(line.key, { length, lines: [line] });\n else if (length === before.length) before.lines.push(line);\n }\n for (const { lines: kept } of best.values()) chosen.push(...kept);\n };\n pick(robots.fileSignals ?? []);\n for (const group of groups) pick(group.signals ?? []);\n return chosen.sort((a, b) => a.line - b.line).map((line) => `${line.key}: ${line.value}`);\n};\n\n/** This function gives the robots of a site whose robots.txt the crawler cannot read. No path is permitted, and the reason stays. */\nexport const unreachableRobots = (reason: string): Robots => ({\n ...DISALLOW_ALL,\n unreachable: reason,\n});\n\n/** This text is the reason when a robots.txt that answered 200 is a web page, not a robots.txt file. */\nexport const HTML_ROBOTS =\n \"robots.txt is a web page, not a robots.txt file (a login or challenge page)\";\n\n/**\n * This function tells if the body of a robots.txt is a web page. A server can send a challenge, a\n * login or an error page with a 200. Such a body starts with `<!doctype`, `<html`, `<head` or\n * `<body`. With the content type `text/html`, a tag of a page in the first 4 KB is enough. A plain\n * robots.txt that a server sends as `text/html` stays a file.\n */\nexport const isHtmlRobots = (contentType: string | null | undefined, text: string): boolean => {\n const head = (text.charCodeAt(0) === 0xfeff ? text.slice(1) : text).slice(0, 4096);\n if (/^\\s*<(!doctype|html|head|body)[\\s>]/i.test(head)) return true;\n return (\n /html/i.test(contentType ?? \"\") &&\n /<(html|head|body|script|meta|title|div|form)[\\s>/]/i.test(head)\n );\n};\n\n/**\n * This function reads the answer of a robots.txt fetch: the status, the content type and the body.\n * A 2xx with a web page in place of the file is unreadable. Thus no path is permitted (fail\n * closed), as for a body that does not decode. The other statuses follow `robotsFromStatus`.\n */\nexport const robotsFromResponse = (\n status: number,\n contentType: string | null | undefined,\n body: Uint8Array | string,\n): Robots => {\n if (status < 200 || status >= 300) return robotsFromStatus(status, undefined);\n const text = typeof body === \"string\" ? body : new TextDecoder(\"utf-8\").decode(body);\n if (isHtmlRobots(contentType, text)) return unreachableRobots(HTML_ROBOTS);\n return parseRobots(text);\n};\n\n/** This function gives the meaning of the status of a robots.txt fetch (RFC 9309 §2.3.1). */\nexport const robotsFromStatus = (status: number, body: string | undefined): Robots => {\n if (status >= 200 && status < 300) return parseRobots(body ?? \"\");\n if (status === 429) return unreachableRobots(\"status 429, rate-limited\");\n if (status >= 400 && status < 500) return ALLOW_ALL;\n return unreachableRobots(status === 0 ? \"no response\" : `status ${String(status)}`);\n};\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import "node:module";
1
2
  //#region \0rolldown/runtime.js
2
3
  var __defProp = Object.defineProperty;
3
4
  var __exportAll = (all, no_symbols) => {
@@ -1,11 +1,11 @@
1
- import { n as RenderRefused } from "./render-C1CUraWF.mjs";
2
- import { n as DEFAULT_TOKEN } from "./version-BPem-XZd.mjs";
1
+ import { n as RenderRefused } from "./render-CKE2LILH.mjs";
2
+ import { n as DEFAULT_TOKEN } from "./version-Djva7JkN.mjs";
3
3
  import { classify, isClientError, parseHttpDate } from "./classify.mjs";
4
4
  import { DEFAULT_POLICY, HostScheduler, HostStopped, MAX_STOP_MS, checkPolicy, equalJitterMs, policyFor, realClock, restAfter, validatorsToSend } from "./politeness.mjs";
5
5
  import { decide, isProductToken, robotsFromResponse, robotsFromStatus, signalsFor, unreachableRobots } from "./robots.mjs";
6
6
  import { NO_SIGNALS, metaSignals, readSignals } from "./signals.mjs";
7
7
  import { n as canonicalUrl, t as cacheKeyOf } from "./urls-BHFeSyEK.mjs";
8
- import { a as signerOf } from "./webbotauth-CjJdTAfE.mjs";
8
+ import { a as signerOf } from "./webbotauth-Dq_dA_u3.mjs";
9
9
  import { o as decodeBody, r as typesOf, t as extractPage } from "./html-DkQUAE_d.mjs";
10
10
  import { createHash } from "node:crypto";
11
11
  import { Transform } from "node:stream";
@@ -15,7 +15,11 @@ import { lookup } from "node:dns";
15
15
  import { isIP } from "node:net";
16
16
  import { getDocumentProxy, getMeta } from "unpdf";
17
17
  //#region src/egress.ts
18
- /** The connection hook gives this error when the policy refuses each address of a host. */
18
+ /**
19
+ * The connection hook gives this error when the policy refuses each address of a host.
20
+ *
21
+ * @beta
22
+ */
19
23
  var EgressRefused = class extends Error {
20
24
  address;
21
25
  kind;
@@ -335,6 +339,8 @@ const screen = (policy, host, addresses) => {
335
339
  * This function gives a `lookup` hook for the connection options of an undici `Agent`. The hook
336
340
  * resolves the name, removes each address that the policy refuses, and gives the rest to the
337
341
  * socket. When no address stays, the connection fails with `EgressRefused`.
342
+ *
343
+ * @beta
338
344
  */
339
345
  const egressLookup = (policy, resolve = resolveAll) => (host, options, callback) => {
340
346
  resolve(host, {
@@ -407,6 +413,8 @@ const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex")
407
413
  * This function makes a capture record. The hash covers the source, the kind, the external id
408
414
  * and the payload. The id is the source and the first 12 digits of the hash. The times, `scope`
409
415
  * and `labels` are not part of the hash.
416
+ *
417
+ * @beta
410
418
  */
411
419
  const makeRecord = (input) => {
412
420
  const content_hash = sha256(stableStringify({
@@ -430,20 +438,25 @@ const makeRecord = (input) => {
430
438
  };
431
439
  //#endregion
432
440
  //#region src/crawler.ts
433
- /** The note of a request starts with this text when its host was not free before its deadline, so the request did not start. */
441
+ /**
442
+ * The note of a request starts with this text when its host was not free before its deadline, so the request did not start.
443
+ *
444
+ * @beta
445
+ */
434
446
  const DEADLINE_NOTE = "deadline reached";
435
447
  /**
436
- * This function gives the key of the cache entry of a rendered page in the form of version 0.1.
437
- * The index keeps the variant in a column of its own, and it reads this form too.
448
+ * This value is the version of the payload of the records that the crawler writes. A record with no `schema` is version 1.
449
+ *
450
+ * @beta
438
451
  */
439
- const renderedKey = (url) => `${url}#rendered`;
440
- /** This value is the version of the payload of the records that the crawler writes. A record with no `schema` is version 1. */
441
452
  const CAPTURE_SCHEMA = 2;
442
453
  /** The content type of a rendered page: the browser gives its text as UTF-8. */
443
454
  const RENDERED_TYPE = "text/html; charset=utf-8";
444
455
  /**
445
456
  * These query parameters hold secrets in many APIs. The crawler writes `REDACTED` in place of
446
457
  * their values in each capture, in addition to the names of `secretParams`.
458
+ *
459
+ * @beta
447
460
  */
448
461
  const DEFAULT_SECRET_PARAMS = [
449
462
  "access_token",
@@ -606,7 +619,7 @@ var Slots = class {
606
619
  }
607
620
  };
608
621
  /** This function tells if an error of a renderer says that it has no browser: no `playwright-core`, or no browser on the disk. */
609
- const missingBrowser = (message) => message.startsWith("rendering needs the package playwright-core and a browser: npm install playwright-core, then npx playwright-core install chromium-headless-shell. For the MCP server with npx, start it with npx -y -p polite-crawler -p playwright-core polite-crawler mcp") || /Executable doesn't exist|Cannot find (?:module|package) 'playwright-core'/.test(message);
622
+ const missingBrowser = (message) => message.startsWith("rendering needs the package playwright-core and a browser: npm install playwright-core, then npx playwright-core install chromium-headless-shell. For the MCP server with npx, start it with npx -y -p polite-crawler-mcp -p playwright-core polite-crawler-mcp") || /Executable doesn't exist|Cannot find (?:module|package) 'playwright-core'/.test(message);
610
623
  /** This text tells why the crawler did not fetch a page: a rule of the site, or no robots.txt to read. */
611
624
  const robotsRefusal = (robots, by) => robots.unreachable === void 0 ? `robots.txt disallows it for ${by}` : `robots.txt could not be read (${robots.unreachable}), so nothing on the site is fetched`;
612
625
  const messageOf$1 = (error) => error instanceof Error ? error.message.split(/\r?\n/)[0] ?? "" : String(error);
@@ -912,6 +925,7 @@ const raceAbort = (work, signal) => {
912
925
  });
913
926
  });
914
927
  };
928
+ /** @beta */
915
929
  var Crawler = class {
916
930
  options;
917
931
  clock;
@@ -2600,6 +2614,8 @@ const atBoundary = (text, position, forward) => {
2600
2614
  * This function finds a quote in a text, in the `quote` mode of `foldText`. It ignores case, runs
2601
2615
  * of spaces, typographic punctuation, soft hyphens, zero-width characters and the differences of
2602
2616
  * NFKC, NFC and NFD. The positions are in the original text.
2617
+ *
2618
+ * @beta
2603
2619
  */
2604
2620
  const locate = (text, quote) => {
2605
2621
  const needle = fold(quote, "quote");
@@ -2749,7 +2765,11 @@ const TIME_BUDGET_MS = 2e4;
2749
2765
  * approximately one second.
2750
2766
  */
2751
2767
  const MAX_INFLATED_BYTES$1 = 8388608;
2752
- /** This function finds a PDF from its first bytes or its content type. Some servers put bytes before `%PDF-`. */
2768
+ /**
2769
+ * This function finds a PDF from its first bytes or its content type. Some servers put bytes before `%PDF-`.
2770
+ *
2771
+ * @beta
2772
+ */
2753
2773
  const isPdf = (body, contentType) => {
2754
2774
  if (/\bpdf\b/i.test(contentType ?? "")) return true;
2755
2775
  const end = Math.min(body.length - 4, 1024);
@@ -2834,6 +2854,7 @@ const cutText = (text, length) => {
2834
2854
  if (end > 0 && end < text.length && code >= 56320 && code <= 57343) end -= 1;
2835
2855
  return text.slice(0, end).trimEnd();
2836
2856
  };
2857
+ /** @beta */
2837
2858
  const readPdf = async (bytes, options = {}) => {
2838
2859
  const maxPages = options.maxPages ?? MAX_PAGES;
2839
2860
  const maxChars = options.maxChars ?? MAX_CHARS;
@@ -2906,7 +2927,11 @@ const ARCHIVE_COPY_RECIPE = "archive-copy";
2906
2927
  const ARCHIVE_INDEX_RECIPE = "archive-index";
2907
2928
  /** The kind of the document of an archived copy. */
2908
2929
  const ARCHIVED_KIND = "archived";
2909
- /** This function gives the document id of the archived copy of a page. */
2930
+ /**
2931
+ * This function gives the document id of the archived copy of a page.
2932
+ *
2933
+ * @beta
2934
+ */
2910
2935
  const archivedDocId = (url) => `archive:${canonicalUrl(url)}`;
2911
2936
  /** The addresses of the archives. */
2912
2937
  const WAYBACK_AVAILABLE = "https://archive.org/wayback/available";
@@ -3108,6 +3133,8 @@ const asArchived = (docs, archived, aiInput) => docs.filter((doc) => typeof doc.
3108
3133
  *
3109
3134
  * It reads the body with the page reader, and gives a document of the kind `archived` with
3110
3135
  * `archived` in its metadata. The usage signals of the HTTP headers in a WARC record apply too.
3136
+ *
3137
+ * @beta
3111
3138
  */
3112
3139
  const archiveExtractor = async (body, capture) => {
3113
3140
  let url;
@@ -3405,7 +3432,11 @@ const archivedPage = async (runtime, url, live, options) => {
3405
3432
  };
3406
3433
  //#endregion
3407
3434
  //#region src/runtime.ts
3408
- /** This function gives the document id of a web page. Each page has one id. */
3435
+ /**
3436
+ * This function gives the document id of a web page. Each page has one id.
3437
+ *
3438
+ * This export is internal: the root entry point does not have it since 0.2.0.
3439
+ */
3409
3440
  const pageDocId = (url) => `url:${canonicalUrl(url)}`;
3410
3441
  /** The JSON-LD that a document keeps, in characters of JSON. */
3411
3442
  const MAX_JSONLD_CHARS = 3e4;
@@ -3468,6 +3499,8 @@ const keepJsonLd = (records) => {
3468
3499
  * encoding that the page declares, with `decodeBody`. It does not throw: a page that it cannot read
3469
3500
  * gives a document that names the error. When the reader cut the text, the document keeps the whole
3470
3501
  * text in `full_text`, so `verify` finds a quote past the cut.
3502
+ *
3503
+ * This export is internal: the root entry point does not have it since 0.2.0.
3471
3504
  */
3472
3505
  const pageExtractor = (body, capture) => {
3473
3506
  if (isPdf(body, capture.contentType)) return pdfDocs(body, capture.url);
@@ -3564,6 +3597,7 @@ const pdfDocs = async (body, url) => {
3564
3597
  }
3565
3598
  }];
3566
3599
  };
3600
+ /** @beta */
3567
3601
  var Extractors = class {
3568
3602
  byRecipe = /* @__PURE__ */ new Map([["page", pageExtractor], ["archive-copy", archiveExtractor]]);
3569
3603
  replaceable = /* @__PURE__ */ new Set(["archive-copy"]);
@@ -3598,14 +3632,24 @@ const STRUCTURAL_META = [
3598
3632
  "archived",
3599
3633
  "archived_kind"
3600
3634
  ];
3601
- /** The note of a document that is held back. */
3635
+ /**
3636
+ * The note of a document that is held back.
3637
+ *
3638
+ * @beta
3639
+ */
3602
3640
  const HELD_BACK = "This page asked not to be used as AI input, so its text is held back. Open the URL to read it, or read again with userProvided when a person gave this URL directly (the crawler logs that).";
3603
3641
  /**
3604
3642
  * This function tells if a document is held back: the page asked not to be AI input, and no person
3605
3643
  * gave its URL directly. The only way to read a held-back document is `userProvided`.
3644
+ *
3645
+ * @beta
3606
3646
  */
3607
3647
  const isHeldBack = (doc, userProvided = false) => doc.ai_input === "no" && !userProvided;
3608
- /** This function gives the metadata that a held-back document shows: only the structural fields, which hold no text of the page. */
3648
+ /**
3649
+ * This function gives the metadata that a held-back document shows: only the structural fields, which hold no text of the page.
3650
+ *
3651
+ * @beta
3652
+ */
3609
3653
  const heldBackMeta = (meta) => Object.fromEntries(Object.entries(meta).filter(([key]) => STRUCTURAL_META.includes(key)));
3610
3654
  const messageOf = (error) => error instanceof Error ? error.message.split(/\r?\n/)[0] ?? "" : String(error);
3611
3655
  /** This function tells which bot check or firewall sent its page in place of the content, or gives undefined. */
@@ -3663,6 +3707,8 @@ const indexCapture = async (runtime, fetched, recipe, rendered) => {
3663
3707
  * from the cache gives the documents that the index has for that capture. Thus a repeated
3664
3708
  * question sends no request and reads nothing again. An extractor that throws gives no document and an
3665
3709
  * `error`, and the fetch still succeeds.
3710
+ *
3711
+ * @beta
3666
3712
  */
3667
3713
  const fetchAndIndex = async (runtime, request) => {
3668
3714
  const fetched = await runtime.crawler.fetch(request);
@@ -3675,7 +3721,11 @@ const fetchAndIndex = async (runtime, request) => {
3675
3721
  ...await indexCapture(runtime, fetched, request.recipe, false)
3676
3722
  };
3677
3723
  };
3678
- /** This function renders a page in the headless browser and adds what the page shows to the index. */
3724
+ /**
3725
+ * This function renders a page in the headless browser and adds what the page shows to the index.
3726
+ *
3727
+ * This export is internal: the root entry point does not have it since 0.2.0.
3728
+ */
3679
3729
  const renderAndIndex = async (runtime, request) => {
3680
3730
  const fetched = await runtime.crawler.render(request);
3681
3731
  if (fetched.outcome !== "ok") return {
@@ -3693,6 +3743,8 @@ const HTML_TYPE = /html|xml/i;
3693
3743
  * the page shows almost nothing without JavaScript, or when the caller asks for it. It renders
3694
3744
  * only an HTML page: not a PDF, a different file or a bot check. A page that sends the reader on
3695
3745
  * with a `<meta http-equiv="refresh">` gets one more plain fetch, of its target.
3746
+ *
3747
+ * @beta
3696
3748
  */
3697
3749
  const fetchPage = async (runtime, url, options) => {
3698
3750
  const plainRequest = (target) => ({
@@ -3797,6 +3849,8 @@ const isoTime = (name, value) => {
3797
3849
  * This function gives the version of a document that was current at a time (ISO 8601). It uses
3798
3850
  * `ReadingIndex.versionAt` when the index has it. Else it takes the newest version at or before
3799
3851
  * the time.
3852
+ *
3853
+ * @beta
3800
3854
  */
3801
3855
  const versionAtTime = (runtime, id, at) => {
3802
3856
  const index = runtime.index;
@@ -3830,6 +3884,8 @@ const onBoundary = (text, position) => {
3830
3884
  * This function writes an `override` record to the capture log. A person gave the URL of a
3831
3885
  * held-back document directly, and the hold stops for one action (`read` or `verify`). The record
3832
3886
  * takes the scope and the labels of `marks`, or else those of the document, as its captures do.
3887
+ *
3888
+ * @beta
3833
3889
  */
3834
3890
  const recordOverride = async (runtime, doc, action, marks = {}) => {
3835
3891
  const at = runtime.now();
@@ -3855,6 +3911,8 @@ const recordOverride = async (runtime, doc, action, marks = {}) => {
3855
3911
  * This function reads a range of characters of one stored document. It applies the rule for a
3856
3912
  * held-back document: no title, no text, and only the structural metadata, unless `userProvided`.
3857
3913
  * The positions do not cut a character in two.
3914
+ *
3915
+ * @beta
3858
3916
  */
3859
3917
  const readDocument = async (runtime, id, options = {}) => {
3860
3918
  const from = wholeNumber("from", options.from, 0);
@@ -4066,6 +4124,8 @@ const examineQuote = (runtime, id, quote, options) => {
4066
4124
  * unless `userProvided`. The same rule applies to each version. A version that asked not to be AI
4067
4125
  * input stays held back after a later capture drops the signal. The crawler logs an override when
4068
4126
  * `userProvided` opens a held-back version.
4127
+ *
4128
+ * @beta
4069
4129
  */
4070
4130
  const verifyQuote = async (runtime, id, quote, options = {}) => {
4071
4131
  const { result, override } = examineQuote(runtime, id, quote, options);
@@ -4076,6 +4136,8 @@ const verifyQuote = async (runtime, id, quote, options = {}) => {
4076
4136
  * This function searches the stored documents. The syntax of the query is the syntax of
4077
4137
  * `parseQuery`. A held-back document is not in the results. The option `rank` chooses the
4078
4138
  * ranking (`RankMode`), and the default is `exact`.
4139
+ *
4140
+ * @beta
4079
4141
  */
4080
4142
  const findDocuments = async (runtime, q, options = {}) => {
4081
4143
  await Promise.resolve();
@@ -4234,6 +4296,8 @@ const replayRecord = async (runtime, target, envelope, tally) => {
4234
4296
  * - It counts and skips a damaged line, a missing blob and an extractor that throws.
4235
4297
  * - The documents of a recipe that has no extractor in this runtime stay, from the old index.
4236
4298
  * - A capture that a different writer adds during the rebuild goes into the new index.
4299
+ *
4300
+ * @beta
4237
4301
  */
4238
4302
  const reindex = async (runtime, options = {}) => {
4239
4303
  const since = runtime.now();
@@ -4288,6 +4352,8 @@ const missed = (runtime, envelope) => {
4288
4352
  * function reads the log from a short time before the newest cache entry.
4289
4353
  *
4290
4354
  * It gives the counts, or undefined when the index missed nothing.
4355
+ *
4356
+ * @beta
4291
4357
  */
4292
4358
  const catchUpIndex = async (runtime) => {
4293
4359
  if (runtime.index.stale) return {
@@ -4319,6 +4385,6 @@ const catchUpIndex = async (runtime) => {
4319
4385
  };
4320
4386
  };
4321
4387
  //#endregion
4322
- export { snippetOf as A, egressLookup as B, fallbackReason as C, foldText as D, fold as E, renderedKey as F, makeRecord as I, sha256 as L, Crawler as M, DEADLINE_NOTE as N, locate as O, DEFAULT_SECRET_PARAMS as P, stableStringify as R, archivedOf as S, readPdf as T, versionAtTime as _, fetchPage as a, archivedDocId as b, isHeldBack as c, pageExtractor as d, readDocument as f, verifyQuote as g, renderAndIndex as h, fetchAndIndex as i, CAPTURE_SCHEMA as j, parseQuery as k, isoTime as l, reindex as m, HELD_BACK as n, findDocuments as o, recordOverride as p, catchUpIndex as r, heldBackMeta as s, Extractors as t, pageDocId as u, archiveExtractor as v, isPdf as w, archivedNote as x, archivedCopy as y, EgressRefused as z };
4388
+ export { DEADLINE_NOTE as A, fold as C, snippetOf as D, parseQuery as E, EgressRefused as F, egressLookup as I, makeRecord as M, sha256 as N, CAPTURE_SCHEMA as O, stableStringify as P, readPdf as S, locate as T, archivedDocId as _, fetchPage as a, fallbackReason as b, isHeldBack as c, recordOverride as d, reindex as f, archivedCopy as g, archiveExtractor as h, fetchAndIndex as i, DEFAULT_SECRET_PARAMS as j, Crawler as k, isoTime as l, versionAtTime as m, HELD_BACK as n, findDocuments as o, verifyQuote as p, catchUpIndex as r, heldBackMeta as s, Extractors as t, readDocument as u, archivedNote as v, foldText as w, isPdf as x, archivedOf as y };
4323
4389
 
4324
- //# sourceMappingURL=runtime-D_r2SYge.mjs.map
4390
+ //# sourceMappingURL=runtime-C_2BQibH.mjs.map