@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Walter D. Hawkins
|
|
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,198 @@
|
|
|
1
|
+
# @ashlar-blue/x402-trust
|
|
2
|
+
|
|
3
|
+
**Find an x402 facilitator, and decide whether to trust it before you pay.**
|
|
4
|
+
|
|
5
|
+
Reference implementation of two proposed x402 extensions:
|
|
6
|
+
|
|
7
|
+
- **`discovery`** ([x402-foundation/x402 #2979](https://github.com/x402-foundation/x402/pull/2979)) — find a facilitator from a domain name via DNS `TXT` + `/.well-known/x402`. No central directory, no list, no gatekeeper.
|
|
8
|
+
- **`attestations`** ([#3000](https://github.com/x402-foundation/x402/pull/3000)) — verify what a service claims about itself *before* paying it: TEE attestation blocks and soulbound, revocable, evidence-linked reputation records.
|
|
9
|
+
|
|
10
|
+
Discovery finds a service. Attestations tell you whether to trust it.
|
|
11
|
+
|
|
12
|
+
## Three verbs
|
|
13
|
+
|
|
14
|
+
An agent that spends money has to do three things. This library does each in a
|
|
15
|
+
handful of lines, with **zero dependencies** and — for the two verification
|
|
16
|
+
verbs — **zero network and zero trust in us**.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { checkService, Transcript, verifyReceipt } from '@ashlar-blue/x402-trust';
|
|
20
|
+
|
|
21
|
+
// 1. BEFORE YOU PAY — resolve a service and get a plain decision + reasons.
|
|
22
|
+
const t = await checkService('facilitator.example');
|
|
23
|
+
if (!t.safeToPay) throw new Error(t.reasons.join('; ')); // e.g. off-domain resource, stale manifest
|
|
24
|
+
|
|
25
|
+
// 2. WHAT YOU PRODUCE — commit the work your agent did to a Merkle transcript.
|
|
26
|
+
const work = new Transcript()
|
|
27
|
+
.add('inv-002', 'GET /price?asset=XRP', '{"price":"2.99"}');
|
|
28
|
+
const root = work.root(); // sign or anchor this ONE value
|
|
29
|
+
const receipt = work.prove(0); // hand a counterparty a proof of just their line
|
|
30
|
+
|
|
31
|
+
// 3. WHAT YOU'RE HANDED — verify a receipt offline, trusting no one.
|
|
32
|
+
verifyReceipt(receipt); // true iff the work was in the committed set
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`checkService` composes the hardened resolver (same-origin enforcement,
|
|
36
|
+
SSRF/DoS guards, HTTPS-only redirects, live `/supported` cross-check) into one
|
|
37
|
+
call, so you do not re-implement the adversarial edge cases. The verification
|
|
38
|
+
functions are pure [RFC 6962](https://www.rfc-editor.org/rfc/rfc6962) Merkle
|
|
39
|
+
math: if a proof verifies, the claim is true no matter who produced it — an
|
|
40
|
+
enterprise does not have to trust FlareClaw to use FlareClaw's proofs.
|
|
41
|
+
|
|
42
|
+
Three more offline verifiers cover the artifacts the catalog and reconciler
|
|
43
|
+
emit — `verifyDirectoryEntry` (one host in a census), `verifyReconcileRow` (one
|
|
44
|
+
row of a two-observer reconciliation), and the signed-manifest / evidence
|
|
45
|
+
grammar below.
|
|
46
|
+
|
|
47
|
+
**Runnable tour:** [`examples/demo.mjs`](./examples/demo.mjs) — `npm run build && node examples/demo.mjs`. Produces work, verifies it, catches a tamper, and refuses a spoofed facilitator, all offline.
|
|
48
|
+
|
|
49
|
+
The rest of this document is the building blocks `checkService` sits on, for
|
|
50
|
+
callers who want the lower-level surface.
|
|
51
|
+
|
|
52
|
+
## The Open Catalog, self-hostable (staged for 0.2.0)
|
|
53
|
+
|
|
54
|
+
Two further modules ship the catalog itself as a reference implementation —
|
|
55
|
+
the "anyone can rebuild this from public data" claim, made executable:
|
|
56
|
+
|
|
57
|
+
- **`open-catalog`** — the catalog engine: mirror the CDP Bazaar via its own
|
|
58
|
+
public API (with attribution), crawl `discovery` records breadth-first with
|
|
59
|
+
per-operator frontier-diversity bounds, and serve a provenance-tagged,
|
|
60
|
+
sha256-digested snapshot. Run it yourself and you are an independent
|
|
61
|
+
observer, not a mirror of ours.
|
|
62
|
+
- **`catalog-mcp`** — a transport-free MCP server core for that catalog:
|
|
63
|
+
stateless protocol revision 2026-07-28 primary, sessionless legacy
|
|
64
|
+
`initialize` shim, four read tools, a signable Server Card, and evidence
|
|
65
|
+
(snapshot digest + on-chain anchor) carried in BOTH `_meta` and readable
|
|
66
|
+
content. Wire it to any HTTP framework in ~20 lines; every dependency is
|
|
67
|
+
injected.
|
|
68
|
+
|
|
69
|
+
## Zero dependencies
|
|
70
|
+
|
|
71
|
+
`npm ls` returns nothing. That is deliberate: a library whose job is *"decide whether to trust a stranger before sending them money"* cannot credibly arrive with a dependency tree you will never read. The entire trust path — DNS record parsing, manifest validation, redirect handling, the `eth_call`, the digest grammar — is auditable in one sitting.
|
|
72
|
+
|
|
73
|
+
The one consequence: we cannot compute `keccak256` at runtime, so the badge function selector and well-known `kind` hashes are compile-time constants. They are [asserted against `ethers` in the test suite](https://gitlab.com/flareclaw-saving-demo-group/flareclaw-verifier), so they are verified, not trusted. You can pass your own hasher for any other kind.
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install @ashlar-blue/x402-trust
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Find a facilitator from a domain name
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { resolveX402 } from '@ashlar-blue/x402-trust';
|
|
85
|
+
|
|
86
|
+
const r = await resolveX402('flareclaw.app');
|
|
87
|
+
|
|
88
|
+
r.via; // 'dns-txt' — found via _x402.flareclaw.app
|
|
89
|
+
r.manifest.facilitator.baseUrl;
|
|
90
|
+
r.liveKinds; // fetched from /supported — authoritative over the manifest
|
|
91
|
+
r.kindsMatchLive; // false = the operator's manifest is stale
|
|
92
|
+
r.attestationUsable // false = treat any TEE claim as absent
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Runs in Node out of the box. In a browser or a worker, inject a DNS resolver (DoH) and it works unchanged:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
await resolveX402('flareclaw.app', { resolveTxt: myDohResolver, fetchImpl: fetch });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### What it refuses to do
|
|
102
|
+
|
|
103
|
+
These are the extension's security rules, enforced rather than documented:
|
|
104
|
+
|
|
105
|
+
| Situation | Behaviour |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `wk` in the TXT record points off-domain | hard error (spoofing signal) |
|
|
108
|
+
| Manifest redirects off-domain | hard error — the in-domain rule is re-applied to **every hop**, so the check runs on where the bytes came from, not where you asked |
|
|
109
|
+
| `facilitator.baseUrl` is off-domain | hard error — otherwise any domain could claim someone else's facilitator, and point every crawler at them |
|
|
110
|
+
| TEE block whose `verifier` is not a dereferenceable HTTPS URL | `attestationUsable: false` — an unverifiable claim must not read as a verified one |
|
|
111
|
+
|
|
112
|
+
## Decide whether to trust it
|
|
113
|
+
|
|
114
|
+
The important part, and the part most callers get wrong:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { checkOperatorTrust } from '@ashlar-blue/x402-trust';
|
|
118
|
+
|
|
119
|
+
const trust = await checkOperatorTrust({
|
|
120
|
+
manifest: r.manifest,
|
|
121
|
+
rpcUrl: 'https://coston2-api.flare.network/ext/C/rpc',
|
|
122
|
+
registry: '0xb02f83e994830C4954c89C10482665A3963229c5', // PIN THIS
|
|
123
|
+
subject: r.manifest.badges.subject,
|
|
124
|
+
kind: 'x402-facilitator-attested',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
trust.verified; // was this operator ever verified? (registry)
|
|
128
|
+
trust.liveAttestation; // is it in that mode right now? (manifest)
|
|
129
|
+
trust.attestedNow; // both — this is what you gate on
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Why these are three values and not one
|
|
133
|
+
|
|
134
|
+
A registry record is soulbound, therefore **durable**. TEE attestation is **transient**. Point the first at the second and you get a claim that is true when written and silently false later, with nothing in the record to reveal the drift.
|
|
135
|
+
|
|
136
|
+
We shipped exactly that bug. Our own `x402-facilitator-attested` record read `active` on-chain while our own manifest honestly reported `attestation: none`, because the enclave was down. The record was not lying about what it attested — a verification really did happen, and its evidence still hashes — it was being *read* as a liveness signal it cannot carry.
|
|
137
|
+
|
|
138
|
+
So:
|
|
139
|
+
|
|
140
|
+
| Question | Source of truth |
|
|
141
|
+
|---|---|
|
|
142
|
+
| *Was this operator verified, by whom, against what evidence?* | the registry record (durable, revocable) |
|
|
143
|
+
| *Is it operating that way right now?* | the operator's live manifest (self-degrading) |
|
|
144
|
+
|
|
145
|
+
`checkOperatorTrust` keeps them apart and only combines them explicitly. It **fails closed**: anything it cannot establish yields `attestedNow: false`.
|
|
146
|
+
|
|
147
|
+
`hasActiveBadge` likewise **throws rather than returning `false`** when it cannot reach the chain. "I could not check" and "I checked and it is not active" are different answers, and collapsing them is how an RPC outage becomes a silent trust upgrade.
|
|
148
|
+
|
|
149
|
+
## Drop into the official SDK (`@x402/core`, Stripe Machine Payments)
|
|
150
|
+
|
|
151
|
+
Every integration built from the [Stripe Machine Payments sample](https://docs.stripe.com/payments/machine/x402) (and most others built on `@x402/core`) hardcodes its facilitator:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { facilitator } from "@coinbase/x402"; // ← a constant
|
|
155
|
+
const client = new HTTPFacilitatorClient(facilitator);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`discoverFacilitator()` turns that constant into a resolved, verified choice — one line changes:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { HTTPFacilitatorClient } from "@x402/core/server";
|
|
162
|
+
import { discoverFacilitator } from "@ashlar-blue/x402-trust";
|
|
163
|
+
|
|
164
|
+
const { config, resolution } = await discoverFacilitator("facilitator.example.com", {
|
|
165
|
+
scheme: "exact",
|
|
166
|
+
network: "eip155:8453", // require support, judged against the LIVE /supported
|
|
167
|
+
});
|
|
168
|
+
const client = new HTTPFacilitatorClient(config); // drop-in
|
|
169
|
+
|
|
170
|
+
resolution.attestationUsable; // and you know whether its TEE claim is checkable
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
No dependency on `@x402/core` is taken — the returned `config` is a plain `{ url, timeoutMs? }` matching core's `FacilitatorConfig` shape, so your SDK version is the only SDK version involved.
|
|
174
|
+
|
|
175
|
+
One honest constraint: `HTTPFacilitatorClient` calls the fixed paths `/verify`, `/settle`, `/supported`. A discovered manifest declaring different endpoint paths cannot be expressed as a `FacilitatorConfig`, so `discoverFacilitator()` **fails at resolve time** (`reason: "endpoints"`) instead of handing you a config that 404s at pay time.
|
|
176
|
+
|
|
177
|
+
## Evidence references
|
|
178
|
+
|
|
179
|
+
Records point at the evidence that justified them. A bare URI has the trust model of brand reputation — it can be edited or taken down after the fact — so records carry a digest:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { encodeEvidenceRef, parseEvidenceRef, sameEvidence, canonicalJson } from '@ashlar-blue/x402-trust';
|
|
183
|
+
|
|
184
|
+
const ref = encodeEvidenceRef({
|
|
185
|
+
alg: 'sha256',
|
|
186
|
+
hex: '...',
|
|
187
|
+
ref: 'https://api.flareclaw.app/evidence/<digest>.json',
|
|
188
|
+
});
|
|
189
|
+
// "x402ev/1; digest=sha256:...; ref=https://..."
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Verify one by fetching the artifact, canonicalizing it ([RFC 8785 JCS](https://www.rfc-editor.org/rfc/rfc8785)), hashing, and comparing to the digest. A fetched artifact that **mismatches** its digest is evidence-invalid and must be a hard negative; an artifact that is merely **unreachable** should down-weight the record, not revoke it. Link rot and tampering are different failures.
|
|
193
|
+
|
|
194
|
+
## Status
|
|
195
|
+
|
|
196
|
+
`0.2.0`. Both extensions are open pull requests, not ratified standards — the wire formats may change in response to review, and this package tracks them. It is published because a specification with an installable implementation is a proposal with users attached, and one without is a PDF. Requires Node ≥20 (the manifest signer validates UTF-16 well-formedness via `String.prototype.isWellFormed`).
|
|
197
|
+
|
|
198
|
+
MIT.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export declare const DIRECTORY_SCHEMA = "x402-attested-directory/1";
|
|
2
|
+
/**
|
|
3
|
+
* What a census can say about a host. Ordered strongest to weakest claim, and
|
|
4
|
+
* deliberately parallel to the fc-liveness vocabulary: `unreachable` is a
|
|
5
|
+
* statement about OUR vantage on this date, never about the host.
|
|
6
|
+
*/
|
|
7
|
+
export type DirectoryVerdict = 'discoverable-verified' | 'discoverable' | 'one-edit-away' | 'not-discoverable' | 'unreachable';
|
|
8
|
+
export interface DirectoryEntry {
|
|
9
|
+
domain: string;
|
|
10
|
+
verdict: DirectoryVerdict;
|
|
11
|
+
/** One line a reader can check against the columns, never an accusation. */
|
|
12
|
+
detail: string;
|
|
13
|
+
/** The record as observed, verbatim, when there was one. */
|
|
14
|
+
record?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Census {
|
|
17
|
+
schema: typeof DIRECTORY_SCHEMA;
|
|
18
|
+
observedAt: string;
|
|
19
|
+
observer: string;
|
|
20
|
+
/** sha256 of the program that produced this. "Re-run it" needs a referent. */
|
|
21
|
+
methodDigest: string;
|
|
22
|
+
/** How the target set was chosen — a census over a hand-picked set is not a census. */
|
|
23
|
+
targetSource: string;
|
|
24
|
+
size: number;
|
|
25
|
+
inputRoot: string;
|
|
26
|
+
outputRoot: string;
|
|
27
|
+
tally: Record<DirectoryVerdict, number>;
|
|
28
|
+
/** Present ONLY when the run was genuinely attested. Absent means not. */
|
|
29
|
+
attestation?: {
|
|
30
|
+
type: string;
|
|
31
|
+
imageDigest?: string;
|
|
32
|
+
tokenDigest?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function buildCensus(entries: DirectoryEntry[], meta: {
|
|
36
|
+
observer: string;
|
|
37
|
+
methodDigest: string;
|
|
38
|
+
targetSource: string;
|
|
39
|
+
observedAt: string;
|
|
40
|
+
attestation?: Census['attestation'];
|
|
41
|
+
}): Census;
|
|
42
|
+
/** The digest a census is anchored under. */
|
|
43
|
+
export declare function censusDigest(c: Census): string;
|
|
44
|
+
/** Prove ONE host's verdict without disclosing the rest of the census. */
|
|
45
|
+
export declare function proveEntry(entries: DirectoryEntry[], index: number): {
|
|
46
|
+
entry: DirectoryEntry;
|
|
47
|
+
index: number;
|
|
48
|
+
size: number;
|
|
49
|
+
proof: string[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Check one host's verdict against a published outputRoot, holding nothing
|
|
53
|
+
* else. This is what makes the census checkable retail rather than wholesale.
|
|
54
|
+
*/
|
|
55
|
+
export declare function verifyEntry(entry: DirectoryEntry, index: number, size: number, proof: string[], outputRoot: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Re-derive both roots from a full target list and result set. A second
|
|
58
|
+
* observer runs this against their OWN sweep: matching roots mean identical
|
|
59
|
+
* observations, and differing roots localise the disagreement rather than
|
|
60
|
+
* hiding it. Disagreement is the signal.
|
|
61
|
+
*/
|
|
62
|
+
export declare function recomputeRoots(entries: DirectoryEntry[]): {
|
|
63
|
+
inputRoot: string;
|
|
64
|
+
outputRoot: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Compare two censuses over the same targets. Returns the domains whose
|
|
68
|
+
* verdicts differ — the two-observer diff that makes a directory falsifiable.
|
|
69
|
+
*/
|
|
70
|
+
export declare function diffCensus(a: DirectoryEntry[], b: DirectoryEntry[]): {
|
|
71
|
+
sameTargets: boolean;
|
|
72
|
+
disagreements: {
|
|
73
|
+
domain: string;
|
|
74
|
+
a: DirectoryVerdict;
|
|
75
|
+
b: DirectoryVerdict;
|
|
76
|
+
}[];
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=attested-directory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attested-directory.d.ts","sourceRoot":"","sources":["../src/attested-directory.ts"],"names":[],"mappings":"AA6CA,eAAO,MAAM,gBAAgB,8BAA8B,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GACtB,uBAAuB,GACvB,cAAc,GACd,eAAe,GACf,kBAAkB,GAClB,aAAa,CAAC;AAEpB,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,CAAC;IAC1B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,MAAM;IACnB,MAAM,EAAE,OAAO,gBAAgB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;IACxC,0EAA0E;IAC1E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9E;AA2DD,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE;IACzD,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;CACvC,GAAG,MAAM,CAgBT;AAED,6CAA6C;AAC7C,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9C;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG;IAClE,KAAK,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;CACvE,CAIA;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACvB,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GACxF,OAAO,CAET;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAKnG;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,CAAC,EAAE,cAAc,EAAE,GAAG;IAClE,WAAW,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,gBAAgB,CAAC;QAAC,CAAC,EAAE,gBAAgB,CAAA;KAAE,EAAE,CAAC;CACvG,CAWA"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// GENERATED from src/x402/attested-directory.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
|
+
* attested-directory — a census of x402 discovery records that a stranger can
|
|
6
|
+
* check without trusting the party who produced it.
|
|
7
|
+
*
|
|
8
|
+
* THE PROBLEM THIS EXISTS FOR. Every directory in this ecosystem asks you to
|
|
9
|
+
* trust an index. The measured consequence (our own sweep, 2026-08-18) is a
|
|
10
|
+
* catalogue where 78% of listings have exactly one payer ever, five listings
|
|
11
|
+
* carry 45% of all traffic, and listings age out for being unpopular. An index
|
|
12
|
+
* that deletes you for having no customers is not infrastructure, and "trust
|
|
13
|
+
* our index" is not a claim anyone should accept — including about ours.
|
|
14
|
+
*
|
|
15
|
+
* WHAT REPLACES THE TRUST. A census here commits to three things at once:
|
|
16
|
+
*
|
|
17
|
+
* methodDigest WHICH PROGRAM ran. Content-addressed, so "re-run it" is a
|
|
18
|
+
* well-defined instruction rather than an invitation to write
|
|
19
|
+
* your own and hope it matches.
|
|
20
|
+
* inputRoot WHICH TARGETS it ran over. Without this a publisher can
|
|
21
|
+
* quietly drop hosts whose verdicts it dislikes and no reader
|
|
22
|
+
* can tell — the omission problem, applied to directories.
|
|
23
|
+
* outputRoot WHAT IT FOUND, as an RFC 6962 tree rather than a flat hash.
|
|
24
|
+
*
|
|
25
|
+
* The tree is the part that makes it usable. A flat digest over 1,564 verdicts
|
|
26
|
+
* forces anyone checking ONE host to download all 1,564 and recompute. With a
|
|
27
|
+
* Merkle root, "host X had verdict Y in census C" is eleven hashes, and the
|
|
28
|
+
* reader never sees the other 1,563 — which matters because a directory that
|
|
29
|
+
* can only be checked wholesale will not be checked at all.
|
|
30
|
+
*
|
|
31
|
+
* WHAT THIS DOES NOT CLAIM. Being signed inside an enclave would prove the
|
|
32
|
+
* program was not tampered with between build and run. That is a stronger
|
|
33
|
+
* claim than a census signed anywhere else, and it is deliberately NOT made
|
|
34
|
+
* here: this module produces the artifact, and `attestation` is populated only
|
|
35
|
+
* when a caller supplies a real one. A census with no attestation says so.
|
|
36
|
+
*
|
|
37
|
+
* Discovery is not endorsement. A verdict describes what a program observed
|
|
38
|
+
* from one vantage at one time. It says nothing about whether the operator is
|
|
39
|
+
* honest, and the vocabulary below is chosen so it cannot be read that way.
|
|
40
|
+
*/
|
|
41
|
+
import { createHash } from 'node:crypto';
|
|
42
|
+
import { jcsCanonical, assertJcsSafe } from './manifest-sig.js';
|
|
43
|
+
import { transcriptRoot, inclusionProof, verifyInclusion } from './merkle-transcript.js';
|
|
44
|
+
export const DIRECTORY_SCHEMA = 'x402-attested-directory/1';
|
|
45
|
+
/** Targets as tree leaves: the question asked, independent of the answer. */
|
|
46
|
+
const inputLeaf = (domain) => ({ drawId: domain, nonce: 'x402-directory-target', seed: '0x00' });
|
|
47
|
+
/**
|
|
48
|
+
* Verdict leaves. The domain is inside the leaf, so a proof cannot be moved
|
|
49
|
+
* from one host to another — without it, a verdict for a host that passed
|
|
50
|
+
* could be replayed as a verdict for one that did not.
|
|
51
|
+
*/
|
|
52
|
+
const outputLeaf = (e) => {
|
|
53
|
+
// Commit the OBSERVED RECORD, not just the verdict and prose. Without it a
|
|
54
|
+
// proof bundle's `record` could be swapped for arbitrary content and still
|
|
55
|
+
// verify — a "verified" census entry carrying a fabricated record — and
|
|
56
|
+
// two sweeps differing only in the observed record would recompute
|
|
57
|
+
// identical roots, so recomputeRoots would miss a real observation
|
|
58
|
+
// difference. `record ?? null` keeps the leaf defined when a host has no
|
|
59
|
+
// record. (Crypto review D2, 2026-08-19.)
|
|
60
|
+
const core = { domain: e.domain, verdict: e.verdict, detail: e.detail, record: e.record ?? null };
|
|
61
|
+
assertJcsSafe(core);
|
|
62
|
+
return {
|
|
63
|
+
drawId: e.domain,
|
|
64
|
+
nonce: e.verdict,
|
|
65
|
+
seed: '0x' + createHash('sha256').update(jcsCanonical(core), 'utf8').digest('hex'),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Reject duplicate domains. A census with the same domain twice — one verdict
|
|
70
|
+
* at index i, a different one at index j — lets a publisher anchor ONE digest
|
|
71
|
+
* and then serve each retail verifier a valid inclusion proof for whichever
|
|
72
|
+
* verdict suits, both verifying against the same outputRoot. That is exactly
|
|
73
|
+
* the equivocation CT log operators are audited against, and it makes the
|
|
74
|
+
* retail claim "host X had verdict Y in census C" ill-defined. (Review D1.)
|
|
75
|
+
*/
|
|
76
|
+
function assertCanonicalOrder(entries) {
|
|
77
|
+
// Unique AND ascending by domain, asserted rather than imposed. buildCensus
|
|
78
|
+
// must NOT silently re-sort: proveEntry/verifyEntry operate on the SAME
|
|
79
|
+
// array a caller holds, so if buildCensus reordered internally, a caller's
|
|
80
|
+
// index-based proof would not verify against the census's own outputRoot.
|
|
81
|
+
// classifySweep already yields this order; anything else is a caller bug.
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
let prev = '';
|
|
84
|
+
for (const e of entries) {
|
|
85
|
+
if (seen.has(e.domain))
|
|
86
|
+
throw new Error(`[x402-directory] duplicate domain in census: ${e.domain}`);
|
|
87
|
+
if (prev && e.domain < prev) {
|
|
88
|
+
throw new Error(`[x402-directory] entries must be sorted by domain (${e.domain} after ${prev}) — use classifySweep`);
|
|
89
|
+
}
|
|
90
|
+
seen.add(e.domain);
|
|
91
|
+
prev = e.domain;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const EMPTY_TALLY = () => ({
|
|
95
|
+
'discoverable-verified': 0, discoverable: 0, 'one-edit-away': 0,
|
|
96
|
+
'not-discoverable': 0, unreachable: 0,
|
|
97
|
+
});
|
|
98
|
+
export function buildCensus(entries, meta) {
|
|
99
|
+
assertCanonicalOrder(entries);
|
|
100
|
+
const tally = EMPTY_TALLY();
|
|
101
|
+
for (const e of entries)
|
|
102
|
+
tally[e.verdict]++;
|
|
103
|
+
return {
|
|
104
|
+
schema: DIRECTORY_SCHEMA,
|
|
105
|
+
observedAt: meta.observedAt,
|
|
106
|
+
observer: meta.observer,
|
|
107
|
+
methodDigest: meta.methodDigest,
|
|
108
|
+
targetSource: meta.targetSource,
|
|
109
|
+
size: entries.length,
|
|
110
|
+
inputRoot: transcriptRoot(entries.map((e) => inputLeaf(e.domain))),
|
|
111
|
+
outputRoot: transcriptRoot(entries.map(outputLeaf)),
|
|
112
|
+
tally,
|
|
113
|
+
...(meta.attestation ? { attestation: meta.attestation } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** The digest a census is anchored under. */
|
|
117
|
+
export function censusDigest(c) {
|
|
118
|
+
assertJcsSafe(c);
|
|
119
|
+
return 'sha256:' + createHash('sha256').update(jcsCanonical(c), 'utf8').digest('hex');
|
|
120
|
+
}
|
|
121
|
+
/** Prove ONE host's verdict without disclosing the rest of the census. */
|
|
122
|
+
export function proveEntry(entries, index) {
|
|
123
|
+
const entry = entries[index];
|
|
124
|
+
if (!entry)
|
|
125
|
+
throw new Error(`index ${index} outside census of ${entries.length}`);
|
|
126
|
+
return { entry, index, size: entries.length, proof: inclusionProof(entries.map(outputLeaf), index) };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check one host's verdict against a published outputRoot, holding nothing
|
|
130
|
+
* else. This is what makes the census checkable retail rather than wholesale.
|
|
131
|
+
*/
|
|
132
|
+
export function verifyEntry(entry, index, size, proof, outputRoot) {
|
|
133
|
+
return verifyInclusion(outputLeaf(entry), index, size, proof, outputRoot);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Re-derive both roots from a full target list and result set. A second
|
|
137
|
+
* observer runs this against their OWN sweep: matching roots mean identical
|
|
138
|
+
* observations, and differing roots localise the disagreement rather than
|
|
139
|
+
* hiding it. Disagreement is the signal.
|
|
140
|
+
*/
|
|
141
|
+
export function recomputeRoots(entries) {
|
|
142
|
+
return {
|
|
143
|
+
inputRoot: transcriptRoot(entries.map((e) => inputLeaf(e.domain))),
|
|
144
|
+
outputRoot: transcriptRoot(entries.map(outputLeaf)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Compare two censuses over the same targets. Returns the domains whose
|
|
149
|
+
* verdicts differ — the two-observer diff that makes a directory falsifiable.
|
|
150
|
+
*/
|
|
151
|
+
export function diffCensus(a, b) {
|
|
152
|
+
const byDomainB = new Map(b.map((e) => [e.domain, e]));
|
|
153
|
+
const sameTargets = a.length === b.length && a.every((e) => byDomainB.has(e.domain));
|
|
154
|
+
const disagreements = [];
|
|
155
|
+
for (const e of a) {
|
|
156
|
+
const other = byDomainB.get(e.domain);
|
|
157
|
+
if (other && other.verdict !== e.verdict) {
|
|
158
|
+
disagreements.push({ domain: e.domain, a: e.verdict, b: other.verdict });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { sameTargets, disagreements };
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=attested-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attested-directory.js","sourceRoot":"","sources":["../src/attested-directory.ts"],"names":[],"mappings":"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"}
|
package/dist/badges.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keccak256(utf8(kind)) for the kinds this ecosystem has defined so far.
|
|
3
|
+
* Callers may also pass a pre-hashed 0x-prefixed bytes32 for any other
|
|
4
|
+
* kind, or supply their own keccak via `hashKind`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const KNOWN_BADGE_KINDS: Readonly<Record<string, string>>;
|
|
7
|
+
/** Resolve a kind name (or pass through a pre-hashed bytes32). */
|
|
8
|
+
export declare function resolveKind(kind: string, hashKind?: (s: string) => string): string;
|
|
9
|
+
export interface BadgeQuery {
|
|
10
|
+
/** JSON-RPC endpoint for the chain the registry lives on. */
|
|
11
|
+
rpcUrl: string;
|
|
12
|
+
/** Registry contract address. PIN THIS — see the note in hasActiveBadge. */
|
|
13
|
+
registry: string;
|
|
14
|
+
/** Address the record is about (facilitator settlement address, etc.). */
|
|
15
|
+
subject: string;
|
|
16
|
+
/** Kind name from KNOWN_BADGE_KINDS, or a pre-hashed bytes32. */
|
|
17
|
+
kind: string;
|
|
18
|
+
hashKind?: (s: string) => string;
|
|
19
|
+
fetchImpl?: typeof fetch;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `hasActiveBadge(subject, kind)` on a conforming registry.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ Spec rule (Security considerations, "Registry substitution"): a
|
|
26
|
+
* malicious manifest can point at a lookalike registry, so a caller MUST
|
|
27
|
+
* pin or allowlist the registries it accepts. This function deliberately
|
|
28
|
+
* takes the registry as an explicit argument rather than reading it from
|
|
29
|
+
* a manifest — so that pinning is a decision you make, not one you
|
|
30
|
+
* forget to make.
|
|
31
|
+
*
|
|
32
|
+
* Throws on RPC failure rather than returning false: "I could not check"
|
|
33
|
+
* and "I checked and it is not active" are different answers, and
|
|
34
|
+
* collapsing them is how a chain outage turns into a silent trust
|
|
35
|
+
* upgrade. Callers decide their own failure policy.
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasActiveBadge(q: BadgeQuery): Promise<boolean>;
|
|
38
|
+
export interface BadgeQuorumQuery extends Omit<BadgeQuery, 'rpcUrl'> {
|
|
39
|
+
/** Independent RPC endpoints. Two must AGREE for an answer. */
|
|
40
|
+
rpcUrls: string[];
|
|
41
|
+
/** How many agreeing answers are required. Default 2 (or all, if fewer). */
|
|
42
|
+
quorum?: number;
|
|
43
|
+
}
|
|
44
|
+
export type BadgeQuorumResult = {
|
|
45
|
+
status: 'agreed';
|
|
46
|
+
active: boolean;
|
|
47
|
+
agreed: number;
|
|
48
|
+
queried: number;
|
|
49
|
+
}
|
|
50
|
+
/** Endpoints returned CONFLICTING answers — someone is lying or broken. */
|
|
51
|
+
| {
|
|
52
|
+
status: 'disputed';
|
|
53
|
+
answers: boolean[];
|
|
54
|
+
errors: string[];
|
|
55
|
+
}
|
|
56
|
+
/** Too few endpoints answered to reach quorum. Not a negative answer. */
|
|
57
|
+
| {
|
|
58
|
+
status: 'unavailable';
|
|
59
|
+
agreed: number;
|
|
60
|
+
quorum: number;
|
|
61
|
+
errors: string[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* `hasActiveBadge` across N independent endpoints, requiring agreement.
|
|
65
|
+
*
|
|
66
|
+
* ── WHY THIS EXISTS ──
|
|
67
|
+
* Badge status GATES PAYMENTS, and a single `eth_call` is one endpoint's
|
|
68
|
+
* word. Failover fixes liveness, not honesty: with one source, a hostile
|
|
69
|
+
* or buggy RPC can report `active` for a revoked venue (or `inactive` to
|
|
70
|
+
* censor a competitor) and the caller cannot tell. Any read whose answer
|
|
71
|
+
* we act on without a second source needs a second source.
|
|
72
|
+
*
|
|
73
|
+
* Three outcomes, deliberately distinct — collapsing them is how a trust
|
|
74
|
+
* check quietly stops being one:
|
|
75
|
+
* agreed — quorum endpoints returned the same answer. Act on it.
|
|
76
|
+
* disputed — endpoints DISAGREED. This is not "unknown", it is
|
|
77
|
+
* evidence that one of them is wrong about a
|
|
78
|
+
* payment-gating fact. Callers should refuse, not retry.
|
|
79
|
+
* unavailable — too few answered. "I could not check" ≠ "not active".
|
|
80
|
+
*/
|
|
81
|
+
export declare function hasActiveBadgeQuorum(q: BadgeQuorumQuery): Promise<BadgeQuorumResult>;
|
|
82
|
+
export interface OperatorTrust {
|
|
83
|
+
/** The durable claim: a verification happened and has not been revoked. */
|
|
84
|
+
readonly verified: boolean;
|
|
85
|
+
/** The live claim, read from the operator's own manifest right now. */
|
|
86
|
+
readonly liveAttestation: 'tee' | 'none' | 'absent';
|
|
87
|
+
/**
|
|
88
|
+
* True only when the durable record AND the live state agree. This is
|
|
89
|
+
* the value to gate a state-dependent decision on.
|
|
90
|
+
*/
|
|
91
|
+
readonly attestedNow: boolean;
|
|
92
|
+
readonly notes: readonly string[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Evaluate an operator the way the spec requires for any decision that
|
|
96
|
+
* depends on RUNTIME state (e.g. "is settlement attested right now?").
|
|
97
|
+
*
|
|
98
|
+
* ── THE MISTAKE THIS FUNCTION EXISTS TO PREVENT ──
|
|
99
|
+
* A registry record is soulbound, therefore durable. TEE attestation is
|
|
100
|
+
* transient. Pointing the first at the second produces a claim that is
|
|
101
|
+
* true when written and silently false later, with nothing in the record
|
|
102
|
+
* to reveal the drift. We shipped exactly that: our own
|
|
103
|
+
* `x402-facilitator-attested` record read `active` on-chain while our own
|
|
104
|
+
* manifest honestly reported `attestation: none` because the enclave was
|
|
105
|
+
* down. The record was not lying about what it attested — a verification
|
|
106
|
+
* really did happen — it was being READ as a liveness signal it cannot
|
|
107
|
+
* carry.
|
|
108
|
+
*
|
|
109
|
+
* So the two answers are kept separate and only combined explicitly:
|
|
110
|
+
* verified — "was this operator verified?" (registry)
|
|
111
|
+
* liveAttestation — "is it in that mode right now?" (manifest)
|
|
112
|
+
* attestedNow — both, which is what you actually gate on.
|
|
113
|
+
*
|
|
114
|
+
* Fails CLOSED: any inability to establish the live state yields
|
|
115
|
+
* attestedNow === false.
|
|
116
|
+
*/
|
|
117
|
+
export declare function checkOperatorTrust(opts: {
|
|
118
|
+
manifest: {
|
|
119
|
+
attestation?: {
|
|
120
|
+
type?: string;
|
|
121
|
+
verifier?: string;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
rpcUrl: string;
|
|
125
|
+
registry: string;
|
|
126
|
+
subject: string;
|
|
127
|
+
kind: string;
|
|
128
|
+
hashKind?: (s: string) => string;
|
|
129
|
+
fetchImpl?: typeof fetch;
|
|
130
|
+
}): Promise<OperatorTrust>;
|
|
131
|
+
//# sourceMappingURL=badges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"badges.d.ts","sourceRoot":"","sources":["../src/badges.ts"],"names":[],"mappings":"AAyBA;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI7D,CAAC;AAKH,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,CAYlF;AAED,MAAM,WAAW,UAAU;IACvB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CA6BpE;AAID,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC;IAChE,+DAA+D;IAC/D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GACvB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACxE,2EAA2E;GACzE;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,OAAO,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE;AAC9D,yEAAyE;GACvE;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAElF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+B1F;AAID,MAAM,WAAW,aAAa;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,eAAe,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;IACpD;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC3C,QAAQ,EAAE;QAAE,WAAW,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC5B,GAAG,OAAO,CAAC,aAAa,CAAC,CAwCzB"}
|