@ashlar-blue/x402-trust 0.3.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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/attested-directory.d.ts +78 -0
- package/dist/attested-directory.d.ts.map +1 -0
- package/dist/attested-directory.js +163 -0
- package/dist/attested-directory.js.map +1 -0
- package/dist/badges.d.ts +131 -0
- package/dist/badges.d.ts.map +1 -0
- package/dist/badges.js +213 -0
- package/dist/badges.js.map +1 -0
- package/dist/catalog-mcp.d.ts +145 -0
- package/dist/catalog-mcp.d.ts.map +1 -0
- package/dist/catalog-mcp.js +409 -0
- package/dist/catalog-mcp.js.map +1 -0
- package/dist/census-reconcile.d.ts +69 -0
- package/dist/census-reconcile.d.ts.map +1 -0
- package/dist/census-reconcile.js +162 -0
- package/dist/census-reconcile.js.map +1 -0
- package/dist/directory-classify.d.ts +47 -0
- package/dist/directory-classify.d.ts.map +1 -0
- package/dist/directory-classify.js +54 -0
- package/dist/directory-classify.js.map +1 -0
- package/dist/discovery.d.ts +304 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +926 -0
- package/dist/discovery.js.map +1 -0
- package/dist/evidence-ref.d.ts +96 -0
- package/dist/evidence-ref.d.ts.map +1 -0
- package/dist/evidence-ref.js +179 -0
- package/dist/evidence-ref.js.map +1 -0
- package/dist/facilitator-config.d.ts +83 -0
- package/dist/facilitator-config.d.ts.map +1 -0
- package/dist/facilitator-config.js +94 -0
- package/dist/facilitator-config.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest-sig.d.ts +131 -0
- package/dist/manifest-sig.d.ts.map +1 -0
- package/dist/manifest-sig.js +262 -0
- package/dist/manifest-sig.js.map +1 -0
- package/dist/merkle-transcript.d.ts +34 -0
- package/dist/merkle-transcript.d.ts.map +1 -0
- package/dist/merkle-transcript.js +162 -0
- package/dist/merkle-transcript.js.map +1 -0
- package/dist/open-catalog.d.ts +111 -0
- package/dist/open-catalog.d.ts.map +1 -0
- package/dist/open-catalog.js +307 -0
- package/dist/open-catalog.js.map +1 -0
- package/dist/reconcile.d.ts +150 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +136 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/trust-sdk.d.ts +127 -0
- package/dist/trust-sdk.d.ts.map +1 -0
- package/dist/trust-sdk.js +147 -0
- package/dist/trust-sdk.js.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +68 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// GENERATED from src/x402/census-reconcile.ts by scripts/sdk/sync-sdk.ts — do not edit here.
|
|
2
|
+
// The canonical module is the one FlareClaw runs in production; this is a copy so
|
|
3
|
+
// that what we publish and what we run cannot diverge. Edit the canonical file.
|
|
4
|
+
/**
|
|
5
|
+
* census-reconcile — a checkable statement of where two observers disagree.
|
|
6
|
+
*
|
|
7
|
+
* THE GAP THIS FILLS. Every party in this ecosystem publishes its own index
|
|
8
|
+
* and asks readers to trust it. When two indexes disagree — and they always
|
|
9
|
+
* do — there is no artifact for the disagreement itself, so a reader must pick
|
|
10
|
+
* an observer to believe. Tonight alone: an uptime observatory reporting 36.7%
|
|
11
|
+
* alive while a cross-probe of the same targets reported 92.5%; our own
|
|
12
|
+
* second-observer sweep differing from a peer's on 17 of 1,521 hosts; and a
|
|
13
|
+
* first draft of our own census mislabelling 1,418 hosts.
|
|
14
|
+
*
|
|
15
|
+
* Melchiorre Oliva's phrase for it is the right one: disagreement is the
|
|
16
|
+
* signal. This makes the signal into an object.
|
|
17
|
+
*
|
|
18
|
+
* THE VOCABULARY IS BORROWED ON PURPOSE. Iman Schrock proposed a five-category
|
|
19
|
+
* reconciliation vocabulary in the SCITT disclosure-evidence thread
|
|
20
|
+
* (2026-08-16) and Emek adopted it in -03. It was built for reconciling two
|
|
21
|
+
* populations of settlement evidence, and it fits two populations of discovery
|
|
22
|
+
* observations without modification. Inventing a sixth vocabulary for the same
|
|
23
|
+
* problem would be worse than useless — the value of a category name is that
|
|
24
|
+
* two parties already agree what it means.
|
|
25
|
+
*
|
|
26
|
+
* Iman also corrected his own runner's strongest category DOWNWARD, because a
|
|
27
|
+
* missing record only shows absence from a supplied population. That
|
|
28
|
+
* correction is load-bearing here: this module NEVER concludes which observer
|
|
29
|
+
* is right. It cannot. Two honest observers with different vantages,
|
|
30
|
+
* different clocks and different resolvers will disagree without either being
|
|
31
|
+
* wrong, and an artifact that adjudicates would be asserting more than its
|
|
32
|
+
* inputs support.
|
|
33
|
+
*
|
|
34
|
+
* WHAT A READER GETS. The reconciliation is Merkle-committed, so a third party
|
|
35
|
+
* who trusts NEITHER observer can verify one host's row — "these two disagreed
|
|
36
|
+
* about this domain, this way" — from a handful of hashes, without downloading
|
|
37
|
+
* either census.
|
|
38
|
+
*/
|
|
39
|
+
import { createHash } from 'node:crypto';
|
|
40
|
+
import { jcsCanonical, assertJcsSafe } from './manifest-sig.js';
|
|
41
|
+
import { transcriptRoot, inclusionProof, verifyInclusion } from './merkle-transcript.js';
|
|
42
|
+
import { recomputeRoots } from './attested-directory.js';
|
|
43
|
+
export const RECONCILE_SCHEMA = 'x402-census-reconciliation/1';
|
|
44
|
+
const EMPTY_TALLY = () => ({
|
|
45
|
+
matched: 0, 'observed-by-a-only': 0, 'observed-by-b-only': 0,
|
|
46
|
+
divergent: 0, indeterminate: 0,
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Row leaves carry the domain, both verdicts AND the note. The note was
|
|
50
|
+
* previously outside the commitment, so a relayer could rewrite a verified
|
|
51
|
+
* row's note to adjudicating language ("observer B is known-bad") and the
|
|
52
|
+
* proof would still verify — defeating the no-adjudication doctrine at exactly
|
|
53
|
+
* the layer a third party checks. It is committed now. (Review R2.)
|
|
54
|
+
*/
|
|
55
|
+
const rowLeaf = (r) => {
|
|
56
|
+
const core = { domain: r.domain, category: r.category, a: r.a ?? null, b: r.b ?? null, note: r.note };
|
|
57
|
+
assertJcsSafe(core);
|
|
58
|
+
return {
|
|
59
|
+
drawId: r.domain,
|
|
60
|
+
nonce: r.category,
|
|
61
|
+
seed: '0x' + createHash('sha256').update(jcsCanonical(core), 'utf8').digest('hex'),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const VALID_VERDICTS = new Set([
|
|
65
|
+
'discoverable-verified', 'discoverable', 'one-edit-away', 'not-discoverable', 'unreachable',
|
|
66
|
+
]);
|
|
67
|
+
/** A verdict that admits the observer could not tell. */
|
|
68
|
+
const isIndeterminate = (v) => v === 'unreachable';
|
|
69
|
+
export function reconcile(aCensus, aEntries, bCensus, bEntries, reconciledAt, digestOf) {
|
|
70
|
+
// R1 — the entries must actually be the ones each census committed to.
|
|
71
|
+
// Without this, a reconciler embeds a census's genuine digest as the
|
|
72
|
+
// provenance of verdicts it never produced — a Merkle-committed, retail-
|
|
73
|
+
// provable FABRICATED attribution, in the one artifact whose whole pitch
|
|
74
|
+
// is "checkable by someone who trusts neither observer". Recompute both
|
|
75
|
+
// root pairs and refuse on any mismatch.
|
|
76
|
+
for (const [label, census, entries] of [['a', aCensus, aEntries], ['b', bCensus, bEntries]]) {
|
|
77
|
+
if (entries.length !== census.size) {
|
|
78
|
+
throw new Error(`[reconcile] ${label}: ${entries.length} entries but census.size ${census.size}`);
|
|
79
|
+
}
|
|
80
|
+
const r = recomputeRoots(entries);
|
|
81
|
+
if (r.inputRoot !== census.inputRoot || r.outputRoot !== census.outputRoot) {
|
|
82
|
+
throw new Error(`[reconcile] ${label}: entries do not recompute to the census's committed roots — refusing to attribute them`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// A present-but-invalid verdict must never reach the category ladder: an
|
|
86
|
+
// entry with a dropped/blank verdict would otherwise slip through every
|
|
87
|
+
// guard and land in 'matched' (undefined === undefined), publishing a
|
|
88
|
+
// committed agreement out of a corrupt record. (Silent-failure 2a.)
|
|
89
|
+
for (const [label, entries] of [['a', aEntries], ['b', bEntries]]) {
|
|
90
|
+
for (const e of entries) {
|
|
91
|
+
if (!VALID_VERDICTS.has(e.verdict)) {
|
|
92
|
+
throw new Error(`[reconcile] ${label}: entry ${e.domain} has an out-of-vocabulary verdict ${JSON.stringify(e.verdict)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const aBy = new Map(aEntries.map((e) => [e.domain, e]));
|
|
97
|
+
const bBy = new Map(bEntries.map((e) => [e.domain, e]));
|
|
98
|
+
// The denominator is the UNION. Reconciling only the intersection hides
|
|
99
|
+
// exactly the population difference most worth seeing.
|
|
100
|
+
const domains = [...new Set([...aBy.keys(), ...bBy.keys()])].sort();
|
|
101
|
+
const rows = domains.map((domain) => {
|
|
102
|
+
const av = aBy.get(domain)?.verdict, bv = bBy.get(domain)?.verdict;
|
|
103
|
+
if (av && !bv) {
|
|
104
|
+
return { domain, category: 'observed-by-a-only', a: av,
|
|
105
|
+
note: `in ${aCensus.observer}'s population, absent from ${bCensus.observer}'s — not a claim either missed it` };
|
|
106
|
+
}
|
|
107
|
+
if (bv && !av) {
|
|
108
|
+
return { domain, category: 'observed-by-b-only', b: bv,
|
|
109
|
+
note: `in ${bCensus.observer}'s population, absent from ${aCensus.observer}'s — not a claim either missed it` };
|
|
110
|
+
}
|
|
111
|
+
if (isIndeterminate(av) || isIndeterminate(bv)) {
|
|
112
|
+
return { domain, category: 'indeterminate', ...(av ? { a: av } : {}), ...(bv ? { b: bv } : {}),
|
|
113
|
+
note: 'at least one observer reported a vantage-limited verdict; no comparison is safe' };
|
|
114
|
+
}
|
|
115
|
+
if (av === bv) {
|
|
116
|
+
return { domain, category: 'matched', ...(av ? { a: av } : {}), ...(bv ? { b: bv } : {}), note: 'both observers, same verdict' };
|
|
117
|
+
}
|
|
118
|
+
return { domain, category: 'divergent', ...(av ? { a: av } : {}), ...(bv ? { b: bv } : {}),
|
|
119
|
+
note: 'both observed it and disagree; this artifact does not decide which is correct' };
|
|
120
|
+
});
|
|
121
|
+
const tally = EMPTY_TALLY();
|
|
122
|
+
for (const r of rows)
|
|
123
|
+
tally[r.category]++;
|
|
124
|
+
// Skew is part of the honesty story ("timing explains many divergences"),
|
|
125
|
+
// so an unparseable observedAt is a refusal, not a silent -1 sentinel that
|
|
126
|
+
// still publishes. (Review R3.)
|
|
127
|
+
const aT = Date.parse(aCensus.observedAt), bT = Date.parse(bCensus.observedAt);
|
|
128
|
+
if (!Number.isFinite(aT) || !Number.isFinite(bT)) {
|
|
129
|
+
throw new Error('[reconcile] a census has an unparseable observedAt — cannot compute skew, refusing');
|
|
130
|
+
}
|
|
131
|
+
const skew = Math.abs(aT - bT) / 1000;
|
|
132
|
+
return {
|
|
133
|
+
rows,
|
|
134
|
+
reconciliation: {
|
|
135
|
+
schema: RECONCILE_SCHEMA,
|
|
136
|
+
reconciledAt,
|
|
137
|
+
a: { observer: aCensus.observer, digest: digestOf(aCensus), size: aCensus.size, observedAt: aCensus.observedAt },
|
|
138
|
+
b: { observer: bCensus.observer, digest: digestOf(bCensus), size: bCensus.size, observedAt: bCensus.observedAt },
|
|
139
|
+
size: rows.length,
|
|
140
|
+
rowsRoot: transcriptRoot(rows.map(rowLeaf)),
|
|
141
|
+
tally,
|
|
142
|
+
adjudication: 'none — two honest vantages may differ without either being wrong',
|
|
143
|
+
observationSkewSeconds: Math.round(skew),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Prove ONE host's reconciliation row without disclosing either census. */
|
|
148
|
+
export function proveRow(rows, index) {
|
|
149
|
+
const row = rows[index];
|
|
150
|
+
if (!row)
|
|
151
|
+
throw new Error(`index ${index} outside reconciliation of ${rows.length}`);
|
|
152
|
+
return { row, index, size: rows.length, proof: inclusionProof(rows.map(rowLeaf), index) };
|
|
153
|
+
}
|
|
154
|
+
export function verifyRow(row, index, size, proof, rowsRoot) {
|
|
155
|
+
return verifyInclusion(rowLeaf(row), index, size, proof, rowsRoot);
|
|
156
|
+
}
|
|
157
|
+
/** The digest a reconciliation is anchored under. */
|
|
158
|
+
export function reconciliationDigest(r) {
|
|
159
|
+
assertJcsSafe(r);
|
|
160
|
+
return 'sha256:' + createHash('sha256').update(jcsCanonical(r), 'utf8').digest('hex');
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=census-reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"census-reconcile.js","sourceRoot":"","sources":["../src/census-reconcile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* directory-classify — the ONE place a raw DNS sweep row becomes a directory
|
|
3
|
+
* verdict.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS ITS OWN MODULE. The classification was duplicated across
|
|
6
|
+
* build-directory-census.ts and directory-lane.ts and had already DIVERGED:
|
|
7
|
+
* the two produced different `detail` strings for the same host. Because
|
|
8
|
+
* `detail` is committed inside the census Merkle leaf (attested-directory.ts
|
|
9
|
+
* outputLeaf), that divergence meant the two scripts computed DIFFERENT census
|
|
10
|
+
* digests over identical observations — a reproducibility defect in an
|
|
11
|
+
* artifact whose entire value is that a third party can reproduce it. One
|
|
12
|
+
* source of truth removes the class of bug, not just this instance.
|
|
13
|
+
*
|
|
14
|
+
* THE LOAD-BEARING RULE. A DNS-only method has NO evidence about host
|
|
15
|
+
* reachability. NXDOMAIN on `_x402.<host>` means "no such record", not "host
|
|
16
|
+
* down". So this classifier NEVER emits `unreachable`: that verdict belongs to
|
|
17
|
+
* a method that actually probed the host (the fc-liveness lane), and emitting
|
|
18
|
+
* it here would assert a host was offline on the strength of a missing TXT
|
|
19
|
+
* record. A verdict vocabulary must be narrowed to the claims the method can
|
|
20
|
+
* support; the `detail` strings say exactly which claim is being made.
|
|
21
|
+
*/
|
|
22
|
+
import type { DirectoryEntry } from './attested-directory.js';
|
|
23
|
+
/** One row of the DNS discovery sweep (scripts/x402/dns-discovery-sweep.mjs). */
|
|
24
|
+
export interface SweepRow {
|
|
25
|
+
host: string;
|
|
26
|
+
/** TXT strings at `_x402.<host>`, or null when the query returned nothing. */
|
|
27
|
+
disc: string[] | null;
|
|
28
|
+
/** TXT strings at `s1._x402key.<host>`, or null. */
|
|
29
|
+
key: string[] | null;
|
|
30
|
+
}
|
|
31
|
+
/** A TXT answer with at least one non-blank string. `[]` and `[""]` are empty. */
|
|
32
|
+
export declare function hasContent(a: string[] | null): a is string[];
|
|
33
|
+
/**
|
|
34
|
+
* Classify one sweep row into a committed directory verdict.
|
|
35
|
+
*
|
|
36
|
+
* The returned `detail` is part of the census leaf, so it is treated as a
|
|
37
|
+
* stable, canonical string — changing its wording changes every census digest
|
|
38
|
+
* and is a deliberate, breaking act, not an editorial one.
|
|
39
|
+
*/
|
|
40
|
+
export declare function classifySweepRow(r: SweepRow): DirectoryEntry;
|
|
41
|
+
/**
|
|
42
|
+
* Classify a whole sweep into a DETERMINISTIC, sorted entry list. Row order in
|
|
43
|
+
* a census must not depend on how the sweep happened to finish, or two
|
|
44
|
+
* observers who saw identical facts would still commit to different roots.
|
|
45
|
+
*/
|
|
46
|
+
export declare function classifySweep(rows: SweepRow[]): DirectoryEntry[];
|
|
47
|
+
//# sourceMappingURL=directory-classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directory-classify.d.ts","sourceRoot":"","sources":["../src/directory-classify.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACtB,oDAAoD;IACpD,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACxB;AAED,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,IAAI,MAAM,EAAE,CAE5D;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,GAAG,cAAc,CA8B5D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,cAAc,EAAE,CAEhE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// GENERATED from src/x402/directory-classify.ts by scripts/sdk/sync-sdk.ts — do not edit here.
|
|
2
|
+
// The canonical module is the one FlareClaw runs in production; this is a copy so
|
|
3
|
+
// that what we publish and what we run cannot diverge. Edit the canonical file.
|
|
4
|
+
/** A TXT answer with at least one non-blank string. `[]` and `[""]` are empty. */
|
|
5
|
+
export function hasContent(a) {
|
|
6
|
+
return Array.isArray(a) && a.some((t) => typeof t === 'string' && t.trim().length > 0);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Classify one sweep row into a committed directory verdict.
|
|
10
|
+
*
|
|
11
|
+
* The returned `detail` is part of the census leaf, so it is treated as a
|
|
12
|
+
* stable, canonical string — changing its wording changes every census digest
|
|
13
|
+
* and is a deliberate, breaking act, not an editorial one.
|
|
14
|
+
*/
|
|
15
|
+
export function classifySweepRow(r) {
|
|
16
|
+
// No usable discovery TXT: not discoverable. We distinguish "no record at
|
|
17
|
+
// all" from "a record with no content" in the detail, because they are
|
|
18
|
+
// different facts even though the verdict is the same.
|
|
19
|
+
if (!hasContent(r.disc)) {
|
|
20
|
+
return {
|
|
21
|
+
domain: r.host,
|
|
22
|
+
verdict: 'not-discoverable',
|
|
23
|
+
detail: r.disc === null ? 'no _x402 TXT record (NXDOMAIN or no answer)' : 'empty _x402 TXT content',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const rec = r.disc.find((t) => /v=x402/i.test(t));
|
|
27
|
+
if (!rec) {
|
|
28
|
+
return {
|
|
29
|
+
domain: r.host,
|
|
30
|
+
verdict: 'not-discoverable',
|
|
31
|
+
detail: '_x402 TXT present but no v=x402 record among its strings',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// A signing key published under the host is what upgrades "resolves" to
|
|
35
|
+
// "checkable". Recorded as a distinct verdict rather than flattened.
|
|
36
|
+
const signed = hasContent(r.key) && r.key.some((t) => /v=x402key1/i.test(t));
|
|
37
|
+
return {
|
|
38
|
+
domain: r.host,
|
|
39
|
+
verdict: signed ? 'discoverable-verified' : 'discoverable',
|
|
40
|
+
detail: signed
|
|
41
|
+
? 'record resolves; signing key published under the host'
|
|
42
|
+
: 'record resolves; no signing key in DNS',
|
|
43
|
+
record: rec,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Classify a whole sweep into a DETERMINISTIC, sorted entry list. Row order in
|
|
48
|
+
* a census must not depend on how the sweep happened to finish, or two
|
|
49
|
+
* observers who saw identical facts would still commit to different roots.
|
|
50
|
+
*/
|
|
51
|
+
export function classifySweep(rows) {
|
|
52
|
+
return rows.map(classifySweepRow).sort((a, b) => (a.domain < b.domain ? -1 : 1));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=directory-classify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directory-classify.js","sourceRoot":"","sources":["../src/directory-classify.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,kFAAkF;AAClF,gFAAgF;AAkChF,kFAAkF;AAClF,MAAM,UAAU,UAAU,CAAC,CAAkB;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAW;IACxC,0EAA0E;IAC1E,uEAAuE;IACvE,uDAAuD;IACvD,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO;YACH,MAAM,EAAE,CAAC,CAAC,IAAI;YACd,OAAO,EAAE,kBAAkB;YAC3B,MAAM,EAAE,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,6CAA6C,CAAC,CAAC,CAAC,yBAAyB;SACtG,CAAC;IACN,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,IAAI,CAAC,GAAG,EAAE,CAAC;QACP,OAAO;YACH,MAAM,EAAE,CAAC,CAAC,IAAI;YACd,OAAO,EAAE,kBAAkB;YAC3B,MAAM,EAAE,0DAA0D;SACrE,CAAC;IACN,CAAC;IACD,wEAAwE;IACxE,qEAAqE;IACrE,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO;QACH,MAAM,EAAE,CAAC,CAAC,IAAI;QACd,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,cAAc;QAC1D,MAAM,EAAE,MAAM;YACV,CAAC,CAAC,uDAAuD;YACzD,CAAC,CAAC,wCAAwC;QAC9C,MAAM,EAAE,GAAG;KACd,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAgB;IAC1C,OAAO,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 `discovery` extension — reference implementation.
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/plans/x402/x402-discovery-extension-spec.md (draft for
|
|
5
|
+
* x402-foundation). Two mechanisms:
|
|
6
|
+
* 1. `/.well-known/x402` HTTPS manifest (authoritative capability record)
|
|
7
|
+
* 2. `_x402.<domain>` DNS TXT pointer (`v=x402-1; wk=<manifest url>; …`)
|
|
8
|
+
*
|
|
9
|
+
* This module is pure protocol logic (parse/validate/build) plus a resolver
|
|
10
|
+
* that composes DNS + fetch per the spec's resolution algorithm. The public
|
|
11
|
+
* facilitator serves the manifest; anyone can resolve it with resolveX402().
|
|
12
|
+
*/
|
|
13
|
+
export type DiscoveryKind = 'facilitator' | 'resource-server' | 'both';
|
|
14
|
+
export interface SupportedKind {
|
|
15
|
+
x402Version: number;
|
|
16
|
+
scheme: string;
|
|
17
|
+
network: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DiscoveryAsset {
|
|
20
|
+
network: string;
|
|
21
|
+
address: string;
|
|
22
|
+
symbol?: string;
|
|
23
|
+
decimals?: number;
|
|
24
|
+
standard?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface DiscoveryManifest {
|
|
27
|
+
x402Version: number;
|
|
28
|
+
kind: DiscoveryKind;
|
|
29
|
+
name?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
facilitator?: {
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
endpoints: {
|
|
34
|
+
supported: string;
|
|
35
|
+
verify: string;
|
|
36
|
+
settle: string;
|
|
37
|
+
};
|
|
38
|
+
kinds: SupportedKind[];
|
|
39
|
+
assets?: DiscoveryAsset[];
|
|
40
|
+
};
|
|
41
|
+
resources?: {
|
|
42
|
+
url: string;
|
|
43
|
+
method?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
}[];
|
|
46
|
+
/** Peer hints: bare domain names of other discovery publishers. Hints only — see sanitizePeers. */
|
|
47
|
+
peers?: string[];
|
|
48
|
+
attestation?: {
|
|
49
|
+
type: string;
|
|
50
|
+
[k: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
docs?: string;
|
|
53
|
+
contact?: string;
|
|
54
|
+
updated?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface X402TxtRecord {
|
|
57
|
+
v: string;
|
|
58
|
+
wk: string;
|
|
59
|
+
k?: DiscoveryKind;
|
|
60
|
+
net?: string[];
|
|
61
|
+
scheme?: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a `_x402.<domain>` TXT record ("v=x402-1; wk=https://…; k=…").
|
|
65
|
+
* Returns null for records that aren't x402-1 (callers skip them).
|
|
66
|
+
* Throws on records that claim x402-1 but are malformed.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* A HARD discovery violation — a record or redirect that is present but
|
|
70
|
+
* malformed or spoofing (missing/HTTP wk, invalid k, duplicate key/record,
|
|
71
|
+
* out-of-domain or downgraded redirect). Distinguished by TYPE, not by message
|
|
72
|
+
* text, so classification cannot be fooled by attacker-controlled strings (a
|
|
73
|
+
* wk URL containing the word "conflicting") and cannot silently swallow a
|
|
74
|
+
* malformed record as a DNS miss. These cache as `invalid`; DNS misses do not.
|
|
75
|
+
*/
|
|
76
|
+
export declare class DiscoveryViolation extends Error {
|
|
77
|
+
}
|
|
78
|
+
export declare function parseX402TxtRecord(txt: string): X402TxtRecord | null;
|
|
79
|
+
/**
|
|
80
|
+
* What a `_x402` TXT record IS, when it isn't ours.
|
|
81
|
+
*
|
|
82
|
+
* WHY THIS EXISTS. A census of the Bazaar catalog on 2026-08-22 found five
|
|
83
|
+
* hosts publishing a TXT record at `_x402` under a grammar that is not this
|
|
84
|
+
* one, in three mutually incompatible syntaxes:
|
|
85
|
+
*
|
|
86
|
+
* v=x4021;descriptor=api;url=https://api.telemost.io/.well-known/x402
|
|
87
|
+
* v=x4021;url=https://tablint.dev/.well-known/x402
|
|
88
|
+
* x402-manifest=https://vibesprings.net/.well-known/x402.json
|
|
89
|
+
* https://api.auor.io/.well-known/x402 (bare URL, no key)
|
|
90
|
+
*
|
|
91
|
+
* Every one of them points at its own domain, so this is not an attack — it is
|
|
92
|
+
* independent convergence on the same design. Two of them differ from a
|
|
93
|
+
* conformant record by exactly two tokens.
|
|
94
|
+
*
|
|
95
|
+
* `parseX402TxtRecord` returns null for all of them, which is correct as a GATE
|
|
96
|
+
* and useless as a DIAGNOSIS: a resolver that silently skips a near-miss makes
|
|
97
|
+
* the publisher invisible and tells nobody they were one character away. The
|
|
98
|
+
* fragmentation risk to `_x402` is not that someone attacks the name, it is
|
|
99
|
+
* that several people spell it differently and no implementation ever says so.
|
|
100
|
+
*
|
|
101
|
+
* So: strict enforcement, generous diagnostics — the same doctrine
|
|
102
|
+
* `diagnoseManifest` applies one layer up. THE GATE IS UNCHANGED.
|
|
103
|
+
*/
|
|
104
|
+
export type TxtDiagnosis = {
|
|
105
|
+
kind: 'conformant';
|
|
106
|
+
record: X402TxtRecord;
|
|
107
|
+
}
|
|
108
|
+
/** Claims x402-1 and breaks the grammar. */
|
|
109
|
+
| {
|
|
110
|
+
kind: 'malformed';
|
|
111
|
+
message: string;
|
|
112
|
+
}
|
|
113
|
+
/** Not conformant, but recognizably an attempt at this record. */
|
|
114
|
+
| {
|
|
115
|
+
kind: 'near-miss';
|
|
116
|
+
hints: string[];
|
|
117
|
+
manifestUrl?: string;
|
|
118
|
+
}
|
|
119
|
+
/** A TXT record at this name that is not about x402 at all. */
|
|
120
|
+
| {
|
|
121
|
+
kind: 'foreign';
|
|
122
|
+
};
|
|
123
|
+
export declare function diagnoseX402TxtRecord(txt: string): TxtDiagnosis;
|
|
124
|
+
/** Render the TXT record value for a manifest URL (for operators with DNS). */
|
|
125
|
+
export declare function buildX402TxtRecord(p: {
|
|
126
|
+
manifestUrl: string;
|
|
127
|
+
kind?: DiscoveryKind;
|
|
128
|
+
networks?: string[];
|
|
129
|
+
schemes?: string[];
|
|
130
|
+
}): string;
|
|
131
|
+
/**
|
|
132
|
+
* Spec security rule: the wk URL must live on the queried domain or a
|
|
133
|
+
* subdomain of it — a TXT record may not claim another operator's manifest.
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* A facilitator endpoint value that is safe to concatenate onto baseUrl and
|
|
137
|
+
* fetch: a rooted relative path only. A leading single "/" makes authority
|
|
138
|
+
* injection impossible (nothing after "/" reaches the authority component), and
|
|
139
|
+
* "@", "://", "//" and whitespace/backslash are refused outright. This is the
|
|
140
|
+
* SYNTACTIC guard; isWkInDomain on the assembled URL is the semantic backstop.
|
|
141
|
+
*/
|
|
142
|
+
export declare function isSafeRelativePath(p: unknown): boolean;
|
|
143
|
+
export declare function isWkInDomain(wkUrl: string, domain: string): boolean;
|
|
144
|
+
/** How far up a consumer may walk. Two ancestors, never below two labels. */
|
|
145
|
+
export declare const MAX_ANCESTOR_STEPS = 2;
|
|
146
|
+
/**
|
|
147
|
+
* The `_x402` owner names to query for a host, nearest first.
|
|
148
|
+
*
|
|
149
|
+
* The FIRST entry is the host itself and is always authoritative for it. Later
|
|
150
|
+
* entries are ancestors, and a record found at one is only usable if
|
|
151
|
+
* `manifestCoversHost` accepts it.
|
|
152
|
+
*/
|
|
153
|
+
export declare function discoveryNamesFor(hostOrUrl: string): string[];
|
|
154
|
+
/**
|
|
155
|
+
* Does a manifest published at `ownerName` speak for `host`?
|
|
156
|
+
*
|
|
157
|
+
* Always true when the manifest was found at the host itself. For an ancestor,
|
|
158
|
+
* the manifest MUST name the host — otherwise a platform's record would vouch
|
|
159
|
+
* for every tenant under it, which is how a discovery layer manufactures
|
|
160
|
+
* adopters it does not have.
|
|
161
|
+
*
|
|
162
|
+
* Referenced URLs are only counted when they are themselves in-domain of the
|
|
163
|
+
* owner, so a manifest cannot vouch for a host it does not control either.
|
|
164
|
+
*/
|
|
165
|
+
export declare function manifestCoversHost(m: unknown, ownerName: string, host: string): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* One structural violation, classified by WHO introduced the requirement.
|
|
168
|
+
*
|
|
169
|
+
* The distinction is the point. A host that fails only on `introducedBy:
|
|
170
|
+
* 'x402-discovery'` is one mechanical edit from compliant; a host failing a
|
|
171
|
+
* 'core' rule has a different problem. Reporting them identically is what
|
|
172
|
+
* makes adoption look like a wall instead of an afternoon.
|
|
173
|
+
*/
|
|
174
|
+
export interface ManifestViolation {
|
|
175
|
+
field: string;
|
|
176
|
+
message: string;
|
|
177
|
+
introducedBy: 'x402-discovery' | 'core';
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Collect EVERY structural violation instead of stopping at the first.
|
|
181
|
+
*
|
|
182
|
+
* Why this exists (Melchiorre Oliva on PR #2979, 2026-08-18): our resolver
|
|
183
|
+
* refused his manifest with `invalid kind: undefined` and read nothing else.
|
|
184
|
+
* Everything else in the record was valid, but a first-throw validator cannot
|
|
185
|
+
* say so — the operator learns one field per debugging round-trip and has no
|
|
186
|
+
* way to tell "one edit away" from "fundamentally wrong". He measured 383
|
|
187
|
+
* hosts (39.5% of those serving a manifest) violating nothing except fields
|
|
188
|
+
* this extension introduces. That population is invisible unless a resolver
|
|
189
|
+
* reports the whole diagnosis and says which requirements are new.
|
|
190
|
+
*
|
|
191
|
+
* The GATE ITSELF IS UNCHANGED: resolveX402 still refuses a manifest that
|
|
192
|
+
* fails any rule. Strict enforcement, generous diagnostics.
|
|
193
|
+
*/
|
|
194
|
+
export declare function diagnoseManifest(m: unknown): {
|
|
195
|
+
ok: boolean;
|
|
196
|
+
violations: ManifestViolation[];
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* True when a manifest's ONLY problems are requirements this extension
|
|
200
|
+
* introduced — i.e. it was a valid x402 manifest before this spec and is one
|
|
201
|
+
* mechanical edit from valid after it. This is the population worth counting
|
|
202
|
+
* when anyone asks what the extension costs deployed hosts.
|
|
203
|
+
*/
|
|
204
|
+
export declare function isOneEditAway(m: unknown): boolean;
|
|
205
|
+
/** Structural validation per the spec. Throws with a reason on failure. */
|
|
206
|
+
export declare function validateManifest(m: unknown): DiscoveryManifest;
|
|
207
|
+
/** Spec cap: entries beyond this are ignored (and worth flagging). */
|
|
208
|
+
export declare const MAX_PEER_HINTS = 32;
|
|
209
|
+
/**
|
|
210
|
+
* Filter a manifest's `peers` array down to entries a consumer may act on:
|
|
211
|
+
* bare multi-label DNS names — no scheme, path, port, or userinfo, no IP
|
|
212
|
+
* literals, not the manifest's own domain — capped at MAX_PEER_HINTS.
|
|
213
|
+
* Everything dropped is returned as a signal, mirroring droppedResources.
|
|
214
|
+
*/
|
|
215
|
+
export declare function sanitizePeers(raw: unknown, ownDomain: string): {
|
|
216
|
+
peers: string[];
|
|
217
|
+
dropped: string[];
|
|
218
|
+
};
|
|
219
|
+
/** True for addresses no discovery fetch should ever connect to:
|
|
220
|
+
* loopback, link-local, RFC1918/ULA private ranges, unspecified. */
|
|
221
|
+
export declare function isPrivateAddress(ip: string): boolean;
|
|
222
|
+
export type LookupImpl = (hostname: string) => Promise<string[]>;
|
|
223
|
+
export interface ResolveResult {
|
|
224
|
+
manifest: DiscoveryManifest;
|
|
225
|
+
manifestUrl: string;
|
|
226
|
+
via: 'dns-txt' | 'well-known';
|
|
227
|
+
txtRecord?: X402TxtRecord;
|
|
228
|
+
/** Live GET {baseUrl}{endpoints.supported} result (authoritative over manifest kinds). */
|
|
229
|
+
liveKinds?: SupportedKind[];
|
|
230
|
+
/** True when live kinds and manifest kinds agree (spec: divergence = misconfiguration). */
|
|
231
|
+
kindsMatchLive?: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Outcome of the live cross-check, EXPLICIT so "not checked" can never be
|
|
234
|
+
* read as "checked and matched":
|
|
235
|
+
* ok — fetched, kinds[] present, kindsMatchLive is meaningful
|
|
236
|
+
* skipped — skipLiveCheck, or no facilitator block
|
|
237
|
+
* unreachable — non-2xx, redirect, timeout, size breach, or bad JSON
|
|
238
|
+
* malformed — 2xx but no kinds[] array to compare
|
|
239
|
+
* refused — the endpoint was not a safe same-origin path
|
|
240
|
+
*/
|
|
241
|
+
liveCheck?: 'ok' | 'skipped' | 'unreachable' | 'malformed' | 'refused';
|
|
242
|
+
/**
|
|
243
|
+
* Resource URLs removed because they pointed off-domain or weren't
|
|
244
|
+
* HTTPS. NON-EMPTY IS A SIGNAL, not noise: the spec invites indexers
|
|
245
|
+
* to probe `resources[]`, so an off-domain entry is an attempt to
|
|
246
|
+
* aim a stranger's crawler somewhere. Surface it; don't just drop it.
|
|
247
|
+
*/
|
|
248
|
+
droppedResources?: string[];
|
|
249
|
+
/**
|
|
250
|
+
* Peer hints that survived sanitizePeers — names only, each still to
|
|
251
|
+
* be resolved from scratch. Never act on a peer without re-resolving.
|
|
252
|
+
*/
|
|
253
|
+
peers?: string[];
|
|
254
|
+
/** Peer entries dropped by sanitizePeers. Non-empty is a manifest-quality signal. */
|
|
255
|
+
droppedPeers?: string[];
|
|
256
|
+
/**
|
|
257
|
+
* False whenever an `attestation` block cannot be independently checked
|
|
258
|
+
* (absent, `type: none`, or no dereferenceable HTTPS verifier URL).
|
|
259
|
+
* Consumers MUST treat a false value as "not attested" — the spec's
|
|
260
|
+
* "absent verification, treat as `none`" rule, made machine-readable so
|
|
261
|
+
* a caller can't accidentally believe an unverifiable claim.
|
|
262
|
+
*/
|
|
263
|
+
attestationUsable?: boolean;
|
|
264
|
+
}
|
|
265
|
+
export type NegativeReason = 'no-record' | 'unreachable' | 'invalid';
|
|
266
|
+
export declare const NEGATIVE_TTL_MS: Readonly<Record<NegativeReason, number>>;
|
|
267
|
+
export declare class X402NegativeCacheError extends Error {
|
|
268
|
+
readonly reason: NegativeReason;
|
|
269
|
+
readonly cachedMessage: string;
|
|
270
|
+
constructor(reason: NegativeReason, cachedMessage: string, domain: string);
|
|
271
|
+
}
|
|
272
|
+
/** Drop cached negatives — tests, and operator triage after a fix. */
|
|
273
|
+
export declare function clearDiscoveryCache(domain?: string): void;
|
|
274
|
+
/** Inspect the cache without mutating it (observability, tests). */
|
|
275
|
+
export declare function discoveryCacheEntry(domain: string, now?: number): {
|
|
276
|
+
reason: NegativeReason;
|
|
277
|
+
message: string;
|
|
278
|
+
expiresInMs: number;
|
|
279
|
+
} | null;
|
|
280
|
+
/**
|
|
281
|
+
* Resolve a domain's x402 capability per the spec:
|
|
282
|
+
* TXT `_x402.<domain>` → manifest URL, else `https://<domain>/.well-known/x402`;
|
|
283
|
+
* then cross-check the facilitator's live `supported` endpoint.
|
|
284
|
+
*/
|
|
285
|
+
export declare function resolveX402(domain: string, opts?: {
|
|
286
|
+
fetchImpl?: typeof fetch;
|
|
287
|
+
resolveTxt?: (name: string) => Promise<string[][]>;
|
|
288
|
+
skipLiveCheck?: boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Address resolution for the private-destination guard. Defaults to
|
|
291
|
+
* real DNS when using the real `fetch`; when a fetchImpl is injected
|
|
292
|
+
* (tests, transports that resolve internally) the guard runs only if
|
|
293
|
+
* a lookupImpl is supplied — a fake fetch makes real DNS both
|
|
294
|
+
* meaningless and a hidden network dependency.
|
|
295
|
+
*/
|
|
296
|
+
lookupImpl?: LookupImpl;
|
|
297
|
+
/** Skip the negative cache for this call (forced refresh). */
|
|
298
|
+
noCache?: boolean;
|
|
299
|
+
/** Override negative TTLs (tests, aggressive crawlers). */
|
|
300
|
+
negativeTtlMs?: Partial<Record<NegativeReason, number>>;
|
|
301
|
+
/** Injectable clock so TTL expiry is testable without waiting. */
|
|
302
|
+
now?: () => number;
|
|
303
|
+
}): Promise<ResolveResult>;
|
|
304
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
|