trooth 0.4.2 → 0.4.3

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 (3) hide show
  1. package/README.md +124 -93
  2. package/bin/trooth.mjs +250 -61
  3. package/package.json +3 -5
package/README.md CHANGED
@@ -1,59 +1,56 @@
1
1
  # trooth
2
2
 
3
- The Trooth Network from your terminal.
3
+ The Trooth Network from your terminal. Published on npm as **`trooth`**.
4
4
 
5
- The Trooth Network gives every company one public, signed record: what it is,
6
- what it sells, who runs it, where its data lives, how it handles AI, and what a
7
- buyer needs before signing. DNS tells a machine where a company is. A TLS
8
- certificate tells it the connection is authentic. Neither says anything about
9
- the company itself. The Trooth Network is that layer, and this CLI reads it.
5
+ Trooth operates the Trooth Network: one public, signed, machine-readable record per company, carrying its identity, products and demos, commercial terms, domain and marketing links, people, documents, security and privacy posture, AI practices, procurement terms and relationships. It is Trooth's only product and it is free.
10
6
 
11
- **`check` reads only public, already-published records. No key, no account.**
12
- **`lint` is entirely local: it opens files and opens no sockets.**
13
- **Trooth publishes facts and counts, never one number that sums a company up.**
7
+ DNS says where a company is. A TLS certificate says the connection is authentic. The Trooth Network says who the company is and what it does with your data.
14
8
 
15
- ## Install / run
9
+ This CLI is the terminal interface to that record. It does two things:
10
+
11
+ - `trooth check <domain>` reads a company's published record from the public Network. No key, no account, and nothing about you is sent beyond what any web request carries (your IP address and a `trooth-cli/<version>` user-agent).
12
+ - `trooth lint [path]` reads what your own infrastructure declares and prints those declarations as facts. Entirely local: it opens files and opens no sockets.
13
+
14
+ **Trooth witnesses and dates facts. It does not grade, rate or rank anyone.** The CLI prints counts, reported apart, and never adds them into one number.
15
+
16
+ ## Install
16
17
 
17
18
  ```bash
18
- # No install needed:
19
+ # No install:
19
20
  npx trooth check stripe.com
20
21
 
21
- # or install it:
22
- npm i -g trooth
23
- trooth check trooth.co
22
+ # Or install it:
23
+ npm install -g trooth
24
+ trooth --version
24
25
  ```
25
26
 
26
- Requires Node 18+ (uses built-in `fetch`). Zero dependencies.
27
+ Node 18 or newer, because the binary uses the built-in `fetch`. Zero dependencies.
27
28
 
28
29
  ## Commands
29
30
 
30
31
  | Command | What it does |
31
- | --- | --- |
32
- | `trooth check <domain>` | Reads a company's record from the live Trooth Network (`GET https://api.trooth.co/directory/api/vendors`) and prints whether it is listed, when it was witnessed and first published, the live-probe and attestation counts, the badge id, and the signature and key id you can verify. Prints an honest "not listed yet" when a company has no published record. |
33
- | `trooth lint [path]` | Reads the infrastructure the given directory **declares** and prints those declarations as facts, plus a canonical SHA-256 digest of them. Entirely local and offline. |
34
- | `trooth --help` / `--version` | Help / version. |
32
+ |---|---|
33
+ | `trooth check <domain>` | Reads a company's record from the live Network and prints whether it is listed, when it was witnessed and first published, the live-probe and attestation counts, the badge id, the id of the key that signed the record, and the first three events in its ledger. `--json` adds the signature itself. |
34
+ | `trooth lint [path]` | Reads the infrastructure the given directory declares and prints those declarations plus a canonical SHA-256 digest of them. Local and offline. `path` defaults to `.`. |
35
+ | `trooth --help` | Help. Also `-h` and `help`. |
36
+ | `trooth --version` | Version. Also `-v` and `version`. |
35
37
 
36
38
  ## Flags
37
39
 
38
- - `--json`: stdout carries exactly one JSON document and nothing else; every
39
- diagnostic goes to stderr. On an error the document is
40
- `{"ok": false, "error": "...", "exit": N}`.
40
+ `--json` is the only flag `check` and `lint` take. With it, stdout carries exactly one JSON document and nothing else, and every diagnostic goes to stderr. On an error the document is `{"ok": false, "error": "...", "exit": N}`; a non-2xx response adds `http_status`, and `lint` with nothing to read adds `files_opened`. `--help` and `--version` print plain text whether or not `--json` is given.
41
41
 
42
- Any other flag is a usage error (exit 2) that names the flag and lists the
43
- known ones.
42
+ Any other flag is a usage error. The message names the flag, and for a `--` flag given to `check` or `lint` it also lists the ones that exist.
44
43
 
45
44
  ## Exit codes
46
45
 
47
46
  | Code | Meaning |
48
- | --- | --- |
49
- | 0 | ok: company listed; lint read at least one declaration; help / version |
50
- | 1 | finding: company not listed; lint found nothing to read |
51
- | 2 | usage error: missing argument, unknown flag or command, unreadable path |
52
- | 3 | Trooth unreachable, non-2xx response, or malformed response |
47
+ |---|---|
48
+ | 0 | The company is listed, or `lint` read at least one declaration, or you asked for help or the version. |
49
+ | 1 | The company is not listed, or `lint` found nothing to read. |
50
+ | 2 | Usage error: a missing argument, an unknown flag or command, or a path that does not exist. |
51
+ | 3 | Trooth could not be read: unreachable, a non-2xx response, or a response that is not JSON. An unexpected failure inside the CLI also exits 3. |
53
52
 
54
- A company that is not listed and a network that could not be read are different
55
- answers and they exit differently, so a pipeline never reads a Trooth outage as
56
- "this vendor is unverified".
53
+ A company with no record and a Network that could not be read are different answers, so they exit differently. A pipeline can tell them apart without parsing prose, and a Trooth outage never reads as a company with no record.
57
54
 
58
55
  ## `trooth check`
59
56
 
@@ -61,6 +58,8 @@ answers and they exit differently, so a pipeline never reads a Trooth outage as
61
58
  trooth check trooth.co
62
59
  ```
63
60
 
61
+ Example output. The values are illustrative; the shape is what the binary prints.
62
+
64
63
  ```
65
64
  Trooth Network // witnessed · public · read-only //
66
65
  Trooth, LLC trooth.co
