@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.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +300 -2
- package/dist/artifact.d.ts +9 -0
- package/dist/artifact.js +38 -0
- package/dist/builtins.d.ts +32 -0
- package/dist/builtins.js +46 -0
- package/dist/evidence.d.ts +326 -0
- package/dist/evidence.js +1391 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/levels.d.ts +19 -0
- package/dist/levels.js +42 -0
- package/dist/origin.d.ts +83 -0
- package/dist/origin.js +296 -0
- package/dist/private-address.d.ts +34 -0
- package/dist/private-address.js +209 -0
- package/dist/purity.d.ts +18 -0
- package/dist/purity.js +15 -0
- package/dist/render.d.ts +47 -0
- package/dist/render.js +315 -0
- package/dist/store.d.ts +59 -0
- package/dist/store.js +319 -0
- package/dist/summary.d.ts +8 -0
- package/dist/summary.js +22 -0
- package/dist/types.d.ts +459 -0
- package/dist/types.js +2 -0
- package/dist/vacuous.d.ts +50 -0
- package/dist/vacuous.js +423 -0
- package/dist/verify.d.ts +176 -0
- package/dist/verify.js +3608 -0
- package/package.json +47 -4
package/dist/index.d.ts
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 { 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";
|
package/dist/levels.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/origin.d.ts
ADDED
|
@@ -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;
|