trooth 0.4.3 → 0.4.4

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 +57 -36
  2. package/bin/trooth.mjs +60 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,7 +30,7 @@ Node 18 or newer, because the binary uses the built-in `fetch`. Zero dependencie
30
30
 
31
31
  | Command | What it does |
32
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. |
33
+ | `trooth check <domain>` | Reads a company's record from the live Network and prints whether it is listed, when it was last witnessed and first published, the live-probe and self-attestation counts, the badge id, the id of the key that signed the record, and the three newest events in its ledger, newest first. `--json` adds the signature itself and every event the feed returns. |
34
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
35
  | `trooth --help` | Help. Also `-h` and `help`. |
36
36
  | `trooth --version` | Version. Also `-v` and `version`. |
@@ -61,27 +61,30 @@ trooth check trooth.co
61
61
  Example output. The values are illustrative; the shape is what the binary prints.
62
62
 
63
63
  ```
64
- Trooth Network // witnessed · public · read-only //
64
+ Trooth Network // public · signed · read-only //
65
65
  Trooth, LLC trooth.co
66
- Standing: listed and witnessed witnessed 2026-08-30 first published 2026-08-01
66
+ Listing state: listed and witnessed last witnessed 2026-08-30 first published 2026-08-01
67
67
 
68
- Live probes 64/65 Attestations 27/35
69
- Probes are checks Trooth read for itself. Attestations are the company's own declarations.
70
- They are counts, reported apart on purpose. Trooth never adds them up into one number.
68
+ Live probes: 65 read; 64 as expected
69
+ Self-attestations: 35 asked; 27 attested
70
+ Live probes are readings Trooth took itself, from the company's public surface.
71
+ Self-attestations are what the company attested about itself; Trooth records them
72
+ and did not witness them. The two are reported apart and never added into one number.
71
73
  Badge rw_... Key ed25519-2026-01
72
74
 
73
- Recent witness events
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
75
+ Latest ledger events, newest first
76
+ • 2026-08-30 record published to the Trooth Network
77
+ • 2026-08-30 reading completed
78
+ • 2026-08-01 record published to the Trooth Network
79
+ --json carries the whole ledger, with the feed's own wording for each event.
77
80
 
78
- A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.
81
+ A dated, point-in-time record. Trooth issues no verdict and no single number.
79
82
  Full record: https://trooth.co/network/trooth.co · Signing keys: https://api.trooth.co/public/keys
80
83
  ```
81
84
 
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).
85
+ The line that begins `Listing state:` gives the listing state. For a listed company it reads `listed and witnessed`, then the date of the most recent published reading (`last witnessed`) and the date the record was first published. The two counts use the same form as the record page on trooth.co: live probes (how many were read at the last reading, and how many of those returned the expected result) and self-attestations (how many the company was asked for, and how many it attested). Live probes are readings Trooth took itself; self-attestations are the company's statements about itself, which Trooth records and does not witness.
83
86
 
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.
87
+ The events section prints the three newest entries in the record's ledger, newest first, by timestamp; entries with the same timestamp keep the feed's order, the later entry first. Known event types get a plain label: `scan_completed` prints as "reading completed", `standing_published` as "record published to the Trooth Network" and `rewitnessed` as "live probes re-read". A type the CLI does not know prints with its underscores turned into spaces. The human view leaves out each event's detail text. `--json` carries every event the feed returns, with its type and detail exactly as the feed has them.
85
88
 
86
89
  For scripting:
87
90
 
@@ -100,9 +103,9 @@ trooth check trooth.co --json
100
103
  "probes": { "passed": 64, "total": 65 },
101
104
  "attested": { "passed": 27, "total": 35 },
102
105
  "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" },
106
+ { "type": "scan_completed", "at": "2026-08-01T00:00:00Z", "detail": "65 probes read · 64 returned the expected result · 27 declarations recorded · reading signed" },
104
107
  { "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" },
108
+ { "type": "scan_completed", "at": "2026-08-30T00:00:00Z", "detail": "65 probes read · 64 returned the expected result · 27 declarations recorded · reading signed" },
106
109
  { "type": "standing_published", "at": "2026-08-30T00:00:00Z", "detail": "point-in-time · published to the Trooth Network" }
