turva-llms-txt-validator 0.3.21 → 0.4.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.
- package/README.md +4 -2
- package/THIRD-PARTY-NOTICES.md +6 -5
- package/dist/bin/cli.js +103 -0
- package/dist/src/index.d.ts +59 -0
- package/dist/src/index.js +2257 -0
- package/package.json +21 -7
- package/bin/cli.mjs +0 -95
- package/src/index.mjs +0 -1880
package/README.md
CHANGED
|
@@ -108,6 +108,8 @@ for (const check of result.checks) {
|
|
|
108
108
|
|
|
109
109
|
`validateHost` rejects on invalid input or a failed `/llms.txt` fetch. Structural failures are returned in `checks` with the summary `not valid`.
|
|
110
110
|
|
|
111
|
+
The package is written in TypeScript and ships its type declarations.
|
|
112
|
+
|
|
111
113
|
## Use in CI
|
|
112
114
|
|
|
113
115
|
Add this step to a GitHub Actions job that already has Node.js 22 or newer available. Replace `your-domain.com` with your domain:
|
|
@@ -137,10 +139,10 @@ For vulnerability reporting and supported versions, see [SECURITY.md](https://gi
|
|
|
137
139
|
GitHub Actions publishes every version from 0.1.3 on with npm trusted publishing, and each one carries a provenance attestation. To check one, install it in an empty directory and ask npm to verify the signatures. Replace the version with the one you want to check.
|
|
138
140
|
|
|
139
141
|
```sh
|
|
140
|
-
mkdir verify-llms && cd verify-llms && npm init -y && npm install turva-llms-txt-validator@0.
|
|
142
|
+
mkdir verify-llms && cd verify-llms && npm init -y && npm install turva-llms-txt-validator@0.4.0 --ignore-scripts && npm audit signatures
|
|
141
143
|
```
|
|
142
144
|
|
|
143
|
-
`npm audit signatures` checks the registry signature and the provenance attestation of each installed package that has one. The attestation of this package names three things to compare with what you expect: the repository `github.com/erekola/llms-txt-validator`, the workflow file `.github/workflows/publish.yml` and the commit that produced the tarball. The npm version page shows them under Provenance. Tag `v0.
|
|
145
|
+
`npm audit signatures` checks the registry signature and the provenance attestation of each installed package that has one. The attestation of this package names three things to compare with what you expect: the repository `github.com/erekola/llms-txt-validator`, the workflow file `.github/workflows/publish.yml` and the commit that produced the tarball. The npm version page shows them under Provenance. Tag `v0.4.0` must point at that same commit, and `gh api repos/erekola/llms-txt-validator/commits/v0.4.0 --jq .sha` prints it. A mismatch is a reason not to use that version. Provenance proves where a release was built and from which commit. It does not prove that the code is safe, so reading the source and the dependencies stays your job.
|
|
144
146
|
|
|
145
147
|
## Hosted version and source
|
|
146
148
|
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
3
|
This package's own license is MIT, in `LICENSE`. The named character reference
|
|
4
|
-
table in `src/index.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
table `NAMED_REFERENCE_DATA` in `src/index.ts` is generated from data published
|
|
5
|
+
by a third-party package and carries that package's own license below. It is
|
|
6
|
+
compiled into `dist/src/index.js` with the rest of the source. This is a
|
|
7
|
+
build-time data source, not an installed runtime dependency: the table is
|
|
8
|
+
generated once and copied into `src/index.ts`, so no reference to it appears in
|
|
9
|
+
`package.json` or in the dependency tree an installer sees.
|
|
9
10
|
|
|
10
11
|
## character-entities
|
|
11
12
|
|
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI for turva-llms-txt-validator. Exit codes: 0 = valid (or valid with
|
|
3
|
+
// warnings), 1 = not valid (or warnings with --strict), 2 = could not fetch
|
|
4
|
+
// or bad input. The two v2 discovery checks and the input-path note carry status
|
|
5
|
+
// "info" and move no exit code, by design. Same checks as the hosted validator, and the same
|
|
6
|
+
// JSON shape for a completed validation:
|
|
7
|
+
// curl -H "Accept: application/json" "https://turva.dev/llms-txt-validator?url=example.com"
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { validateHost, stripControls, maskLocation } from "../src/index.js";
|
|
11
|
+
const USAGE = [
|
|
12
|
+
"usage: llms-txt-validate <domain-or-url> [--json] [--strict]",
|
|
13
|
+
" --json print the result as JSON (same shape as the hosted validator for a completed validation)",
|
|
14
|
+
" --strict exit 1 on warnings too, for CI gates",
|
|
15
|
+
" --help print this help",
|
|
16
|
+
" --version print the installed version and exit"
|
|
17
|
+
];
|
|
18
|
+
const KNOWN = new Set(["--json", "--strict", "--help", "-h", "--version", "-V"]);
|
|
19
|
+
// The arguments are read strictly since 0.3.8 (Tek-496). An unknown flag such as a misspelled
|
|
20
|
+
// --strcit used to be ignored, so a CI step ran without the gate it asked for, and a second
|
|
21
|
+
// target was dropped without a word. Both now stop with exit 2, and every input error is JSON
|
|
22
|
+
// when --json was given, the missing target included.
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const json = args.includes("--json");
|
|
25
|
+
const flags = args.filter((a) => a.startsWith("-") && a !== "-");
|
|
26
|
+
const targets = args.filter((a) => !(a.startsWith("-") && a !== "-"));
|
|
27
|
+
function fail(message) {
|
|
28
|
+
if (json)
|
|
29
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
30
|
+
else {
|
|
31
|
+
console.error("error: " + message);
|
|
32
|
+
console.error(USAGE[0]);
|
|
33
|
+
}
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
37
|
+
for (const line of USAGE)
|
|
38
|
+
console.log(line);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if (flags.includes("--version") || flags.includes("-V")) {
|
|
42
|
+
// Read package.json relative to this file, not the caller's cwd, so this works the same
|
|
43
|
+
// whether the CLI runs installed, linked or through npx (V-04, outside audit 2026-09-26).
|
|
44
|
+
// No network read: the version is the one actually installed, not the npm registry's latest.
|
|
45
|
+
const pkgUrl = new URL("../../package.json", import.meta.url);
|
|
46
|
+
const pkg = JSON.parse(readFileSync(fileURLToPath(pkgUrl), "utf8"));
|
|
47
|
+
console.log(pkg.version);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
const unknown = flags.find((f) => !KNOWN.has(f));
|
|
51
|
+
if (unknown)
|
|
52
|
+
fail("unknown option " + JSON.stringify(stripControls(unknown).slice(0, 40)));
|
|
53
|
+
if (targets.length === 0 || targets[0].trim() === "")
|
|
54
|
+
fail("no domain or URL given");
|
|
55
|
+
if (targets.length > 1)
|
|
56
|
+
fail("one domain or URL at a time, got " + targets.length);
|
|
57
|
+
const mark = { pass: "ok ", warn: "warn", fail: "FAIL", info: "info" };
|
|
58
|
+
try {
|
|
59
|
+
const result = await validateHost(targets[0]);
|
|
60
|
+
if (json) {
|
|
61
|
+
console.log(JSON.stringify(result, null, 2));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Details reach this point without control characters (validateHost strips them), so a
|
|
65
|
+
// published link cannot write an escape sequence to the terminal. A check's reflected
|
|
66
|
+
// value, when it has one, is printed ahead of the sentence, the same position the old
|
|
67
|
+
// concatenated string put it in, so text output reads the same while the JSON field stays
|
|
68
|
+
// separate (round 3 Tek-542, mirrored from worker.js decision 19, Tek-526).
|
|
69
|
+
console.log(result.target);
|
|
70
|
+
for (const c of result.checks)
|
|
71
|
+
console.log(" " + mark[c.status] + " " + c.label + " (" + (c.value !== undefined ? c.value + " " : "") + c.detail + ")");
|
|
72
|
+
console.log("result: " + result.summary);
|
|
73
|
+
}
|
|
74
|
+
if (result.summary === "not valid")
|
|
75
|
+
process.exit(1);
|
|
76
|
+
if (result.summary === "valid with warnings" && flags.includes("--strict"))
|
|
77
|
+
process.exit(1);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const message = stripControls(err && err.message ? err.message : String(err));
|
|
82
|
+
// The hosted validator answers every failure as JSON when JSON was asked for, so a CI
|
|
83
|
+
// step that parses --json output gets {"error": ...} here too instead of an empty stdout
|
|
84
|
+
// and a plain-text stderr (round 16 S3-3). The exit code is unchanged.
|
|
85
|
+
//
|
|
86
|
+
// target and code are added as separate fields (V-05, outside audit 2026-09-26): a bare
|
|
87
|
+
// {"error": message} left a CI step unable to tell which target failed or why without
|
|
88
|
+
// parsing free text. code reads a network error's cause first (undici wraps the real
|
|
89
|
+
// errno code, such as EAI_AGAIN or ECONNREFUSED, in err.cause.code) and falls back to
|
|
90
|
+
// err.code for an error that sets it directly; it is omitted, not null, when neither is set.
|
|
91
|
+
// target goes through maskLocation like every other address the tool prints, so a user name,
|
|
92
|
+
// a password or a query value typed into the command line is never echoed back.
|
|
93
|
+
const code = (err && err.cause && err.cause.code) || (err && err.code);
|
|
94
|
+
if (json) {
|
|
95
|
+
const report = { error: message, target: stripControls(maskLocation(targets[0])) };
|
|
96
|
+
if (code)
|
|
97
|
+
report.code = stripControls(String(code));
|
|
98
|
+
console.log(JSON.stringify(report, null, 2));
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
console.error("error: " + message);
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type CheckStatus = "pass" | "warn" | "fail" | "info";
|
|
2
|
+
export type LlmsCheck = {
|
|
3
|
+
id: string;
|
|
4
|
+
status: CheckStatus;
|
|
5
|
+
label: string;
|
|
6
|
+
detail: string;
|
|
7
|
+
value?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ChecksSummary = "not valid" | "valid with warnings" | "valid";
|
|
10
|
+
export type ValidationResult = {
|
|
11
|
+
target: string;
|
|
12
|
+
summary: ChecksSummary;
|
|
13
|
+
checks: LlmsCheck[];
|
|
14
|
+
};
|
|
15
|
+
export type LinkRelations = {
|
|
16
|
+
describedby: string | null;
|
|
17
|
+
markdown: string | null;
|
|
18
|
+
headCut?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type FetchedLlmsTxt = {
|
|
21
|
+
redirect?: undefined;
|
|
22
|
+
status: number;
|
|
23
|
+
contentType: string;
|
|
24
|
+
linkHeader: string;
|
|
25
|
+
text: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
redirectedFrom: string | null;
|
|
29
|
+
finalUrl: string;
|
|
30
|
+
};
|
|
31
|
+
export type FetchRedirect = {
|
|
32
|
+
redirect: true;
|
|
33
|
+
reason: "no-location" | "too-many" | "bad-location" | "unsafe-target" | "off-host";
|
|
34
|
+
status: number;
|
|
35
|
+
location: string;
|
|
36
|
+
};
|
|
37
|
+
export type ValidateHostOptions = {
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
cap?: number;
|
|
40
|
+
userAgent?: string;
|
|
41
|
+
path?: string;
|
|
42
|
+
accept?: string;
|
|
43
|
+
};
|
|
44
|
+
export type FetchOptions = ValidateHostOptions;
|
|
45
|
+
export type LlmsTxtInput = FetchRedirect | (Partial<FetchedLlmsTxt> & Pick<FetchedLlmsTxt, "status" | "contentType" | "text" | "bytes" | "truncated">);
|
|
46
|
+
export declare const VERSION: string;
|
|
47
|
+
export declare function cut(s: string | number, n: number): string;
|
|
48
|
+
export declare function maskLocation(href: string, base?: string): string;
|
|
49
|
+
export declare function stripBidi(s: unknown): string;
|
|
50
|
+
export declare function stripControls(s: unknown): string;
|
|
51
|
+
export declare function normalizeHostInput(raw: unknown): string | null;
|
|
52
|
+
export declare function enteredPath(raw: unknown): string;
|
|
53
|
+
export declare function isValidPublicHost(host: string | null | undefined): boolean;
|
|
54
|
+
export declare function fetchLlmsTxt(host: string, opts?: FetchOptions): Promise<FetchedLlmsTxt | FetchRedirect>;
|
|
55
|
+
export declare function validateLlmsTxt(f: LlmsTxtInput): LlmsCheck[];
|
|
56
|
+
export declare function findLinkRelations(html: string, linkHeader: string): LinkRelations;
|
|
57
|
+
export declare function validateV2Discovery(found: LinkRelations | null, unreadReason?: string): LlmsCheck[];
|
|
58
|
+
export declare function summarizeChecks(checks: LlmsCheck[]): ChecksSummary;
|
|
59
|
+
export declare function validateHost(input: unknown, opts?: ValidateHostOptions): Promise<ValidationResult>;
|