@crewhaus/provability 0.0.0-stage → 0.8.1

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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @crewhaus/provability — the provability ladder (design/0.8.0-provability-release.md).
3
+ *
4
+ * Four rungs — `cited`, `grounded`, `checked`, `proven` — checked by one
5
+ * deterministic verifier (`verifyDocument`) over an answer and the evidence
6
+ * index of the run that wrote it, with the result as one proof bundle every
7
+ * surface renders. The package makes no model call and imports no model
8
+ * adapter: the one model-backed step, the opt-in paraphrase waiver, is a
9
+ * callback its caller injects.
10
+ */
11
+ export * from "./types";
12
+ export { isProvabilityLevel, levelRank, maxRung, meetsFloor, rungOfRank } from "./levels";
13
+ export { type PathRoots, doiUrl, hasUserinfo, hostMatches, hostOf, isPrivateHost, normalizePath, normalizeUrl, originKey, registrableDomain, } from "./origin";
14
+ export { type EvidenceIndexOptions, type EvidenceMessage, type EvidenceOptions, type InputSchemaLookup, type SessionLogEvidenceOptions, type ToolCall, ProvabilityError, buildEvidenceIndex, evidenceFromMessages, evidenceFromSessionLog, groundingBody, recordFromCall, restrictToTurns, servedFrom, servesPage, sha256Of, spilledOutput, } from "./evidence";
15
+ export { PROOF_ID_REGEX, isProofArtifact, isProofId, newProofId } from "./artifact";
16
+ export { HEAD_CONCURRENCY, HEAD_DEADLINE_MS, MAX_GROUNDING_TOKENS, MAX_HEAD_REQUESTS, bundleHash, claimReasons, independenceKey, isToolAllowed, statementMismatch, unverifiedBundle, verifyDocument, } from "./verify";
17
+ export { type FooterOptions, renderBundleJson, renderFooter, renderMarkdownSection, renderNudge, } from "./render";
18
+ export { summarizeBundle } from "./summary";
19
+ /** claim-ledger's, re-exported: what a definition line shows beyond what it names. */
20
+ export { definitionGloss } from "@crewhaus/claim-ledger";
21
+ export { type BundleListing, type BundleStore, NO_SESSION, createBundleStore, createFileArtifactStore, proofsRoot, } from "./store";
22
+ export { isPureTool } from "./purity";
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @crewhaus/provability — the provability ladder (design/0.8.0-provability-release.md).
3
+ *
4
+ * Four rungs — `cited`, `grounded`, `checked`, `proven` — checked by one
5
+ * deterministic verifier (`verifyDocument`) over an answer and the evidence
6
+ * index of the run that wrote it, with the result as one proof bundle every
7
+ * surface renders. The package makes no model call and imports no model
8
+ * adapter: the one model-backed step, the opt-in paraphrase waiver, is a
9
+ * callback its caller injects.
10
+ */
11
+ export * from "./types";
12
+ export { isProvabilityLevel, levelRank, maxRung, meetsFloor, rungOfRank } from "./levels";
13
+ export { doiUrl, hasUserinfo, hostMatches, hostOf, isPrivateHost, normalizePath, normalizeUrl, originKey, registrableDomain, } from "./origin";
14
+ export { ProvabilityError, buildEvidenceIndex, evidenceFromMessages, evidenceFromSessionLog, groundingBody, recordFromCall, restrictToTurns, servedFrom, servesPage, sha256Of, spilledOutput, } from "./evidence";
15
+ export { PROOF_ID_REGEX, isProofArtifact, isProofId, newProofId } from "./artifact";
16
+ export { HEAD_CONCURRENCY, HEAD_DEADLINE_MS, MAX_GROUNDING_TOKENS, MAX_HEAD_REQUESTS, bundleHash, claimReasons, independenceKey, isToolAllowed, statementMismatch, unverifiedBundle, verifyDocument, } from "./verify";
17
+ export { renderBundleJson, renderFooter, renderMarkdownSection, renderNudge, } from "./render";
18
+ export { summarizeBundle } from "./summary";
19
+ /** claim-ledger's, re-exported: what a definition line shows beyond what it names. */
20
+ export { definitionGloss } from "@crewhaus/claim-ledger";
21
+ export { NO_SESSION, createBundleStore, createFileArtifactStore, proofsRoot, } from "./store";
22
+ export { isPureTool } from "./purity";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The ladder's order (design §2): four rungs above `off`, totally ordered,
3
+ * and `"none"` below them for evidence that meets no rung at all.
4
+ *
5
+ * Higher-rung evidence satisfies a lower floor — an execution result
6
+ * satisfies `cited` — so every comparison in the package goes through
7
+ * {@link levelRank} rather than through string equality.
8
+ */
9
+ import { type ProvabilityLevel, type Rung } from "./types";
10
+ /** `none` 0, `cited` 1, `grounded` 2, `checked` 3, `proven` 4. */
11
+ export declare function levelRank(rung: Rung): number;
12
+ /** The rung at a rank (0..4); anything outside clamps to `none` / `proven`. */
13
+ export declare function rungOfRank(rank: number): Rung;
14
+ /** Does evidence at `rung` satisfy a `floor`? */
15
+ export declare function meetsFloor(rung: Rung, floor: ProvabilityLevel): boolean;
16
+ /** The highest of `rungs`; `none` when given none. */
17
+ export declare function maxRung(...rungs: ReadonlyArray<Rung>): Rung;
18
+ /** Is `value` one of the four rungs (not `none`, not `off`)? */
19
+ export declare function isProvabilityLevel(value: unknown): value is ProvabilityLevel;
package/dist/levels.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The ladder's order (design §2): four rungs above `off`, totally ordered,
3
+ * and `"none"` below them for evidence that meets no rung at all.
4
+ *
5
+ * Higher-rung evidence satisfies a lower floor — an execution result
6
+ * satisfies `cited` — so every comparison in the package goes through
7
+ * {@link levelRank} rather than through string equality.
8
+ */
9
+ import { PROVABILITY_LEVELS } from "./types";
10
+ const RANK = {
11
+ none: 0,
12
+ cited: 1,
13
+ grounded: 2,
14
+ checked: 3,
15
+ proven: 4,
16
+ };
17
+ /** `none` 0, `cited` 1, `grounded` 2, `checked` 3, `proven` 4. */
18
+ export function levelRank(rung) {
19
+ return RANK[rung];
20
+ }
21
+ /** The rung at a rank (0..4); anything outside clamps to `none` / `proven`. */
22
+ export function rungOfRank(rank) {
23
+ if (!(rank >= 1))
24
+ return "none";
25
+ return PROVABILITY_LEVELS[Math.min(Math.floor(rank), PROVABILITY_LEVELS.length) - 1];
26
+ }
27
+ /** Does evidence at `rung` satisfy a `floor`? */
28
+ export function meetsFloor(rung, floor) {
29
+ return RANK[rung] >= RANK[floor];
30
+ }
31
+ /** The highest of `rungs`; `none` when given none. */
32
+ export function maxRung(...rungs) {
33
+ let best = "none";
34
+ for (const rung of rungs)
35
+ if (RANK[rung] > RANK[best])
36
+ best = rung;
37
+ return best;
38
+ }
39
+ /** Is `value` one of the four rungs (not `none`, not `off`)? */
40
+ export function isProvabilityLevel(value) {
41
+ return typeof value === "string" && PROVABILITY_LEVELS.includes(value);
42
+ }
@@ -0,0 +1,83 @@
1
+ import type { EvidenceRef } from "@crewhaus/claim-ledger";
2
+ /** Where workspace paths are measured from. */
3
+ export type PathRoots = {
4
+ /** The workspace root (default `process.cwd()`). A path outside it has no origin. */
5
+ readonly workspaceRoot?: string;
6
+ /** Where a relative path is resolved from (default the workspace root);
7
+ * itself resolved against the workspace root when relative. */
8
+ readonly baseDir?: string;
9
+ };
10
+ /**
11
+ * The comparable form of an `http(s)` URL, or `undefined` when `raw` is not
12
+ * one: scheme and host lowercased, a trailing dot on the host dropped, the
13
+ * default port dropped, credentials and the fragment dropped (a fragment
14
+ * names a place in the page, not another page, and credentials are never
15
+ * written into a bundle), a trailing slash folded (`/a/` is `/a`; the root
16
+ * stays `/`), the query kept (`?id=1` and `?id=2` are different pages).
17
+ *
18
+ * The DOI resolver is special-cased: `http://dx.doi.org/10.1000/ABC` and
19
+ * `https://doi.org/10.1000/abc` are one origin, because DOIs are
20
+ * case-insensitive by definition (the DOI Handbook, §2.4) and every
21
+ * resolver host answers the same.
22
+ *
23
+ * `http` and `https` stay distinct: a page fetched over one is not evidence
24
+ * that the other says the same thing, and folding them would let a citation
25
+ * ground in a body the run never read under that name.
26
+ */
27
+ export declare function normalizeUrl(raw: string): string | undefined;
28
+ /**
29
+ * Does `raw` carry userinfo before its host — `https://acme.example@evil.example/
30
+ * policy`, `https://user:pw@host/`? {@link normalizeUrl} drops it, so the
31
+ * origin is the host after the `@`; a reader takes what comes before it for
32
+ * the host. A citation spelled so names one site to the reader and another
33
+ * to the gate (design §2: `cited` is the source the reader can see), and no
34
+ * citation has a use for credentials. Only an `http(s)` URL that parses has
35
+ * any.
36
+ */
37
+ export declare function hasUserinfo(raw: string): boolean;
38
+ /** The resolver URL a DOI is fetched and grounded under: `https://doi.org/<doi>`, normalized. */
39
+ export declare function doiUrl(doi: string): string;
40
+ /** The host of a normalized URL (IPv6 without its brackets), or `undefined`. */
41
+ export declare function hostOf(url: string): string | undefined;
42
+ /**
43
+ * A workspace path as its workspace-relative POSIX form (`docs/pricing.md`),
44
+ * or `undefined` when it names nothing inside the workspace.
45
+ *
46
+ * Lexical, the first half of tool-verify's `resolveSafe`: the path is
47
+ * resolved against `baseDir` and must land inside the workspace root, `..`
48
+ * and absolute escapes refused. It is NOT realpath'd — the key a citation and
49
+ * a `Read` are compared under is the name, and following a link off the
50
+ * workspace to measure it is exactly what this must not do. The verifier's
51
+ * `cited` check does the symlink-aware half (`resolveContained`) before it
52
+ * calls a path a source. A `file://` URL is read as the path it names; any
53
+ * other scheme is not a path.
54
+ */
55
+ export declare function normalizePath(raw: string, roots?: PathRoots): string | undefined;
56
+ /**
57
+ * The origin a citation's evidence reference is looked up under: the
58
+ * normalized URL, the DOI's resolver URL, or the workspace-relative path.
59
+ * Tool, proof, assumption and free-text references have none.
60
+ */
61
+ export declare function originKey(ref: EvidenceRef, roots?: PathRoots): string | undefined;
62
+ /**
63
+ * The registrable domain of a host — `docs.acme.example` → `acme.example`,
64
+ * `www.bbc.co.uk` → `bbc.co.uk` — for `grounded.independent`'s "distinct
65
+ * origins". An IP literal is its own domain; a single label is itself. See
66
+ * {@link TWO_LEVEL_SUFFIXES} for what this approximation knows.
67
+ */
68
+ export declare function registrableDomain(host: string): string;
69
+ /**
70
+ * Does `host` match any of `globs` (`sources.allow` / `sources.deny`)? The
71
+ * IR's grammar: `*` (any host), `*.<host>` (any host BELOW it — `*.gov`
72
+ * matches `nasa.gov` and `www.nasa.gov`, not `gov`), or an exact host.
73
+ * Case-insensitive; a trailing dot on the host is ignored.
74
+ */
75
+ export declare function hostMatches(host: string, globs: ReadonlyArray<string>): boolean;
76
+ /**
77
+ * Is `host` loopback, private, link-local or otherwise not a public source?
78
+ * The repo's one IP classifier (`private-address.ts`, every spelling of
79
+ * `169.254.169.254` included) plus the names tool-fetch refuses before DNS:
80
+ * `localhost`, `*.localhost` and the mDNS `*.local`. Offline — a public-looking
81
+ * name that RESOLVES privately is caught by the fetch that reads it, not here.
82
+ */
83
+ export declare function isPrivateHost(host: string): boolean;
package/dist/origin.js ADDED
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Origins: the one spelling of "where a body came from" that a citation and
3
+ * a tool call are compared under (design §5.2, §6.1, §6.2).
4
+ *
5
+ * A citation names a source as its author wrote it — `HTTPS://Acme.example/
6
+ * policy/#refunds`, `./docs/../docs/pricing.md`, `doi:10.1000/XYZ` — and the
7
+ * run read it as a tool's input spelled it. Grounding is a lookup of one in
8
+ * the other, so both go through the functions here and nothing else.
9
+ *
10
+ * Everything is offline and lexical: no DNS, no realpath of the path being
11
+ * named (a path is never followed off the workspace to be measured), no
12
+ * request. The cited rung's checks that need the filesystem or the network
13
+ * live in the verifier, behind injected seams.
14
+ */
15
+ import { realpathSync } from "node:fs";
16
+ import * as path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { isPrivateIp, normalizeIpv4 } from "./private-address";
19
+ /** Longer than any path a filesystem opens (tool-verify's `MAX_PATH_CHARS` reasoning). */
20
+ const MAX_PATH_CHARS = 8_192;
21
+ /** The DOI resolver's hosts, folded to one. */
22
+ const DOI_HOSTS = new Set(["doi.org", "dx.doi.org", "www.doi.org"]);
23
+ /**
24
+ * The comparable form of an `http(s)` URL, or `undefined` when `raw` is not
25
+ * one: scheme and host lowercased, a trailing dot on the host dropped, the
26
+ * default port dropped, credentials and the fragment dropped (a fragment
27
+ * names a place in the page, not another page, and credentials are never
28
+ * written into a bundle), a trailing slash folded (`/a/` is `/a`; the root
29
+ * stays `/`), the query kept (`?id=1` and `?id=2` are different pages).
30
+ *
31
+ * The DOI resolver is special-cased: `http://dx.doi.org/10.1000/ABC` and
32
+ * `https://doi.org/10.1000/abc` are one origin, because DOIs are
33
+ * case-insensitive by definition (the DOI Handbook, §2.4) and every
34
+ * resolver host answers the same.
35
+ *
36
+ * `http` and `https` stay distinct: a page fetched over one is not evidence
37
+ * that the other says the same thing, and folding them would let a citation
38
+ * ground in a body the run never read under that name.
39
+ */
40
+ export function normalizeUrl(raw) {
41
+ const trimmed = raw.trim();
42
+ if (!/^https?:\/\//i.test(trimmed))
43
+ return undefined;
44
+ let url;
45
+ try {
46
+ url = new URL(trimmed);
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ if (url.protocol !== "http:" && url.protocol !== "https:")
52
+ return undefined;
53
+ const host = url.hostname.replace(/\.$/, "");
54
+ if (host === "")
55
+ return undefined;
56
+ let pathname = url.pathname;
57
+ if (pathname.length > 1) {
58
+ // Walked in from the end: `/\/+$/` restarts at every slash of a long
59
+ // run that does not end the path — quadratic in a URL the model wrote.
60
+ let end = pathname.length;
61
+ while (end > 0 && pathname.charCodeAt(end - 1) === 0x2f)
62
+ end -= 1;
63
+ pathname = end === 0 ? "/" : pathname.slice(0, end);
64
+ }
65
+ if (DOI_HOSTS.has(host))
66
+ return `https://doi.org${pathname.toLowerCase()}${url.search}`;
67
+ const port = url.port === "" ? "" : `:${url.port}`;
68
+ return `${url.protocol}//${host}${port}${pathname}${url.search}`;
69
+ }
70
+ /**
71
+ * Does `raw` carry userinfo before its host — `https://acme.example@evil.example/
72
+ * policy`, `https://user:pw@host/`? {@link normalizeUrl} drops it, so the
73
+ * origin is the host after the `@`; a reader takes what comes before it for
74
+ * the host. A citation spelled so names one site to the reader and another
75
+ * to the gate (design §2: `cited` is the source the reader can see), and no
76
+ * citation has a use for credentials. Only an `http(s)` URL that parses has
77
+ * any.
78
+ */
79
+ export function hasUserinfo(raw) {
80
+ const trimmed = raw.trim();
81
+ if (!/^https?:\/\//i.test(trimmed))
82
+ return false;
83
+ try {
84
+ const url = new URL(trimmed);
85
+ return url.username !== "" || url.password !== "";
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ /** The resolver URL a DOI is fetched and grounded under: `https://doi.org/<doi>`, normalized. */
92
+ export function doiUrl(doi) {
93
+ const bare = doi.trim().replace(/^doi:\s*/i, "");
94
+ return normalizeUrl(`https://doi.org/${bare}`) ?? `https://doi.org/${bare.toLowerCase()}`;
95
+ }
96
+ /** The host of a normalized URL (IPv6 without its brackets), or `undefined`. */
97
+ export function hostOf(url) {
98
+ try {
99
+ const host = new URL(url).hostname.replace(/\.$/, "").toLowerCase();
100
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
101
+ }
102
+ catch {
103
+ return undefined;
104
+ }
105
+ }
106
+ /** Real paths of workspace roots, so `/var/…` and `/private/var/…` both resolve
107
+ * inside a root given as either. Only the ROOT is realpath'd, never the
108
+ * path being named. */
109
+ const realRoots = new Map();
110
+ function realRoot(root) {
111
+ let real = realRoots.get(root);
112
+ if (real === undefined) {
113
+ try {
114
+ real = realpathSync(root);
115
+ }
116
+ catch {
117
+ real = null;
118
+ }
119
+ realRoots.set(root, real);
120
+ }
121
+ return real ?? undefined;
122
+ }
123
+ function inside(root, abs) {
124
+ if (abs === root)
125
+ return ".";
126
+ const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
127
+ if (!abs.startsWith(prefix))
128
+ return undefined;
129
+ return path.relative(root, abs).split(path.sep).join("/");
130
+ }
131
+ /**
132
+ * A workspace path as its workspace-relative POSIX form (`docs/pricing.md`),
133
+ * or `undefined` when it names nothing inside the workspace.
134
+ *
135
+ * Lexical, the first half of tool-verify's `resolveSafe`: the path is
136
+ * resolved against `baseDir` and must land inside the workspace root, `..`
137
+ * and absolute escapes refused. It is NOT realpath'd — the key a citation and
138
+ * a `Read` are compared under is the name, and following a link off the
139
+ * workspace to measure it is exactly what this must not do. The verifier's
140
+ * `cited` check does the symlink-aware half (`resolveContained`) before it
141
+ * calls a path a source. A `file://` URL is read as the path it names; any
142
+ * other scheme is not a path.
143
+ */
144
+ export function normalizePath(raw, roots = {}) {
145
+ let given = raw.trim();
146
+ if (given === "" || given.includes("\0") || given.length > MAX_PATH_CHARS)
147
+ return undefined;
148
+ if (/^file:\/\//i.test(given)) {
149
+ try {
150
+ given = fileURLToPath(given);
151
+ }
152
+ catch {
153
+ return undefined;
154
+ }
155
+ }
156
+ else if (/^[A-Za-z][A-Za-z0-9+.-]+:/.test(given) && !/^[A-Za-z]:[\\/]/.test(given)) {
157
+ // `https:`, `mailto:`, `tool:` — a scheme, not a path (a Windows drive
158
+ // letter, `C:\`, is a path).
159
+ return undefined;
160
+ }
161
+ const root = path.resolve(roots.workspaceRoot ?? process.cwd());
162
+ const base = path.resolve(root, roots.baseDir ?? ".");
163
+ const abs = path.resolve(base, given);
164
+ const lexical = inside(root, abs);
165
+ if (lexical !== undefined)
166
+ return lexical;
167
+ const real = realRoot(root);
168
+ return real !== undefined && real !== root ? inside(real, abs) : undefined;
169
+ }
170
+ /**
171
+ * The origin a citation's evidence reference is looked up under: the
172
+ * normalized URL, the DOI's resolver URL, or the workspace-relative path.
173
+ * Tool, proof, assumption and free-text references have none.
174
+ */
175
+ export function originKey(ref, roots = {}) {
176
+ switch (ref.kind) {
177
+ case "url":
178
+ return normalizeUrl(ref.url);
179
+ case "doi":
180
+ return doiUrl(ref.doi);
181
+ case "path":
182
+ return normalizePath(ref.path, roots);
183
+ default:
184
+ return undefined;
185
+ }
186
+ }
187
+ /**
188
+ * Two-level public suffixes, for {@link registrableDomain}.
189
+ *
190
+ * A pragmatic eTLD+1, not the Public Suffix List: the common ICANN
191
+ * second-level registries of the countries that use them (`co.uk`,
192
+ * `com.au`, `co.jp`, …). A host under one keeps three labels; every other
193
+ * host keeps two. PRIVATE suffixes (`github.io`, `blogspot.com`,
194
+ * `herokuapp.com`) are deliberately absent, so every `*.github.io` page is
195
+ * one origin: `grounded.independent` asks for corroboration, and the error
196
+ * this approximation can make is to ask for one source too many — never to
197
+ * count two pages one party controls as two parties.
198
+ */
199
+ const TWO_LEVEL_SUFFIXES = new Set([
200
+ "ac.uk co.uk gov.uk ltd.uk me.uk net.uk nhs.uk org.uk plc.uk police.uk sch.uk",
201
+ "asn.au com.au edu.au gov.au id.au net.au org.au",
202
+ "ac.nz co.nz geek.nz govt.nz net.nz org.nz school.nz",
203
+ "ac.jp ad.jp co.jp ed.jp go.jp gr.jp lg.jp ne.jp or.jp",
204
+ "ac.kr co.kr go.kr ne.kr or.kr re.kr",
205
+ "com.br edu.br gov.br net.br org.br",
206
+ "ac.cn com.cn edu.cn gov.cn net.cn org.cn",
207
+ "com.hk edu.hk gov.hk net.hk org.hk",
208
+ "com.tw edu.tw gov.tw net.tw org.tw",
209
+ "com.sg edu.sg gov.sg net.sg org.sg",
210
+ "ac.in co.in edu.in gov.in net.in nic.in org.in res.in",
211
+ "ac.za co.za gov.za net.za org.za",
212
+ "com.mx edu.mx gob.mx net.mx org.mx",
213
+ "com.ar edu.ar gob.ar net.ar org.ar",
214
+ "com.tr edu.tr gov.tr net.tr org.tr",
215
+ "ac.il co.il gov.il net.il org.il",
216
+ "com.my edu.my gov.my net.my org.my",
217
+ "ac.id co.id go.id or.id",
218
+ "ac.th co.th go.th or.th",
219
+ "com.ph edu.ph gov.ph net.ph org.ph",
220
+ "com.vn edu.vn gov.vn net.vn org.vn",
221
+ "com.pk edu.pk gov.pk net.pk org.pk",
222
+ "com.ng edu.ng gov.ng net.ng org.ng",
223
+ "ac.ke co.ke go.ke or.ke",
224
+ "com.eg edu.eg gov.eg net.eg org.eg",
225
+ "com.sa edu.sa gov.sa net.sa org.sa",
226
+ "com.ua edu.ua gov.ua net.ua org.ua",
227
+ "com.pl edu.pl gov.pl net.pl org.pl",
228
+ "ac.at co.at gv.at or.at",
229
+ "com.es edu.es gob.es org.es",
230
+ "com.pt edu.pt gov.pt org.pt",
231
+ "com.gr edu.gr gov.gr org.gr",
232
+ "com.ru edu.ru gov.ru net.ru org.ru",
233
+ "com.cy gov.cy org.cy",
234
+ "com.co edu.co gov.co net.co org.co",
235
+ "com.pe edu.pe gob.pe org.pe",
236
+ "com.ve gob.ve org.ve",
237
+ "co.ve com.ec gob.ec edu.ec",
238
+ ]
239
+ .join(" ")
240
+ .split(" "));
241
+ /**
242
+ * The registrable domain of a host — `docs.acme.example` → `acme.example`,
243
+ * `www.bbc.co.uk` → `bbc.co.uk` — for `grounded.independent`'s "distinct
244
+ * origins". An IP literal is its own domain; a single label is itself. See
245
+ * {@link TWO_LEVEL_SUFFIXES} for what this approximation knows.
246
+ */
247
+ export function registrableDomain(host) {
248
+ let h = host.trim().toLowerCase().replace(/\.$/, "");
249
+ if (h.startsWith("[") && h.endsWith("]"))
250
+ h = h.slice(1, -1);
251
+ if (h.includes(":") || normalizeIpv4(h) !== null)
252
+ return h;
253
+ const labels = h.split(".").filter((label) => label !== "");
254
+ if (labels.length <= 2)
255
+ return labels.join(".");
256
+ const lastTwo = labels.slice(-2).join(".");
257
+ return TWO_LEVEL_SUFFIXES.has(lastTwo) ? labels.slice(-3).join(".") : lastTwo;
258
+ }
259
+ /**
260
+ * Does `host` match any of `globs` (`sources.allow` / `sources.deny`)? The
261
+ * IR's grammar: `*` (any host), `*.<host>` (any host BELOW it — `*.gov`
262
+ * matches `nasa.gov` and `www.nasa.gov`, not `gov`), or an exact host.
263
+ * Case-insensitive; a trailing dot on the host is ignored.
264
+ */
265
+ export function hostMatches(host, globs) {
266
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
267
+ for (const raw of globs) {
268
+ const glob = raw.trim().toLowerCase();
269
+ if (glob === "*")
270
+ return true;
271
+ if (glob.startsWith("*.")) {
272
+ const suffix = glob.slice(1);
273
+ if (h.length > suffix.length && h.endsWith(suffix))
274
+ return true;
275
+ }
276
+ else if (glob !== "" && h === glob) {
277
+ return true;
278
+ }
279
+ }
280
+ return false;
281
+ }
282
+ /**
283
+ * Is `host` loopback, private, link-local or otherwise not a public source?
284
+ * The repo's one IP classifier (`private-address.ts`, every spelling of
285
+ * `169.254.169.254` included) plus the names tool-fetch refuses before DNS:
286
+ * `localhost`, `*.localhost` and the mDNS `*.local`. Offline — a public-looking
287
+ * name that RESOLVES privately is caught by the fetch that reads it, not here.
288
+ */
289
+ export function isPrivateHost(host) {
290
+ let h = host.trim().toLowerCase().replace(/\.$/, "");
291
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local"))
292
+ return true;
293
+ if (h.startsWith("[") && h.endsWith("]"))
294
+ h = h.slice(1, -1);
295
+ return isPrivateIp(h);
296
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The private-address classifier, for the `cited` rung's `private_address`
3
+ * check (design §6.1): a citation of `http://169.254.169.254/` is not a
4
+ * source, however well-formed it is.
5
+ *
6
+ * The block below is the repo's ONE classifier, copied byte for byte from
7
+ * `@crewhaus/tool-fetch` — `apps/cli/src/tool-registry.test.ts` lists this
8
+ * package among the copies it sweeps, proves every copy identical and
9
+ * EXECUTES one. Never edit it here: change every copy at once, or none.
10
+ */
11
+ /**
12
+ * Canonicalise an IPv4 literal.
13
+ *
14
+ * `inet_aton` forms are the classic allow-list bypass: `0177.0.0.1`,
15
+ * `0x7f.0.0.1`, `2130706433` and `127.1` are all 127.0.0.1, and a check that
16
+ * only understands dotted-decimal waves every one of them through. Returns
17
+ * `null` when the string is not an IPv4 literal at all.
18
+ */
19
+ export declare function normalizeIpv4(raw: string): string | null;
20
+ /**
21
+ * Expand an IPv6 literal to its eight 16-bit groups, or `null` when the string
22
+ * is not one.
23
+ *
24
+ * Classifying IPv6 by its TEXT is where the bypasses live, because one address
25
+ * has many spellings and the one a check was written against is rarely the one
26
+ * that arrives. `http://[::ffff:169.254.169.254]/` never reaches a guard in
27
+ * that form: the WHATWG URL parser re-serialises the embedded quad as hex
28
+ * pieces, so what the guard sees is `::ffff:a9fe:a9fe`. Normalising to numbers
29
+ * first means the prefix tests below are arithmetic, and spelling stops
30
+ * mattering.
31
+ */
32
+ export declare function parseIpv6(raw: string): ReadonlyArray<number> | null;
33
+ /** Ranges that are never a public RPC endpoint. */
34
+ export declare function isPrivateIp(address: string): boolean;