107
110
  ],
108
111
  "receipt_signature": "...",
@@ -112,9 +115,9 @@ trooth check trooth.co --json
112
115
  }
113
116
  ```
114
117
 
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.
118
+ 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, in the feed's order. Each event's `detail` is written when the event is stored and is not rewritten afterward, so older and newer events of the same type can be worded differently.
116
119
 
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.
120
+ `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 contains `score`, `tier`, `grade`, `rank`, `rating`, `level` or `percent`, or is `rate`, is dropped no matter what the feed sends.
118
121
 
119
122
  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
123
 
@@ -122,50 +125,58 @@ A company with no record exits 1 and emits `{"domain": "...", "listed": false, "
122
125
 
123
126
  ## `trooth lint`
124
127
 
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.
128
+ `lint` reads what your infrastructure **declares** and reports it. It does not judge it. There is no verdict, no threshold, no severity and no rating, 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.
126
129
 
127
130
  ```bash
128
131
  trooth lint ./infra
129
132
  ```
130
133
 
131
- Example output. The values are illustrative; the shape is what the binary prints.
134
+ Output for the small fixture in this repository (`trooth lint tests/fixtures/infra`, trooth 0.4.3):
132
135
 
133
136
  ```
134
137
  trooth lint // local · offline · declarations only //
135
- infra 14 declaration file(s) read
136
- terraform 11 · kubernetes 3
138
+ tests/fixtures/infra 2 declaration file(s) read
139
+ terraform 1 · kubernetes 1
137
140
 
138
141
  Declared
139
- Regions and zones eu-west-1, us-east-1
140
- Storage declarations 9
141
- of those declaring encryption 9
142
- Logging declarations 4
143
- Identity declarations 6
142
+ Regions and zones us-east-1
143
+ Storage declarations 1
144
+ of those declaring encryption 1
145
+ Logging declarations 0
146
+ Identity declarations 0
144
147
  Open to any address (0.0.0.0/0, ::/0) 1
145
- Marked public 2
148
+ Marked public 0
146
149
  Inline credential literals 0
147
150
 
148
151
  Most declared resource types
149
- 5 aws_s3_bucket
150
- 3 aws_iam_role
151
- 2 aws_cloudwatch_log_group
152
+ 1 aws_s3_bucket
153
+ 1 aws_s3_bucket_server_side_encryption_configuration
154
+ 1 aws_security_group_rule
152
155
 
153
- Digest sha256:...
156
+ Digest sha256:a40bf6cf27dfd0b9079ac48a326fb2d1423b076c96b405451dde1ed2d7848944
154
157
  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.
158
+ The same tree read by the same trooth version produces the same digest, so you can
159
+ record it as evidence that a state was observed without publishing the tree.
160
+
161
+ How this was read
162
+ A pattern reader, not a Terraform evaluator: variables and modules are not resolved.
163
+ .tf by pattern, one resource block at a time. .tf.json and plan JSON parsed, one
164
+ resource at a time. Kubernetes YAML by pattern, one document at a time, by kind.
165
+ Dockerfiles for regions, open addresses, public markers and credential literals only.
157
166
 
158
- Counts of what the files declare. Not a judgement: a public load balancer is
167
+ Counts of what the files declare. Not a judgment: a public load balancer is
159
168
  supposed to be public. Trooth issues no verdict here and checks nothing against
160
169
  any standard. Nothing left this machine: lint opens files and opens no sockets.
161
170
  Publish what you choose on your record at https://trooth.co/dashboard.
