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 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.3.21 --ignore-scripts && npm audit signatures
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.3.21` must point at that same commit, and `gh api repos/erekola/llms-txt-validator/commits/v0.3.21 --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.
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
 
@@ -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.mjs` (`NAMED_REFERENCE_DATA`) is generated from data
5
- published by a third-party package and carries that package's own license
6
- below. This is a build-time data source, not an installed runtime dependency:
7
- the table is generated once and copied into `src/index.mjs`, so no reference
8
- to it appears in `package.json` or in the dependency tree an installer sees.
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
 
@@ -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>;