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 +15 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/corpus-client.d.ts +24 -0
- package/corpus-client.js +135 -0
- package/example.mjs +9 -0
- package/package.json +17 -0
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;
|
package/corpus-client.js
ADDED
|
@@ -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
|
+
}
|