162
171
  ```
163
172
 
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.
173
+ The bucket's encryption configuration is a setting on the bucket, not a second store, so the fixture has one storage declaration, and that one declares encryption.
174
+
175
+ It reads `.tf`, `.tf.json`, Kubernetes YAML (anything carrying both `apiVersion` and `kind`), `terraform show -json` plan files and Dockerfiles. It is a pattern reader, not a Terraform evaluator: variables, modules and `for_each` are never resolved, so a count can differ from what Terraform itself would plan. `.tf` files are read by pattern, one resource block at a time. `.tf.json` and plan files are parsed as JSON, one resource at a time. Kubernetes YAML is read by pattern, one document at a time, and classified by its `kind`. Dockerfiles are read for regions, open addresses, public markers and credential literals only. Storage, logging and identity are counted by a resource's type or a document's kind, never by the words around it. The output states this under "How this was read".
165
176
 
166
177
  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.
167
178
 
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.
179
+ The digest is a SHA-256 over the `facts` object in canonical form. The timestamp, the path and the CLI version are outside it, so the same tree read by the same trooth version produces the same digest. A release that changes how something is counted changes the digest of the same tree: the fixture above digests differently under 0.4.2 and 0.4.3. Record the digest in CI, or on your own record, as evidence that a given state was observed, without publishing the tree it came from.
169
180
 
170
181
  ```bash
171
182
  # Keep the fact document as a build artifact.
@@ -203,6 +214,16 @@ https://api.trooth.co/public/mcp
203
214
 
