scvd-corpus-client 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # scvd-corpus-client — changelog
2
+
3
+ Versions are immutable once published. Minor versions add functions and
4
+ never change an existing function's result.
5
+
6
+ ## 0.1.0 — 2026-09-10
7
+
8
+ Initial release preparation: `corpus`, `freshSet`, `hostHistory`, `month`,
9
+ `feeds`, `diff`, `defects`, `withDenominator`, `CorpusHttpError`, and
10
+ `corpusIndex` with `CorpusIndexOptions`. Zero dependencies.
11
+
12
+ The original readers were implemented September 3 (roadmap C5). Compact
13
+ discovery was added September 10: one metadata page per call, caller-controlled
14
+ pagination, gaps and verification limits preserved. Existing readers keep
15
+ their original responses. The package reads evidence; it does not verify it.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sean Record
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # scvd-corpus-client
2
+
3
+ Zero-dependency reader for [scvd.store](https://scvd.store)'s signed
4
+ x402 corpus: the weekly census, the fresh set, one host's readiness
5
+ history, the month, the feeds, the diff and the defect vocabulary, each
6
+ as the store serves it. The `scvd` CLI's library half.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install scvd-corpus-client
12
+ ```
13
+
14
+ Node 18.17 or newer. To use a source checkout, import
15
+ `./corpus-client/corpus-client.js` instead of the package name.
16
+
17
+ ## Use
18
+
19
+ ```js
20
+ import { corpus, hostHistory, month, feeds } from "scvd-corpus-client";
21
+
22
+ const census = await corpus(); // the weekly signed census, whole
23
+ const history = await hostHistory("door.example");
24
+ const august = await month("2026-08"); // the state of x402 for one month
25
+ const atom = await feeds(); // the four Atom feeds, by address
26
+ ```
27
+
28
+ Each reader makes at most one GET to a stable address and returns the store's
29
+ JSON whole. Nothing is summarised, scored or re-derived here: the
30
+ corpus is signed, with each digest submitted for Bitcoin anchoring.
31
+ Pending submissions still need completed proofs and independent verification. A client that rewrote it
32
+ would be a second source of truth. Check the signatures with
33
+ [`x402-verify`](https://www.npmjs.com/package/x402-verify) or any
34
+ ed25519 library against the key at `/.well-known/scvd-signing-key`.
35
+
36
+ `withDenominator(count, of, noun)` prints a counted reading with its
37
+ denominator beside it — "3 of 4 rounds" — and never a percentage: the
38
+ store's rule is that counts travel with denominators and a share
39
+ invites a ranking.
40
+
41
+ ## What it is not
42
+
43
+ Not a ranking and not advice: a host's history is what the store
44
+ observed on the rounds it probed, with the gaps counted against the
45
+ observer. A host never met comes back as never met, a fact about
46
+ coverage. The doors' own `what_this_is_not` fields ride in every answer.
47
+
48
+ ## Versioning
49
+
50
+ Versions are immutable once published. Minor versions add functions
51
+ and never change an existing function's result; the result shapes are
52
+ the store's own documents, which carry their own versions. The dated
53
+ record is `CHANGELOG.md`.
54
+
55
+ ### Compact discovery
56
+
57
+ `corpusIndex({ limit?, cursor?, base?, fetch?, timeoutMs? })` returns one
58
+ page from `/corpus/index.json`. Omit `limit` to use the server's default;
59
+ the server enforces its maximum. It makes one GET and never follows `next`
60
+ or fetches snapshot bodies. For example:
61
+
62
+ ```js
63
+ import { corpusIndex } from "scvd-corpus-client";
64
+
65
+ const page = await corpusIndex({ limit: 1 });
66
+ console.log(page); // includes unreadable rows, counts and verification limits
67
+ // When you decide to fetch another page:
68
+ if (page.has_more === true && typeof page.next === "string") {
69
+ const cursor = new URL(page.next).searchParams.get("cursor");
70
+ if (!cursor) throw new Error("Incomplete pagination: next has no cursor");
71
+ const nextPage = await corpusIndex({ limit: 1, cursor });
72
+ console.log(nextPage);
73
+ }
74
+ ```
75
+
76
+ A page with `has_more: true` and no `next` is incomplete. Keep unreadable
77
+ rows in the denominator, and preserve the server's `verification` and
78
+ `completeness` fields. This is metadata discovery; the helper does not
79
+ verify signatures, chain links or Bitcoin proofs. Pagination does not
80
+ establish a point-in-time inventory. `corpus()` keeps its original whole
81
+ `/corpus.json` response.
82
+
83
+ Invalid option shapes throw `TypeError` without a request. HTTP refusals
84
+ throw `CorpusHttpError` with the status and server body; timeouts and
85
+ network failures reject. An unreadable successful page rejects instead
86
+ of becoming an empty inventory. The existing `timeoutMs` option defaults
87
+ to 30 seconds. Every read is free and needs no account, key or wallet.
88
+
89
+ For bounded snapshot export and offline verification, use the published
90
+ [x402-verify evidence CLI](https://github.com/seancrecord/scvd-general-store-repo/tree/main/verifier#portable-evidence),
91
+ with explicit byte-limit settings for larger snapshots.
@@ -0,0 +1,24 @@
1
+ export const DEFAULT_BASE: string;
2
+ export const DOORS: Readonly<{
3
+ corpus: string;
4
+ corpus_index: string;
5
+ fresh_set: string;
6
+ host: (host: string) => string;
7
+ month: (month?: string) => string;
8
+ feeds: string;
9
+ diff: string;
10
+ defects: string;
11
+ }>;
12
+ export interface ClientOptions { base?: string; fetch?: typeof fetch; timeoutMs?: number }
13
+ export interface CorpusIndexOptions extends ClientOptions { limit?: number; cursor?: string }
14
+ /** One metadata page, returned whole; does not follow next or verify evidence. */
15
+ export function corpusIndex(options?: CorpusIndexOptions): Promise<Record<string, unknown>>;
16
+ export class CorpusHttpError extends Error { status: number; body: unknown }
17
+ export function corpus(options?: ClientOptions): Promise<Record<string, unknown>>;
18
+ export function freshSet(options?: ClientOptions): Promise<Record<string, unknown>>;
19
+ export function hostHistory(host: string, options?: ClientOptions): Promise<Record<string, unknown>>;
20
+ export function month(which?: string, options?: ClientOptions): Promise<Record<string, unknown>>;
21
+ export function feeds(options?: ClientOptions): Promise<Record<string, unknown>>;
22
+ export function diff(options?: ClientOptions): Promise<Record<string, unknown>>;
23
+ export function defects(options?: ClientOptions): Promise<Record<string, unknown>>;
24
+ export function withDenominator(count: number, of: number, noun: string): string;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * scvd-corpus-client — the signed corpus, read as the store serves it.
3
+ *
4
+ * Every function is one GET to a public, stable address and returns
5
+ * the store's own JSON, whole. Nothing is summarised, scored or
6
+ * re-derived here: snapshots are signed with timestamp status, and a
7
+ * client that rewrote it would be a second source of truth. The
8
+ * signatures are checkable with the x402-verify package, or any
9
+ * ed25519 library, against the key at /.well-known/scvd-signing-key.
10
+ *
11
+ * Node builtins and global fetch only. Nothing installed.
12
+ */
13
+
14
+ export const DEFAULT_BASE = "https://scvd.store";
15
+
16
+ export const DOORS = Object.freeze({
17
+ corpus: "/corpus.json",
18
+ corpus_index: "/corpus/index.json",
19
+ fresh_set: "/fresh-set.json",
20
+ host: (host) => `/corpus/host/${encodeURIComponent(host)}.json`,
21
+ month: (month) => (month ? `/corpus/month/${month}` : "/corpus/month"),
22
+ feeds: "/feeds",
23
+ diff: "/corpus/diff.json",
24
+ defects: "/defects.json",
25
+ });
26
+
27
+ const UA = "scvd-corpus-client (+https://scvd.store/corpus)";
28
+ /** Trailing slashes off an origin, without a regular expression over caller input. */
29
+ function trimSlashes(value) {
30
+ let end = String(value).length;
31
+ while (end > 0 && value[end - 1] === "/") end -= 1;
32
+ return String(value).slice(0, end);
33
+ }
34
+
35
+
36
+ export class CorpusHttpError extends Error {
37
+ constructor(path, status, body) {
38
+ super(`${path} answered ${status}${body && typeof body === "object" && body.error ? `: ${body.error}` : ""}`);
39
+ this.name = "CorpusHttpError";
40
+ this.status = status;
41
+ this.body = body;
42
+ }
43
+ }
44
+
45
+ async function getJson(base, path, fetchImpl, timeoutMs, requireObject = false) {
46
+ const response = await fetchImpl(`${trimSlashes(base)}${path}`, {
47
+ headers: { accept: "application/json", "user-agent": UA },
48
+ signal: AbortSignal.timeout(timeoutMs),
49
+ });
50
+ let body = null;
51
+ try {
52
+ body = await response.json();
53
+ } catch {
54
+ body = null;
55
+ }
56
+ if (!response.ok) throw new CorpusHttpError(path, response.status, body);
57
+ if (requireObject && (body === null || typeof body !== "object" || Array.isArray(body))) {
58
+ throw new TypeError(`${path} did not return a JSON object.`);
59
+ }
60
+ return body;
61
+ }
62
+
63
+ function opts({ base = DEFAULT_BASE, fetch: fetchImpl = fetch, timeoutMs = 30_000 } = {}) {
64
+ return { base, fetchImpl, timeoutMs };
65
+ }
66
+
67
+ /** The weekly signed census, whole. */
68
+ export function corpus(options) {
69
+ const o = opts(options);
70
+ return getJson(o.base, DOORS.corpus, o.fetchImpl, o.timeoutMs);
71
+ }
72
+
73
+ /** One discovery page, not signature verification. Never follows next or fetches snapshots. */
74
+ export function corpusIndex(options = {}) {
75
+ const { limit, cursor } = options;
76
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
77
+ throw new TypeError("corpusIndex: limit must be a positive whole number; the server sets the maximum");
78
+ }
79
+ if (cursor !== undefined && (typeof cursor !== "string" || !cursor)) {
80
+ throw new TypeError("corpusIndex: cursor must be the nonempty string from the preceding page");
81
+ }
82
+ const o = opts(options);
83
+ const query = new URLSearchParams();
84
+ if (limit !== undefined) query.set("limit", String(limit));
85
+ if (cursor !== undefined) query.set("cursor", cursor);
86
+ const suffix = query.toString() ? `?${query}` : "";
87
+ return getJson(o.base, `${DOORS.corpus_index}${suffix}`, o.fetchImpl, o.timeoutMs, true);
88
+ }
89
+
90
+ /** This week's doors that answered a conformant challenge. */
91
+ export function freshSet(options) {
92
+ const o = opts(options);
93
+ return getJson(o.base, DOORS.fresh_set, o.fetchImpl, o.timeoutMs);
94
+ }
95
+
96
+ /** One host's readiness history: rounds probed of rounds since first sighting, the tier with its fraction, the gaps counted against the observer. */
97
+ export function hostHistory(host, options) {
98
+ const o = opts(options);
99
+ return getJson(o.base, DOORS.host(String(host).toLowerCase()), o.fetchImpl, o.timeoutMs);
100
+ }
101
+
102
+ /** The state of x402 for one month (YYYY-MM), or the latest month. */
103
+ export function month(which, options) {
104
+ if (which !== undefined && !/^\d{4}-\d{2}$/.test(String(which))) throw new TypeError("month: pass YYYY-MM, or nothing for the latest");
105
+ const o = opts(options);
106
+ return getJson(o.base, DOORS.month(which), o.fetchImpl, o.timeoutMs);
107
+ }
108
+
109
+ /** The four Atom feeds, by address, from the store's own index. */
110
+ export function feeds(options) {
111
+ const o = opts(options);
112
+ return getJson(o.base, DOORS.feeds, o.fetchImpl, o.timeoutMs);
113
+ }
114
+
115
+ /** What changed between the two latest signed snapshots. */
116
+ export function diff(options) {
117
+ const o = opts(options);
118
+ return getJson(o.base, DOORS.diff, o.fetchImpl, o.timeoutMs);
119
+ }
120
+
121
+ /** The defect vocabulary, live. */
122
+ export function defects(options) {
123
+ const o = opts(options);
124
+ return getJson(o.base, DOORS.defects, o.fetchImpl, o.timeoutMs);
125
+ }
126
+
127
+ /**
128
+ * A counted reading with its denominator beside it, as a string a
129
+ * report can print: "3 of 4 rounds". Never a percentage: the store's
130
+ * rule is that counts travel with denominators and a share invites a
131
+ * ranking.
132
+ */
133
+ export function withDenominator(count, of, noun) {
134
+ return `${count} of ${of} ${noun}`;
135
+ }
package/example.mjs ADDED
@@ -0,0 +1,9 @@
1
+ // What the corpus holds about one host. Run: node example.mjs door.example
2
+ import { hostHistory, withDenominator } from "./corpus-client.js";
3
+
4
+ const host = process.argv[2] ?? "door.example";
5
+ const history = await hostHistory(host);
6
+ console.log(JSON.stringify(history, null, 2));
7
+ if (typeof history.rounds_probed === "number" && typeof history.rounds_since_first_sighting === "number") {
8
+ console.log(withDenominator(history.rounds_probed, history.rounds_since_first_sighting, "rounds since first sighting"));
9
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "scvd-corpus-client",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency reader for scvd.store's signed x402 corpus: the weekly census, the fresh set, one host's readiness history, the month, the feeds and the diff, as the store serves them. The scvd CLI's library half.",
5
+ "type": "module",
6
+ "main": "./corpus-client.js",
7
+ "types": "./corpus-client.d.ts",
8
+ "exports": { ".": { "types": "./corpus-client.d.ts", "default": "./corpus-client.js" } },
9
+ "files": ["corpus-client.js", "corpus-client.d.ts", "example.mjs", "CHANGELOG.md", "README.md", "LICENSE"],
10
+ "engines": { "node": ">=18.17.0" },
11
+ "keywords": ["x402", "corpus", "census", "dataset", "readiness", "agent-payments", "evidence", "atom"],
12
+ "license": "MIT",
13
+ "author": "scvd.store",
14
+ "homepage": "https://scvd.store/corpus",
15
+ "repository": { "type": "git", "url": "git+https://github.com/seancrecord/scvd-general-store-repo.git", "directory": "corpus-client" },
16
+ "bugs": { "url": "https://github.com/seancrecord/scvd-general-store-repo/issues" }
17
+ }