@@ -69,17 +68,24 @@ Standing: listed and witnessed witnessed 2026-08-30 first published 2026-08-
69
68
  Live probes 64/65 Attestations 27/35
70
69
  Probes are checks Trooth read for itself. Attestations are the company's own declarations.
71
70
  They are counts, reported apart on purpose. Trooth never adds them up into one number.
72
- Badge bronze_rw_... Key ed25519-2026-01
71
+ Badge rw_... Key ed25519-2026-01
73
72
 
74
73
  Recent witness events
75
- • 2026-08-30 rewitnessed 64 of 65 live probes re-run
74
+ • 2026-08-01 scan completed 64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid
75
+ • 2026-08-01 standing published point-in-time · published to the Trooth Network
76
+ • 2026-08-30 scan completed 64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid
76
77
 
77
78
  A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.
78
79
  Full record: https://trooth.co/network/trooth.co · Signing keys: https://api.trooth.co/public/keys
79
80
  ```
80
81
 
82
+ The line that begins `Standing:` is the binary's label for the listing state. For a listed company it reads `listed and witnessed`, then the date of the most recent published reading and the date the record was first published. The two counts are live probes (how many returned the expected result, out of how many were read at the last reading) and attestations (how many the company attested, out of how many were asked for).
83
+
84
+ The events section prints the first three entries in the record's ledger. The feed keeps the ledger oldest first, so these are the record's earliest events even though the heading says "Recent"; `--json` carries every event the feed returns. Event types and details are the feed's own wording, printed as received.
85
+
86
+ For scripting:
87
+
81
88
  ```bash
82
- # For scripting: one JSON document on stdout, exit 1 when a company is not listed.
83
89
  trooth check trooth.co --json
84
90
  ```
85
91
 
@@ -90,10 +96,15 @@ trooth check trooth.co --json
90
96
  "company_name": "Trooth, LLC",
91
97
  "witnessed_at": "2026-08-30T00:00:00Z",
92
98
  "first_published_at": "2026-08-01T00:00:00Z",
93
- "badge_id": "bronze_rw_...",
99
+ "badge_id": "rw_...",
94
100
  "probes": { "passed": 64, "total": 65 },
95
101
  "attested": { "passed": 27, "total": 35 },
96
- "events": [ { "type": "rewitnessed", "at": "2026-08-30T00:00:00Z", "detail": "64 of 65 live probes re-run" } ],
102
+ "events": [
103
+ { "type": "scan_completed", "at": "2026-08-01T00:00:00Z", "detail": "64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid" },
104
+ { "type": "standing_published", "at": "2026-08-01T00:00:00Z", "detail": "point-in-time · published to the Trooth Network" },
105
+ { "type": "scan_completed", "at": "2026-08-30T00:00:00Z", "detail": "64 of 65 live probes passed · 27 of 35 declarations recorded · signature valid" },
106
+ { "type": "standing_published", "at": "2026-08-30T00:00:00Z", "detail": "point-in-time · published to the Trooth Network" }
107
+ ],
97
108
  "receipt_signature": "...",
98
109
  "authority_key_id": "ed25519-2026-01",
99
110
  "verify_keys": "https://api.trooth.co/public/keys",
@@ -101,26 +112,27 @@ trooth check trooth.co --json
101
112
  }
102
113
  ```
103
114
 