204
215
  Four read-only tools, public data, no key. The pattern is written up at [trooth.co/docs/agents](https://trooth.co/docs/agents).
205
216
 
217
+ ## Changed in 0.4.4
218
+
219
+ - `check` asks the Trooth Network for the one record it needs (`/directory/api/vendors/<domain>`) instead of downloading the whole directory list and searching it. When the API does not serve that path yet, it reads the list as 0.4.3 did. The printed output, `check --json` and the exit codes are unchanged.
220
+
221
+ ## Changed in 0.4.3
222
+
223
+ - `check` labels the listing state `Listing state:` and prints the two counts in the record page's form (`65 read; 64 as expected`, `35 asked; 27 attested`). It prints the three newest ledger events, newest first, with plain labels and without the detail text. 0.4.2 printed the ledger's three oldest events under a heading that called them recent. `check --json` is unchanged.
224
+ - `lint` no longer counts a storage setting (a bucket's encryption configuration, a bucket policy, a volume attachment) as a second store, and a setting that declares encryption credits the store it refers to. It no longer counts `encrypted = false`, or an empty encryption setting in a plan, as declaring encryption. It classifies by resource type or Kubernetes `kind` only, parses `.tf.json` and plan JSON and counts their resource types, and reads Kubernetes YAML one document at a time. The same tree can therefore produce different counts, and a different digest, than under 0.4.2. The `lint --json` field names are unchanged.
225
+ - The human `lint` output states how each source was read.
226
+
206
227
  ## Removed in 0.4.0
207
228
 
208
229
  Breaking, and deliberately so.
package/bin/trooth.mjs CHANGED
@@ -40,7 +40,7 @@ import { join, relative, extname, basename } from 'node:path';
40
40
 
41
41
  const API = process.env.TROOTH_API || 'https://api.trooth.co';
42
42
  const require = createRequire(import.meta.url);
43
- let VERSION = '0.4.3';
43
+ let VERSION = '0.4.4';
44
44
  try { VERSION = require('../package.json').version; } catch {}
45
45
 
46
46
  const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
@@ -160,20 +160,27 @@ Trooth signs what it witnessed. It never signs on a company's behalf.${X}
160
160
 
161
161
  /* -------------------------------------------------------------- fetch ---- */
162
162
 
163
- async function callTrooth(path, init, what) {
164
- let res;
163
+ /** One request to the API. Exits 3 when the API cannot be reached; any HTTP
164
+ * status comes back to the caller. */
165
+ async function requestTrooth(path, init, what) {
165
166
  try {
166
- res = await fetch(`${API}${path}`, {
167
+ return await fetch(`${API}${path}`, {
167
168
  ...init,
168
169
  headers: { accept: 'application/json', 'user-agent': `trooth-cli/${VERSION}`, ...(init && init.headers) },
169
170
  });
170
171
  } catch (e) {
171
172
  fail(EXIT.UPSTREAM, `could not reach ${what} at ${API}: ${e && e.message ? e.message : e}`);
172
173
  }
173
- if (!res.ok) {
174
- const body = await res.text().catch(() => '');
175
- fail(EXIT.UPSTREAM, `${what} returned HTTP ${res.status}.${body ? ' ' + body.slice(0, 300).replace(/\s+/g, ' ') : ''}`, { http_status: res.status });
176
- }
174
+ }
175
+
176
+ async function failHttp(res, what) {
177
+ const body = await res.text().catch(() => '');
178
+ fail(EXIT.UPSTREAM, `${what} returned HTTP ${res.status}.${body ? ' ' + body.slice(0, 300).replace(/\s+/g, ' ') : ''}`, { http_status: res.status });
179
+ }
180
+
181
+ async function callTrooth(path, init, what) {
182
+ const res = await requestTrooth(path, init, what);
183
+ if (!res.ok) await failHttp(res, what);
177
184
  try { return await res.json(); }
178
185
  catch { fail(EXIT.UPSTREAM, `${what} returned a response that is not JSON.`); }
179
186
  }
@@ -258,15 +265,57 @@ function projectRecord(v, domain) {
258
265
  return scrub(rec);
259
266
  }
260
267
 
268
+ /** The whole list, searched here. This is how every release up to 0.4.3 read a
269
+ * company, and it is now only the fallback in readVendor below. */
270
+ async function readVendorFromList(domain, what) {
271
+ const data = await callTrooth('/directory/api/vendors', { method: 'GET' }, what);
272
+ const vendors = Array.isArray(data && data.vendors) ? data.vendors : [];
273
+ return vendors.find((v) => v && normalizeDomain(v.domain) === domain) || null;
274
+ }
275
+
276
+ /** One company's record from the directory feed, or null when the feed carries
277
+ * no record for that domain. It asks for that one record
278
+ * (/directory/api/vendors/<domain>), which returns the same object the list
279
+ * carries for that domain, and does not download every company to find it. */
280
+ async function readVendor(domain) {
281
+ const what = 'the Trooth Network';
282
+ const res = await requestTrooth(`/directory/api/vendors/${encodeURIComponent(domain)}`, { method: 'GET' }, what);
283
+
284
+ if (res.status === 404) {
285
+ const text = await res.text().catch(() => '');
286
+ let body = null;
287
+ try { body = JSON.parse(text); } catch {}
288
+ // The route answered and has no record for this domain. It says so with a
289
+ // JSON body carrying `listed: false`.
290
+ if (body && typeof body === 'object' && body.listed === false) return null;
291
+ // FALLBACK TO THE LIST. Any other 404 comes from a deploy of the directory
292
+ // worker that predates the single-record route: it answers an unknown path
293
+ // with a plain-text "Not found". Read the list and search it here, as 0.4.3
294
+ // does, so this release keeps working against a worker that has not been
295
+ // redeployed yet, and against a TROOTH_API that points at an older one.
296
+ // This fallback can go in the first release after api.trooth.co serves the
297
+ // route, that is, once a request for a domain with no record there returns
298
+ // a JSON 404 with `listed: false`.
299
+ return readVendorFromList(domain, what);
300
+ }
301
+ if (!res.ok) await failHttp(res, what);
302
+
303
+ let v;
304
+ try { v = await res.json(); }
305
+ catch { fail(EXIT.UPSTREAM, `${what} returned a response that is not JSON.`); }
306
+ if (!v || typeof v !== 'object' || Array.isArray(v) || normalizeDomain(v.domain) !== domain) {
307
+ fail(EXIT.UPSTREAM, `${what} returned a response that is not the record for ${domain}.`);
308
+ }
309
+ return v;
310
+ }
311
+
261
312
  async function check() {
262
313
  const { positional } = parseArgs('check');
263
314
  if (positional.length > 1) fail(EXIT.USAGE, `check takes one <domain>, got: ${positional.join(' ')}`);
264
315
  const domain = normalizeDomain(positional[0]);
265
316
  if (!domain) fail(EXIT.USAGE, 'missing <domain>. Try: trooth check stripe.com');
266
317
 
267
- const data = await callTrooth('/directory/api/vendors', { method: 'GET' }, 'the Trooth Network');
268
- const vendors = Array.isArray(data && data.vendors) ? data.vendors : [];
269
- const vendor = vendors.find((v) => v && normalizeDomain(v.domain) === domain) || null;
318
+ const vendor = await readVendor(domain);
270
319
 
271
320
  if (!vendor) {
272
321
  if (asJson) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trooth",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
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": {