104
- Those are the only fields `check --json` emits. Anything else the feed happens
105
- to carry is dropped on the way out, so a script written against this shape
106
- keeps working — and a field that would carry a score, tier, grade, rank or
107
- rating is dropped no matter what the feed sends.
115
+ In `probes`, `total` is how many live probes were read at the last reading and `passed` (the API's field name) is how many returned the expected result. In `attested`, `total` is how many declarations were asked for and `passed` is how many the company attested. `witnessed_at` is the date of the most recent published reading. `events` is the whole ledger, oldest first.
116
+
117
+ `category` and `description` are added when the record carries them. Those are the only fields `check --json` emits: anything else the feed happens to carry is dropped on the way out, so a script written against this shape keeps working. A field whose name would carry a score, grade, rank, rating or percentage is dropped no matter what the feed sends.
118
+
119
+ A company with no record exits 1 and emits `{"domain": "...", "listed": false, "record_url": "..."}`. That is not a judgment. It means the Network's public feed carries no record for that domain. A company gets a record at [trooth.co/get-started](https://trooth.co/get-started), free.
120
+
121
+ `receipt_signature` is Trooth's Ed25519 signature and `authority_key_id` names the key that made it; the public keys are listed at [trooth.co/verify/keys](https://trooth.co/verify/keys). You cannot re-run that signature check from this output yet: the exact bytes the signature covers are not published, and this JSON does not carry every field that goes into them. [`trooth-signatures`](https://github.com/troothllc/trooth-signatures) states that gap and what will close it.
108
122
 
109
123
  ## `trooth lint`
110
124
 
111
- `lint` reads what your infrastructure **declares** and reports it. It does not
112
- judge it. There is no verdict, no pass mark, no severity and no score, and
113
- nothing is checked against a named standard or regulation — declaring public
114
- ingress is not a failing, because a load balancer is supposed to be public.
115
- What the facts mean is your call.
125
+ `lint` reads what your infrastructure **declares** and reports it. It does not judge it. There is no verdict, no pass mark, no severity and no score, and nothing is checked against a named standard or regulation. Declaring public ingress is not a failing: a load balancer is supposed to be public. What the facts mean is your decision.
116
126
 
117
127
  ```bash
118
128
  trooth lint ./infra
119
129
  ```
120
130
 
131
+ Example output. The values are illustrative; the shape is what the binary prints.
132
+
121
133
  ```
122
134
  trooth lint // local · offline · declarations only //
123
- ./infra 14 declaration file(s) read
135
+ infra 14 declaration file(s) read
124
136
  terraform 11 · kubernetes 3
125
137
 
126
138
  Declared
@@ -133,74 +145,93 @@ Declared
133
145
  Marked public 2
134
146
  Inline credential literals 0
135
147
 
136
- Digest sha256:a963291330bce2ab…
148
+ Most declared resource types
149
+ 5 aws_s3_bucket
150
+ 3 aws_iam_role
151
+ 2 aws_cloudwatch_log_group
152
+
153
+ Digest sha256:...
154
+ A SHA-256 over the facts above, in canonical form, with the timestamp excluded.
155
+ The same tree always produces the same digest, so you can record it as evidence
156
+ that a state was observed without publishing the tree it came from.
157
+
158
+ Counts of what the files declare. Not a judgement: a public load balancer is
159
+ supposed to be public. Trooth issues no verdict here and checks nothing against
160
+ any standard. Nothing left this machine: lint opens files and opens no sockets.
161
+ Publish what you choose on your record at https://trooth.co/dashboard.
137
162
  ```
138
163
 
139
- It reads `.tf`, `.tf.json`, Kubernetes YAML (anything with `apiVersion` and
140
- `kind`), `terraform show -json` plan files and Dockerfiles. It is a declaration
141
- reader, not a full HCL parser, and it says so rather than pretending otherwise.
164
+ It reads `.tf`, `.tf.json`, Kubernetes YAML (anything carrying both `apiVersion` and `kind`), `terraform show -json` plan files and Dockerfiles. It is a declaration reader, not a full HCL parser: it matches patterns in the text of each file and parses none of them, so a count can differ from what a full parser would find. Its output does not state this limit.
142
165
 
143
- **Nothing leaves the machine.** No file name, no line, no code and no value is
144
- ever printed or transmitted — only counts, resource type names and region
145
- strings. The credential count is a count: it never shows the literal it found.
166
+ Nothing leaves the machine. No file name, no line, no code and no value is printed or transmitted, only the path you gave it, counts, resource type names and region strings. The credential count is a count: the literal it found is never shown.
146
167
 
147
- **The digest** is a SHA-256 over the canonical fact document with the timestamp
148
- excluded, so the same tree always produces the same digest. Record it in CI, or
149
- on your Trooth record, as evidence that a given state was observed — without
150
- publishing the tree it came from.
168
+ The digest is a SHA-256 over the canonical fact document with the timestamp excluded, so the same tree always produces the same digest. Record it in CI, or on your own record, as evidence that a given state was observed, without publishing the tree it came from.
151
169
 
152
170
  ```bash
153
- # In CI: keep the fact document as a build artifact.
154
- trooth lint --json > trooth-attestation.json
171
+ # Keep the fact document as a build artifact.
172
+ trooth lint --json > trooth-lint.json
155
173
  ```
156
174
 
157
- ## Use it from an AI assistant
175
+ ## In GitHub Actions
158
176
 
159
- The same public network powers Trooth's read-only MCP server, so ChatGPT,
160
- Claude, Cursor, or any MCP client can ask about a company in plain words:
177
+ The same `lint`, as a step, is [`troothllc/trooth-action`](https://github.com/troothllc/trooth-action). It is advisory by default: it writes what your infrastructure declares to the job summary and does not fail your workflow over anything it read unless you opt in to one of two gates. It does fail the step when it could not read at all: a path that does not exist, or a CLI it could not install or start.
178
+
179
+ ```yaml
180
+ - uses: troothllc/trooth-action@v1
181
+ with:
182
+ path: ./infra
183
+ ```
184
+
185
+ That repository's README documents the inputs and outputs.
186
+
187
+ ## Environment
188
+
189
+ | Variable | Effect |
190
+ |---|---|
191
+ | `TROOTH_API` | Base URL for `check`. Defaults to `https://api.trooth.co`. `lint` ignores it, because `lint` makes no requests. |
192
+ | `NO_COLOR` | Disables ANSI color. Color is already off when stdout is not a TTY. |
193
+
194
+ There is no API key. The binary asks for no credential of any kind and has no write path.
195
+
196
+ ## From an AI assistant
197
+
198
+ The same public Network powers Trooth's read-only MCP server, so ChatGPT, Claude, Cursor or any MCP client can ask about a company in plain words:
161
199
 
162
200
  ```
163
201
  https://api.trooth.co/public/mcp
164
202
  ```
165
203
 
166
- ## Notes
167
-
168
- - `check` reads only the **public** record. There is no key and no account, and
169
- it never sends anything about you. It is a read.
170
- - A "not listed" result is not a judgement. It means no record has been
171
- published for that domain yet. A company gets one at
172
- <https://trooth.co/get-started>.
173
- - Set `TROOTH_API` to point at a different base URL (defaults to
174
- `https://api.trooth.co`). `lint` ignores it: `lint` makes no requests.
175
- - Set `NO_COLOR=1` to disable ANSI colour. Colour is already off when stdout is
176
- not a TTY.
204
+ Four read-only tools, public data, no key. The pattern is written up at [trooth.co/docs/agents](https://trooth.co/docs/agents).
177
205
 
178
- ## Changes in 0.4.0
206
+ ## Removed in 0.4.0
179
207
 
180
208
  Breaking, and deliberately so.
181
209
 
182
- - **`scan` and `eu` are removed.** Trooth does not scan infrastructure against a
183
- standard, does not issue a verdict and does not offer a compliance product.
184
- Both commands now exit 2 with a sentence saying so, rather than "unknown
185
- command", so an old CI job or an old README gets an explanation.
186
- - **`lint` is real.** In 0.3.0 it shelled out to `@trooth/os`, which was never
187
- published, so the command failed for everyone who ran it. It is now
188
- implemented in this binary, entirely locally.
189
- - **`scan_id` is gone from `check --json`.** Every other field is unchanged.
190
- - **`--strict` is gone**, along with the only command that took it.
191
- - The package description and keywords no longer name any certification,
192
- standard or regulation, because Trooth does not offer one.
210
+ - The `scan` and `eu` commands are gone. Trooth does not check infrastructure against a standard and does not issue a verdict. Running either exits 2 with a sentence saying what happened, rather than "unknown command", so an old CI job or an old bookmark gets an explanation.
211
+ - `lint` is real. In 0.3.0 it shelled out to a package that was never published, so the command failed for everyone who ran it. It is implemented in this binary now, entirely locally.
212
+ - `scan_id` is gone from `check --json`. Every other field is unchanged.
213
+ - `--strict` is gone, along with the only command that took it.
214
+ - The package description and keywords no longer name any certification, standard or regulation, because Trooth does not offer one.
215
+
216
+ The Trooth Network is Trooth's only product. This CLI, the public API and the MCP server are interfaces to that one record, not separate products.
193
217
 
194
- The Trooth Network is Trooth's only product. This CLI, the public API and the
195
- MCP server are interfaces to it, not separate products.
218
+ ## Security
196
219
 
197
- Trooth automates. Trooth never signs for you.
220
+ Report a vulnerability through the [Vulnerability Disclosure Policy](https://trooth.co/security/vulnerability-disclosure-policy). Nothing in this CLI takes a credential, so there is no key to leak from it.
221
+
222
+ ## Links
223
+
224
+ - The Network: [trooth.co/network](https://trooth.co/network)
225
+ - This CLI on the site: [trooth.co/cli](https://trooth.co/cli)
226
+ - API reference: [trooth.co/docs/api](https://trooth.co/docs/api)
227
+ - Developers: [trooth.co/developers](https://trooth.co/developers)
228
+ - Publish your own record, free: [trooth.co/get-started](https://trooth.co/get-started)
229
+ - Contact: [trooth.co/contact](https://trooth.co/contact)
198
230
 
199
231
  ## License
200
232
 
201
233
  Apache License 2.0. See [LICENSE](LICENSE).
202
234
 
203
- The same licence applies to this repository, to the `trooth` package on npm and
204
- to the copyright header in `bin/trooth.mjs`. They are meant to agree; if you
205
- ever find that they do not, that is a defect and we want to hear about it at
206
- https://trooth.co/contact.
235
+ The same license applies to this repository, to the `trooth` package on npm and to the copyright header in `bin/trooth.mjs`. They are meant to agree. If you find that they do not, that is a defect: please report it at [trooth.co/contact](https://trooth.co/contact).
236
+
237
+ Trooth signs what it witnessed. It never signs on a company's behalf.
package/bin/trooth.mjs CHANGED
@@ -4,10 +4,11 @@
4
4
  // Licensed under the Apache License, Version 2.0. See the LICENSE file in this
5
5
  // repository, or http://www.apache.org/licenses/LICENSE-2.0
6
6
  //
7
- // The Trooth Network is one public record per company: identity, products and demos,
8
- // domain and marketing links, people, documents, security and privacy posture,
9
- // procurement terms, relationships and sub-processors — each one witnessed, signed
10
- // and dated. This CLI is the terminal interface to that record.
7
+ // The Trooth Network is one public, signed record per company: identity, products and
8
+ // demos, domain and marketing links, people, documents, security and privacy posture,
9
+ // procurement terms, relationships and sub-processors. Each fact is dated and labeled
10
+ // with where it came from: witnessed, public record, attested or declared. This CLI is
11
+ // the terminal interface to that record.
11
12
  //
12
13
  // Commands:
13
14
  // trooth check <domain> Read a company's witnessed record from the public Trooth
@@ -18,9 +19,9 @@
18
19
  // trooth --help Show help. trooth --version Show version.
19
20
  //
20
21
  // WHAT THIS TOOL DOES NOT DO, ON PURPOSE:
21
- // It does not score, rate, rank, grade or tier a company or a repository.
22
- // It does not check anything against a named certification, standard or regulation.
23
- // It does not produce a verdict, a pass mark or a percentage.
22
+ // It does not grade, rate or rank a company or a repository.
23
+ // It does not check anything against a named standard, framework or regulation.
24
+ // It does not produce a verdict, a threshold result or a percentage.
24
25
  // It publishes facts and counts, reported apart, and never adds them into one number.
25
26
  //
26
27
  // Exit codes (stable, for scripts):
@@ -39,12 +40,12 @@ import { join, relative, extname, basename } from 'node:path';
39
40
 
40
41
  const API = process.env.TROOTH_API || 'https://api.trooth.co';
41
42
  const require = createRequire(import.meta.url);
42
- let VERSION = '0.4.0';
43
+ let VERSION = '0.4.3';
43
44
  try { VERSION = require('../package.json').version; } catch {}
44
45
 
45
46
  const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
46
47
 
47
- // Colour only when stdout is a TTY and NO_COLOR is unset, so piped output is clean.
48
+ // Color only when stdout is a TTY and NO_COLOR is unset, so piped output is clean.
48
49
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
49
50
  const c = (code) => (useColor ? code : '');
50
51
  const J = c('\x1b[32m'), D = c('\x1b[2m'), B = c('\x1b[1m'), R = c('\x1b[31m'), A = c('\x1b[33m'), C = c('\x1b[36m'), X = c('\x1b[0m');
@@ -94,7 +95,7 @@ const FLAGS = {
94
95
  * means an old README, an old CI job or an old blog post gets a sentence that says
95
96
  * what happened, instead of "unknown command". */
96
97
  const RETIRED = {
97
- scan: 'Trooth does not scan infrastructure against a standard and does not issue a verdict. `trooth lint` reads what your infrastructure declares and prints those facts, locally.',
98
+ scan: 'Trooth does not check infrastructure against a standard and does not issue a verdict. `trooth lint` reads what your infrastructure declares and prints those facts, locally.',
98
99
  eu: 'Trooth does not ingest regulation-specific evidence. Publish evidence on your company record at https://trooth.co/dashboard.',
99
100
  preflight: 'Retired. `trooth lint` reads declared infrastructure facts locally instead.',
100
101
  };
@@ -142,7 +143,7 @@ ${B}Examples${X}
142
143
  trooth check stripe.com ${D}# read a company's witnessed record${X}
143
144
  trooth check trooth.co --json ${D}# one JSON document on stdout, for scripting${X}
144
145
  trooth lint ./infra ${D}# read declared facts + print a canonical digest${X}
145
- trooth lint --json >> attestation.json ${D}# the same facts, for a CI artifact${X}
146
+ trooth lint --json > trooth-lint.json ${D}# the same facts as one JSON document, for a CI artifact${X}
146
147
 
147
148
  ${B}Flags${X}
148
149
  --json machine-readable JSON on stdout; diagnostics on stderr
@@ -153,7 +154,7 @@ ${B}Exit codes${X}
153
154
  ${D}check reads only public, already-published records. No key, no account.
154
155
  lint is entirely local: it opens files, and opens no sockets. Your source never leaves.
155
156
  Trooth publishes facts and counts, never one number that sums a company up.
156
- Trooth automates. Trooth never signs for you.${X}
157
+ Trooth signs what it witnessed. It never signs on a company's behalf.${X}
157
158
  `;
158
159
  }
159
160
 
@@ -202,6 +203,37 @@ function count(obj) {
202
203
  return { passed: obj.passed, total: obj.total };
203
204
  }
204
205
 
206
+ /** The two count lines, in the website's form ("65 read; 64 as expected"). */
207
+ function countLines(rec) {
208
+ const lines = [];
209
+ if (rec.probes) lines.push(`${B}Live probes:${X} ${rec.probes.total} read; ${rec.probes.passed} as expected`);
210
+ if (rec.attested) lines.push(`${B}Self-attestations:${X} ${rec.attested.total} asked; ${rec.attested.passed} attested`);
211
+ return lines;
212
+ }
213
+
214
+ /** The feed keeps the ledger oldest first. This returns the `n` most recent
215
+ * events, newest first, ordered by timestamp; events with the same timestamp,
216
+ * or none, keep the feed's order, later entries first. */
217
+ function latestEvents(events, n) {
218
+ const t = (e) => { const v = Date.parse(e.at); return Number.isNaN(v) ? -Infinity : v; };
219
+ return events
220
+ .map((e, i) => ({ e, i }))
221
+ .sort((a, b) => (t(b.e) - t(a.e)) || (b.i - a.i))
222
+ .slice(0, n)
223
+ .map(({ e }) => e);
224
+ }
225
+
226
+ /** Display names for the feed's event types. The type strings themselves are
227
+ * identifiers and are printed unchanged in --json. */
228
+ const EVENT_LABELS = {
229
+ scan_completed: 'reading completed',
230
+ standing_published: 'record published to the Trooth Network',
231
+ rewitnessed: 'live probes re-read',
232
+ };
233
+ function eventLabel(type) {
234
+ return Object.prototype.hasOwnProperty.call(EVENT_LABELS, type) ? EVENT_LABELS[type] : type.replace(/_/g, ' ');
235
+ }
236
+
205
237
  function projectRecord(v, domain) {
206
238
  const events = Array.isArray(v.events)
207
239
  ? v.events.filter((e) => e && e.type).map((e) => ({ type: String(e.type), at: e.at, detail: e.detail }))
@@ -240,11 +272,12 @@ async function check() {
240
272
  if (asJson) {
241
273
  emitJson({ domain, listed: false, record_url: `https://trooth.co/network/${encodeURIComponent(domain)}` });
242
274
  } else {
243
- out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed on the Trooth Network yet${X}`);
244
- out(`\n${D}No witnessed record has been published for this domain. That is not a`);
245
- out(`judgement. It simply has not been witnessed. A company gets a record by`);
246
- out(`listing at ${X}${C}https://trooth.co/get-started${X}${D}: Trooth reads its public surface and`);
247
- out(`publishes a signed, dated record that anyone — or any agent — can check.${X}\n`);
275
+ out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed in the Trooth Network's public feed${X}`);
276
+ out(`\n${D}The public feed carries no record for this domain. That says nothing about the`);
277
+ out(`company: a domain that never listed, a listing Trooth has not published, and a`);
278
+ out(`record that was revoked all read this way. A company gets a record by listing at`);
279
+ out(`${X}${C}https://trooth.co/get-started${X}${D}: Trooth reads its public surface and publishes`);
280
+ out(`a signed, dated record that anyone, or any agent, can read.${X}\n`);
248
281
  }
249
282
  process.exit(EXIT.FINDING);
250
283
  }
@@ -254,19 +287,22 @@ async function check() {
254
287
 
255
288
  const when = fmtDate(rec.witnessed_at);
256
289
  const since = fmtDate(rec.first_published_at);
257
- out(`\n${J}${B}Trooth Network${X} ${D}// witnessed · public · read-only //${X}`);
290
+ out(`\n${J}${B}Trooth Network${X} ${D}// public · signed · read-only //${X}`);
258
291
  out(`${B}${rec.company_name}${X} ${C}${domain}${X}`);
259
- out(`Standing: ${J}listed and witnessed${X}` +
260
- (when ? ` ${D}witnessed ${when}${X}` : ` ${D}date not published${X}`) +
292
+ out(`Listing state: ${J}listed and witnessed${X}` +
293
+ (when ? ` ${D}last witnessed ${when}${X}` : ` ${D}date not published${X}`) +
261
294
  (since ? ` ${D}first published ${since}${X}` : ''));
262
295
 
263
- const line = [];
264
- if (rec.probes) line.push(`${B}Live probes${X} ${rec.probes.passed}/${rec.probes.total}`);
265
- if (rec.attested) line.push(`${B}Attestations${X} ${rec.attested.passed}/${rec.attested.total}`);
266
- if (line.length) {
267
- out(`\n${line.join(' ')}`);
268
- out(`${D}Probes are checks Trooth read for itself. Attestations are the company's own declarations.`);
269
- out(`They are counts, reported apart on purpose. Trooth never adds them up into one number.${X}`);
296
+ // Two counts, never a ratio, in the form the website's record page uses: an
297
+ // "N/M" cell reads as a bar, and a bar reads as a grade. The JSON fields keep
298
+ // the feed's names (`passed`, `total`).
299
+ const counts = countLines(rec);
300
+ if (counts.length) {
301
+ out('');
302
+ for (const l of counts) out(l);
303
+ out(`${D}Live probes are readings Trooth took itself, from the company's public surface.`);
304
+ out(`Self-attestations are what the company attested about itself; Trooth records them`);
305
+ out(`and did not witness them. The two are reported apart and never added into one number.${X}`);
270
306
  }
271
307
 
272
308
  const ids = [];
@@ -274,14 +310,14 @@ async function check() {
274
310
  if (rec.authority_key_id) ids.push(`${D}Key ${rec.authority_key_id}${X}`);
275
311
  if (ids.length) out(ids.join(' '));
276
312
 
277
- if (rec.events.length) {
278
- out(`\n${B}Recent witness events${X}`);
279
- for (const e of rec.events.slice(0, 3)) {
280
- out(` ${J}•${X} ${D}${fmtDate(e.at)}${X} ${e.type.replace(/_/g, ' ')}${e.detail ? `${D} ${e.detail}${X}` : ''}`);
281
- }
313
+ const latest = latestEvents(rec.events, 3);
314
+ if (latest.length) {
315
+ out(`\n${B}Latest ledger events, newest first${X}`);
316
+ for (const e of latest) out(` ${J}•${X} ${D}${fmtDate(e.at)}${X} ${eventLabel(e.type)}`);
317
+ out(`${D} --json carries the whole ledger, with the feed's own wording for each event.${X}`);
282
318
  }
283
319
 
284
- out(`\n${D}A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.`);
320
+ out(`\n${D}A dated, point-in-time record. Trooth issues no verdict and no single number.`);
285
321
  out(`Full record: ${X}${C}${rec.record_url}${X}${D} · Signing keys: ${rec.verify_keys}${X}\n`);
286
322
  process.exit(EXIT.OK);
287
323
  }
@@ -292,20 +328,31 @@ async function check() {
292
328
  * It reads the infrastructure a repository DECLARES and reports those
293
329
  * declarations as facts: how many storage resources declare encryption, which
294
330
  * regions appear, how many rules declare exposure to the whole internet. It
295
- * does not judge them. There is no verdict, no pass mark, no severity, no
296
- * score, and nothing is checked against a named standard or regulation.
331
+ * does not judge them. There is no verdict, no threshold, no severity and no
332
+ * rating, and nothing is checked against a named standard or regulation.
297
333
  * Declaring public ingress is not a failing; a load balancer is supposed to be
298
- * public. What the facts mean is the reader's call.
334
+ * public. What the facts mean is the reader's decision.
299
335
  *
300
336
  * It opens files and opens no sockets. Nothing about the repository leaves the
301
- * machine. The digest at the end is a SHA-256 over the canonical fact document
302
- * with the timestamp excluded, so the same tree always produces the same digest
303
- * and you can record it as evidence that a given state was observed, without
337
+ * machine. The digest at the end is a SHA-256 over the `facts` object in
338
+ * canonical form (the timestamp, path and CLI version are outside it), so the
339
+ * same tree read by the same CLI version always produces the same digest and
340
+ * you can record it as evidence that a given state was observed, without
304
341
  * publishing the tree it came from.
305
342
  *
306
- * It is a declaration reader, not an HCL parser: .tf.json, Kubernetes YAML and
307
- * terraform plan JSON are parsed properly, and .tf files are read at the
308
- * attribute level. That limit is stated in the output rather than hidden. */
343
+ * HOW EACH SOURCE IS READ. It is a pattern reader, not a Terraform evaluator:
344
+ * variables, modules and for_each are never resolved.
345
+ * .tf regular expressions, split into top-level resource blocks
346
+ * .tf.json parsed as JSON; each resource is one unit
347
+ * plan JSON parsed as JSON (`terraform show -json`); each planned
348
+ * managed resource is one unit
349
+ * Kubernetes YAML regular expressions, one unit per YAML document,
350
+ * classified by its top-level `kind`
351
+ * Dockerfile read for regions, open addresses, public markers and
352
+ * credential literals only; it declares no resource types
353
+ * Storage, logging and identity are classified on a unit's resource type or
354
+ * kind, never on the text around it. The human output prints this list in
355
+ * short, so the limit is stated where the counts are. */
309
356
 
310
357
  const SKIP_DIRS = new Set([
311
358
  'node_modules', '.git', '.terraform', '.next', 'dist', 'build', 'vendor',
@@ -357,6 +404,125 @@ const PUBLIC_RE = /\b(?:publicly_accessible\s*[:=]\s*true|acl\s*[:=]\s*["']publi
357
404
  // A literal that looks like a credential sitting in the file. Reported as a
358
405
  // count only: no file name, no line, and never the value itself.
359
406
  const SECRET_RE = /\b(?:password|secret|api[_-]?key|access[_-]?key|token|private[_-]?key)\s*[:=]\s*["'][^"'${}\n]{8,}["']/i;
407
+ // A storage-matching Terraform type that names a SETTING on a store rather than
408
+ // a store: aws_s3_bucket_server_side_encryption_configuration, a bucket policy,
409
+ // a volume attachment, a subnet group. The substring match above catches these
410
+ // (the fixture's one bucket used to count as two storage declarations), so they
411
+ // are not counted as storage. A setting that declares encryption and references
412
+ // a store credits that store with declaring encryption. Applied to snake_case
413
+ // Terraform types only; Kubernetes kinds are CamelCase and are not settings.
414
+ const STORAGE_SETTING = /_(?:configuration|policy|acl|versioning|notification|public_access_block|ownership_controls|attachment|object|item|logging|iam_member|iam_binding|access_point|mount_target|snapshot|subnet_group|parameter_group|option_group)$/;
415
+ const isStorageSetting = (type) => type.includes('_') && STORAGE_SETTING.test(type);
416
+ // An attribute set to false, null or empty declares nothing: `encrypted = false`
417
+ // and `storage_encrypted: false` must not count as declaring encryption.
418
+ const NEGATIVE_ATTR_RE = /^[^\n:=]*[:=]\s*(?:false|null|"false"|'false'|""|''|\[\]|\{\})\s*,?\s*$/gim;
419
+ const declaresEncryption = (body) => ENCRYPT_RE.test(body.replace(NEGATIVE_ATTR_RE, ''));
420
+
421
+ /** JSON text with `"key":` rewritten as `key:`, so the attribute patterns above
422
+ * (written for `key = "v"` and `key: "v"`) read JSON sources too. Escaped
423
+ * quotes inside string values are never rewritten. */
424
+ function flattenJson(text) {
425
+ return text.replace(/"([A-Za-z_][\w.-]*)"\s*:/g, '$1:');
426
+ }
427
+
428
+ /** A parsed JSON value with false, null, empty strings, empty arrays and empty
429
+ * objects removed, so a plan's unset attributes read as absent. */
430
+ function prune(v) {
431
+ if (Array.isArray(v)) { const a = v.map(prune).filter((x) => x !== undefined); return a.length ? a : undefined; }
432
+ if (v && typeof v === 'object') {
433
+ const o = {};
434
+ for (const [k, x] of Object.entries(v)) { const y = prune(x); if (y !== undefined) o[k] = y; }
435
+ return Object.keys(o).length ? o : undefined;
436
+ }
437
+ return v === null || v === false || v === '' ? undefined : v;
438
+ }
439
+
440
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
441
+ /** A pattern that finds a Terraform address (aws_s3_bucket.logs) in another
442
+ * resource's body, as `aws_s3_bucket.logs.id` or `${aws_s3_bucket.logs.arn}`,
443
+ * and not inside a longer address such as aws_s3_bucket.logs2. */
444
+ const addressRef = (address) => new RegExp(`(?:^|[^\\w.])${escRe(address)}(?![\\w-])`);
445
+
446
+ /** Every resource under `resource` in a .tf.json document, in either of the
447
+ * shapes Terraform's JSON syntax allows (objects, or arrays of objects). */
448
+ function tfJsonResources(doc) {
449
+ const out = [];
450
+ const each = (v, fn) => { if (Array.isArray(v)) v.forEach((x) => each(x, fn)); else if (v && typeof v === 'object') fn(v); };
451
+ each(doc, (top) => each(top.resource, (byType) => {
452
+ for (const [type, byName] of Object.entries(byType)) {
453
+ if (!/^[a-z0-9_]+$/.test(type)) continue;
454
+ each(byName, (names) => { for (const [name, body] of Object.entries(names)) out.push({ type, name, body }); });
455
+ }
456
+ }));
457
+ return out;
458
+ }
459
+
460
+ /** The managed resources a plan (`terraform show -json`) says will exist:
461
+ * planned_values, every module deep, or resource_changes when a plan has no
462
+ * planned_values. Data sources and deletions are not declarations. */
463
+ function planResources(doc) {
464
+ const out = [];
465
+ const walkModule = (m) => {
466
+ if (!m || typeof m !== 'object') return;
467
+ for (const r of Array.isArray(m.resources) ? m.resources : []) {
468
+ if (r && r.mode !== 'data' && typeof r.type === 'string') out.push({ type: r.type, name: String(r.name ?? ''), body: r.values ?? {} });
469
+ }
470
+ for (const c of Array.isArray(m.child_modules) ? m.child_modules : []) walkModule(c);
471
+ };
472
+ if (doc && doc.planned_values && doc.planned_values.root_module) walkModule(doc.planned_values.root_module);
473
+ else {
474
+ for (const rc of Array.isArray(doc && doc.resource_changes) ? doc.resource_changes : []) {
475
+ const after = rc && rc.change ? rc.change.after : null;
476
+ if (rc && rc.mode !== 'data' && typeof rc.type === 'string' && after) out.push({ type: rc.type, name: String(rc.name ?? ''), body: after });
477
+ }
478
+ }
479
+ return out;
480
+ }
481
+
482
+ /** Split one declaration file into the units lint classifies. Each unit has a
483
+ * `type` (a resource type or Kubernetes kind, or null when there is none),
484
+ * `refs` (patterns another unit's body would contain to refer to this one),
485
+ * `body` (the text attribute patterns run on) and `typed` (whether the type
486
+ * belongs in the resource type list). */
487
+ function unitsOf(kind, file, text) {
488
+ if (kind === 'terraform' && !basename(file).toLowerCase().endsWith('.tf.json')) {
489
+ return text.split(/\n(?=resource\s+")/).map((b) => {
490
+ const h = b.match(/^resource\s+"([a-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/);
491
+ return h ? { type: h[1], refs: [addressRef(`${h[1]}.${h[2]}`)], body: b, typed: false }
492
+ : { type: null, refs: [], body: b, typed: false };
493
+ });
494
+ }
495
+ if (kind === 'kubernetes') {
496
+ return text.split(/^---[^\n]*$/m).map((d) => {
497
+ const k = d.match(/^kind\s*:\s*["']?([A-Za-z][A-Za-z0-9]*)/m);
498
+ return { type: k ? k[1] : null, refs: [], body: d, typed: false };
499
+ });
500
+ }
501
+ if (kind === 'container') return [{ type: null, refs: [], body: text, typed: false }];
502
+
503
+ // .tf.json and plan JSON.
504
+ let doc;
505
+ try { doc = JSON.parse(text); } catch { return [{ type: null, refs: [], body: flattenJson(text), typed: false }]; }
506
+ const asBody = (v) => flattenJson(JSON.stringify(prune(v) ?? {}, null, 1));
507
+ if (kind === 'terraform-plan') {
508
+ return planResources(doc).map((r) => {
509
+ const v = prune(r.body) || {};
510
+ // A plan carries values, not expressions, so a setting names its store
511
+ // by the store's own bucket name or id.
512
+ const refs = ['bucket', 'id'].map((k) => v[k]).filter((x) => typeof x === 'string' && x.length >= 3)
513
+ .map((x) => new RegExp(escRe(JSON.stringify(x))));
514
+ return { type: r.type, refs, body: asBody(r.body), typed: true };
515
+ });
516
+ }
517
+ const units = tfJsonResources(doc).map((r) => ({ type: r.type, refs: [addressRef(`${r.type}.${r.name}`)], body: asBody(r.body), typed: true }));
518
+ // Everything outside `resource` (providers, variables, locals) is one untyped
519
+ // unit, the way the text before the first resource block is in a .tf file.
520
+ if (doc && typeof doc === 'object' && !Array.isArray(doc)) {
521
+ const rest = { ...doc }; delete rest.resource;
522
+ units.push({ type: null, refs: [], body: asBody(rest), typed: false });
523
+ }
524
+ return units;
525
+ }
360
526
 
361
527
  function classify(file, text) {
362
528
  const n = basename(file).toLowerCase();
@@ -393,7 +559,9 @@ function lint() {
393
559
  const byKind = { terraform: 0, 'terraform-plan': 0, kubernetes: 0, container: 0 };
394
560
  const regions = new Set();
395
561
  const resourceTypes = new Map();
396
- let read = 0, storage = 0, storageEncrypted = 0, logging = 0, identity = 0;
562
+ const stores = []; // { refs, encrypted }, one per storage declaration
563
+ const encryptingSettings = []; // bodies of storage settings that declare encryption
564
+ let read = 0, logging = 0, identity = 0;
397
565
  let openIngress = 0, publicAccess = 0, inlineCredentials = 0;
398
566
 
399
567
  for (const f of files) {
@@ -407,29 +575,44 @@ function lint() {
407
575
  byKind[kind]++;
408
576
  read++;
409
577
 
410
- for (const m of text.matchAll(REGION_RE)) regions.add(m[1]);
578
+ // JSON sources are read through flattenJson so `"region": "x"` and
579
+ // `"password": "..."` meet the same patterns as HCL and YAML.
580
+ const isJson = extname(f).toLowerCase() === '.json';
581
+ const flat = isJson ? flattenJson(text) : text;
582
+ for (const m of flat.matchAll(REGION_RE)) regions.add(m[1]);
583
+ for (const line of flat.split('\n')) if (SECRET_RE.test(line)) inlineCredentials++;
411
584
 
412
- if (kind === 'terraform') {
585
+ if (kind === 'terraform' && !isJson) {
413
586
  for (const m of text.matchAll(RESOURCE_RE)) {
414
587
  resourceTypes.set(m[1], (resourceTypes.get(m[1]) || 0) + 1);
415
588
  }
416
589
  }
417
590
 
418
- // Terraform is split on top-level resource blocks so an attribute in one
419
- // resource is never credited to another; every other kind is read whole.
420
- const blocks = kind === 'terraform' ? text.split(/\n(?=resource\s+")/) : [text];
421
- for (const b of blocks) {
422
- const header = b.match(/^resource\s+"([a-z0-9_]+)"/);
423
- const subject = header ? header[1] : b;
424
- if (STORAGE_TYPE.test(subject)) { storage++; if (ENCRYPT_RE.test(b)) storageEncrypted++; }
425
- if (LOGGING_TYPE.test(subject)) logging++;
426
- if (IDENTITY_TYPE.test(subject)) identity++;
427
- if (OPEN_CIDR_RE.test(b)) openIngress++;
428
- if (PUBLIC_RE.test(b)) publicAccess++;
591
+ // Each unit is classified on its own type, so an attribute in one resource
592
+ // is never credited to another, and a word in a file's text is never taken
593
+ // for a resource type.
594
+ for (const u of unitsOf(kind, f, text)) {
595
+ const t = u.type;
596
+ if (t && u.typed) resourceTypes.set(t, (resourceTypes.get(t) || 0) + 1);
597
+ if (t && STORAGE_TYPE.test(t)) {
598
+ if (isStorageSetting(t)) { if (declaresEncryption(u.body)) encryptingSettings.push(u.body); }
599
+ else stores.push({ refs: u.refs, encrypted: declaresEncryption(u.body) });
600
+ }
601
+ if (t && LOGGING_TYPE.test(t)) logging++;
602
+ if (t && IDENTITY_TYPE.test(t)) identity++;
603
+ if (OPEN_CIDR_RE.test(u.body)) openIngress++;
604
+ if (PUBLIC_RE.test(u.body)) publicAccess++;
429
605
  }
430
- for (const line of text.split('\n')) if (SECRET_RE.test(line)) inlineCredentials++;
431
606
  }
432
607
 
608
+ // A store declares encryption in its own body, or through a setting resource
609
+ // (anywhere in the tree) that declares encryption and refers to it.
610
+ for (const s of stores) {
611
+ if (!s.encrypted && s.refs.some((re) => encryptingSettings.some((b) => re.test(b)))) s.encrypted = true;
612
+ }
613
+ const storage = stores.length;
614
+ const storageEncrypted = stores.filter((s) => s.encrypted).length;
615
+
433
616
  if (read === 0) {
434
617
  const msg = `no infrastructure declarations found under ${target}.`;
435
618
  diag(`${A}nothing to read${X} ${msg}`);
@@ -466,7 +649,7 @@ function lint() {
466
649
  root: (() => { const r = relative(process.cwd(), target); return !r ? '.' : r.startsWith('..') ? target : r; })(),
467
650
  facts,
468
651
  digest: `sha256:${digest}`,
469
- note: 'Declared facts only. Read locally; nothing was transmitted. No score, no verdict, no assessment against any standard.',
652
+ note: 'Declared facts only. Read locally; nothing was transmitted. No verdict and no assessment against any standard.',
470
653
  };
471
654
 
472
655
  if (asJson) { emitJson(doc); process.exit(EXIT.OK); }
@@ -495,10 +678,16 @@ function lint() {
495
678
 
496
679
  out(`\n${B}Digest${X} ${C}${doc.digest}${X}`);
497
680
  out(`${D}A SHA-256 over the facts above, in canonical form, with the timestamp excluded.`);
498
- out(`The same tree always produces the same digest, so you can record it as evidence`);
499
- out(`that a state was observed without publishing the tree it came from.${X}`);
681
+ out(`The same tree read by the same trooth version produces the same digest, so you can`);
682
+ out(`record it as evidence that a state was observed without publishing the tree.${X}`);
683
+
684
+ out(`\n${B}How this was read${X}`);
685
+ out(`${D} A pattern reader, not a Terraform evaluator: variables and modules are not resolved.`);
686
+ out(` .tf by pattern, one resource block at a time. .tf.json and plan JSON parsed, one`);
687
+ out(` resource at a time. Kubernetes YAML by pattern, one document at a time, by kind.`);
688
+ out(` Dockerfiles for regions, open addresses, public markers and credential literals only.${X}`);
500
689
 
501
- out(`\n${D}Counts of what the files declare. Not a judgement: a public load balancer is`);
690
+ out(`\n${D}Counts of what the files declare. Not a judgment: a public load balancer is`);
502
691
  out(`supposed to be public. Trooth issues no verdict here and checks nothing against`);
503
692
  out(`any standard. Nothing left this machine: lint opens files and opens no sockets.`);
504
693
  out(`Publish what you choose on your record at ${X}${C}https://trooth.co/dashboard${X}${D}.${X}\n`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trooth",
3
- "version": "0.4.2",
4
- "description": "The Trooth Network from your terminal. Read any company's public, signed record with no key and no account: identity, products, terms, people, documents, security and AI posture. Also reads what your own repo declares, offline. No score, no verdict.",
3
+ "version": "0.4.3",
4
+ "description": "The Trooth Network from your terminal. Read a company's public, signed record with no key and no account: listing state, dates, live-probe and self-attestation counts, signing key id. Also reads what your own repo declares, offline. No verdict.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "trooth": "bin/trooth.mjs"
@@ -17,7 +17,7 @@
17
17
  "scripts": {
18
18
  "check": "node ./bin/trooth.mjs check",
19
19
  "lint": "node ./bin/trooth.mjs lint",
20
- "test": "node ./bin/trooth.mjs lint tests/fixtures/infra --json > /dev/null && node ./bin/trooth.mjs --version"
20
+ "test": "node ./bin/trooth.mjs lint tests/fixtures/infra --json > /dev/null && node ./bin/trooth.mjs --version && node tests/action.test.mjs && node tests/lint.test.mjs"
21
21
  },
22
22
  "license": "Apache-2.0",
23
23
  "author": "Trooth, LLC",
@@ -36,9 +36,7 @@
36
36
  "vendor-lookup",
37
37
  "due-diligence",
38
38
  "procurement",
39
- "trust-network",
40
39
  "company-directory",
41
- "mcp",
42
40
  "ai-agents",
43
41
  "agent-tools",
44
42
  "signed-records",