@proof-holdings/delegation-verifier 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 proof.holdings
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,273 @@
1
+ # @proof-holdings/delegation-verifier
2
+
3
+ Verify a **Proof of Delegation**: a signed statement that the controller of a domain authorized
4
+ a specific agent artifact — an npm package, a container, an endpoint — for a set of capability
5
+ scopes.
6
+
7
+ You do not need an account with anyone to use this. It runs against public surfaces only, and
8
+ it has **no built-in trust list**: you name the issuers you accept.
9
+
10
+ ```bash
11
+ npm install @proof-holdings/delegation-verifier
12
+ ```
13
+
14
+ Zero runtime dependencies. Node ≥ 18 (or any runtime with WebCrypto, `fetch` and
15
+ `DecompressionStream` — Deno, Bun, Cloudflare Workers).
16
+
17
+ ## What a valid result means — and what it does not
18
+
19
+ A verified delegation says exactly one thing:
20
+
21
+ > The controller of `principal` authorized `delegate` for these scopes, and the issuer checked
22
+ > that control when it signed.
23
+
24
+ It is **not** a statement that the artifact is safe, audited, maintained, or endorsed, and it is
25
+ not a review of the business behind the domain. Do not render it as a generic "verified" badge.
26
+
27
+ ## Usage
28
+
29
+ ```ts
30
+ import { verifyPublishedDelegation } from '@proof-holdings/delegation-verifier';
31
+
32
+ const result = await verifyPublishedDelegation(serverCard, {
33
+ // No defaults: you decide whose signature means anything to you.
34
+ trustedIssuers: [
35
+ {
36
+ issuer: 'proof.holdings',
37
+ jwksUri: 'https://api.proof.holdings/.well-known/jwks.json',
38
+ statusEndpoint: 'https://api.proof.holdings/api/v1/proofs/validate',
39
+ },
40
+ ],
41
+ // The artifact YOU resolved — the package you are installing, the endpoint you are calling.
42
+ delegate: { type: 'purl', value: 'pkg:npm/postmark-mcp' },
43
+ expectedPrincipal: 'postmarkapp.com',
44
+ requiredScopes: ['send-email'],
45
+ });
46
+
47
+ if (result.valid) {
48
+ // `result.principalChecked` / `result.statusChecked` say how much was actually asked.
49
+ console.log(`${result.delegation.principal} authorized ${result.delegation.delegate}`);
50
+ } else {
51
+ console.warn(`not verified: ${result.reason} — ${result.message}`);
52
+ }
53
+ ```
54
+
55
+ `verifyDelegation(token, options)` is the same check when you already hold the token instead of
56
+ a card.
57
+
58
+ ### `delegate` is required, and this is the point
59
+
60
+ A published delegation token is a **bearer artifact**. Anyone can copy a genuine token out of
61
+ one card and paste it into another. The check that defeats that is comparing the token's
62
+ `delegate` claim against the artifact identity **you resolved independently** — the package
63
+ name you are about to `npm install`, the URL you are about to call. Never pass a value you read
64
+ out of the card being verified: a card describing itself is not evidence about itself.
65
+
66
+ That is why `delegate` is a required argument rather than an optional one, and why omitting it
67
+ is reported as `invalid_options` instead of quietly skipping the comparison.
68
+
69
+ ### `expectedPrincipal` closes the OTHER half
70
+
71
+ An issuer binds `delegate` to nothing. The domain in `principal` is whatever the requester
72
+ proved control of; the artifact is a value they chose. So anyone who can verify `evil.example`
73
+ can mint a genuine, signature-valid, non-revoked delegation naming **someone else's** package.
74
+
75
+ `delegate` stops a token being *copied*; only pinning `principal` stops one being *minted by
76
+ the wrong party*. Pass `expectedPrincipal` whenever you know whose artifact you think this is —
77
+ which is nearly always, since you know what you are installing.
78
+
79
+ A result without it is still returned, because a directory or a lookup UI legitimately asks
80
+ "who claims this artifact?" — but it carries `principalChecked: false`, and it must be
81
+ presented as *"X says it authorized this"*, never as *verified*.
82
+
83
+ ## The chain
84
+
85
+ 1. **Trust policy** — the token's `iss` must be one you named. No trust list, no verification.
86
+ 2. **Signature** — RS256/ES256 against the issuer's JWKS. The algorithm is chosen by the
87
+ verifier, never read from the token, so `alg: none` and HMAC forgeries die before any key is
88
+ fetched. Key sets are cached; an unknown `kid` triggers at most one refetch per 30s.
89
+ 3. **Claims** — `token_type`, schema version, `exp`/`iat` (5s tolerance by default), and the
90
+ required claim set.
91
+ 4. **Principal** — pinned to the domain you expect, when you supply one. See above: without it
92
+ the answer says who *claims* the artifact, not that the right party did.
93
+ 5. **Delegate** — canonicalized and compared byte-for-byte with the identity you resolved.
94
+ 6. **Status** — two channels, both authenticated:
95
+ - the IETF Token Status List the token points at (`status.status_list`), whose uri is
96
+ refused unless it belongs to your trusted issuer, and whose signature is checked against
97
+ the same key set;
98
+ - the issuer's status endpoint, which is the only thing that can report the **cascade**: a
99
+ delegation is worth no more than the domain control proof beneath it, and a revoked domain
100
+ proof does not flip the delegation's own status-list bit.
101
+
102
+ Status checking is **fail-closed**: if either channel cannot answer, the result is
103
+ `status_unavailable`, not "valid". Pass `statusCheck: 'skip'` to check signature and claims
104
+ only — the result then carries `statusChecked: false`, which means *revocation was not
105
+ checked*, never *not revoked*.
106
+
107
+ ### What happens when the domain proof underneath is renewed for a shorter period
108
+
109
+ A delegation's own `exp` can never exceed its control (domain) proof's remaining lifetime at
110
+ issuance — the issuer rejects a longer request rather than truncating it. proof.holdings applies
111
+ the same rule on the OTHER side, at every later renewal of the domain proof itself: the renewal
112
+ call still succeeds, but if the requested duration would shorten the domain proof's expiry below
113
+ an already-issued delegation's own `exp`, the requested value is not honoured — the domain proof
114
+ is kept at (at least) the longest-lived delegation's expiry instead, with no error returned. A
115
+ delegate's token therefore never silently outlives the domain proof it depends on — you do not
116
+ need to re-check more often just because the domain owner renewed their proof for a shorter term
117
+ than before.
118
+
119
+ ### Issuer status endpoint contract
120
+
121
+ `POST <statusEndpoint>` with `{"proof_token": "<jwt>"}` answering JSON with at least
122
+ `{"valid": boolean}` and optionally `{"reason": "revoked" | "suspended" | ...}`.
123
+
124
+ ## Failure reasons
125
+
126
+ Every failure carries an `outcome: 'invalid' | 'unconfirmed'` beside `reason` —
127
+ `classifyFailureReason(reason)` if you need it standalone. `unconfirmed` means the revocation
128
+ check itself could not run; treat it as "not checked", never as "checked and fine". Everything
129
+ else is `invalid`: a completed check with a negative verdict, including a call-shape error (those
130
+ are unreachable once your own call is well-formed, and are classified for completeness, not
131
+ because they occur in practice).
132
+
133
+ | `reason` | `outcome` | Meaning |
134
+ | --- | --- | --- |
135
+ | `invalid_options` | `invalid` | The call is wrong — usually a missing or uncanonicalizable `delegate`. |
136
+ | `no_trust_policy` | `invalid` | `trustedIssuers` was empty. There is no default. |
137
+ | `malformed_token` | `invalid` | Not a decodable JWT. |
138
+ | `untrusted_issuer` | `invalid` | Signed by someone you did not name. |
139
+ | `unsupported_algorithm` | `invalid` | The token asked for an algorithm this verifier does not accept. |
140
+ | `unknown_key` / `bad_signature` | `invalid` | No matching key, or the signature does not hold. |
141
+ | `jwks_unavailable` | `unconfirmed` | Could not read the key set — "I could not check", not "it is bad". |
142
+ | `not_a_delegation` / `unsupported_schema` | `invalid` | Wrong token type or a schema major this version does not understand. |
143
+ | `expired` / `not_yet_valid` / `malformed_claims` | `invalid` | Claim-level rejections. |
144
+ | `principal_mismatch` | `invalid` | Issued by a different domain than you expected. |
145
+ | `delegate_mismatch` | `invalid` | **Attests a different artifact than the one you resolved.** |
146
+ | `scope_not_granted` | `invalid` | Does not grant a scope you required. |
147
+ | `status_uri_untrusted` | `unconfirmed` | The status-list uri in the token is not the issuer's. Not fetched. |
148
+ | `status_unavailable` | `unconfirmed` | Revocation could not be checked. Fail closed. |
149
+ | `revoked` / `suspended` | `invalid` | Withdrawn, or reversibly paused. |
150
+ | `unknown_delegation` | `invalid` | The issuer has no record of it — distinct from "could not answer". |
151
+ | `no_publication` / `multiple_publications` | `invalid` | The card carries no delegation, or several different ones. |
152
+
153
+ ### Why the status endpoint is not always called, and what that leaves uncovered
154
+
155
+ A `valid`/`revoked`/`suspended` read from the token's status list ends the revocation check right
156
+ there — no request to the issuer's status endpoint. That is safe **for any issuer that adopts a
157
+ status list at all**: the IETF draft's premise is that the list is the issuer's authoritative
158
+ revocation channel, so an issuer publishing one is committing to encode everything relevant to
159
+ that token's validity in it. An issuer that keeps a side-channel revocation reason out of its own
160
+ published list is not honoring the channel it chose to publish — a fact about that issuer, not
161
+ something a generic verifier can special-case.
162
+
163
+ The endpoint still fires for the cases the bit structurally cannot answer: a token with no
164
+ status-list claim at all, `unknown_delegation` ("no record of this handle" is distinguishable
165
+ from "a record that is dead" only by a live call), and — for issuers whose validity cascades
166
+ through something else, like a domain control proof underneath a delegation — an EXPIRY driven by
167
+ that something else, which a status-list bit never encodes (only revoked/suspended are, by the
168
+ same draft's own vocabulary). For proof.holdings specifically, that cascade-expiry gap is bounded,
169
+ not eliminated: a delegation can no longer be minted longer than its underlying control proof, and
170
+ a control cannot be re-issued shorter than an outstanding delegation without capping the
171
+ reissue to cover it — but a control shortened before that guarantee existed, or a reissue that hit
172
+ its own rare fail-open path, can still leave a delegation whose list bit and own `exp` claim both
173
+ read fine while its issuer would answer `expired`. This is a known, accepted trade-off — not
174
+ silently assumed away.
175
+
176
+ ### One thing a failure reason is not
177
+
178
+ Claims are checked before the signature (cheap rejections first, and no `valid: true` is
179
+ reachable without a verified signature). A consequence: on a *forged* token the `reason` is
180
+ attacker-chosen — anyone can produce `delegate_mismatch` or `principal_mismatch` with an
181
+ unsigned JWT. Read a failure as "this did not verify", never as attested evidence about who
182
+ tried.
183
+
184
+ ### A reason reports the FIRST failure, not the only one
185
+
186
+ The checks are ordered, and the first one to fail returns. `expired` in particular is decided
187
+ *before* `expectedPrincipal` and `delegate` are compared, so an expired token yields `expired`
188
+ whatever the pins are — an expired delegation genuinely issued for your artifact and an expired
189
+ token copied into a stranger's card are indistinguishable in the reason alone. The same holds
190
+ upward: `untrusted_issuer` supersedes everything after it.
191
+
192
+ So a failure reason tells you where the walk stopped, not what the remaining checks would have
193
+ said: they never ran. Do not build policy on the assumption that a reason implies the later
194
+ checks were evaluated — only `valid: true` means every check passed.
195
+
196
+ ## Where delegations are published
197
+
198
+ Three sanctioned carriers live INSIDE a card and are all read by `verifyPublishedDelegation`:
199
+
200
+ - MCP server card — `_meta["holdings.proof/delegation"]`
201
+ - MCP official registry — the same key nested under
202
+ `_meta["io.modelcontextprotocol.registry/publisher-provided"]`
203
+ - A2A agent card — an entry in `capabilities.extensions[]` with
204
+ `uri: "https://proof.holdings/delegation"`
205
+
206
+ Each carries `{ "token": "<jwt>", "docs"?: "<url>" }`.
207
+
208
+ ### The fourth carrier: a DNS pointer
209
+
210
+ `_mcp.<domain>` may hold a TXT record naming the card that carries the delegation:
211
+
212
+ ```
213
+ _mcp.example.com. IN TXT "v=proofdlg1; card=https://example.com/.well-known/mcp/server.json"
214
+ ```
215
+
216
+ It holds **no token** — DNS is a channel with no confidentiality and cached copies at resolvers
217
+ nobody controls. It exists so a reader can start from a DOMAIN rather than from an artifact, which
218
+ is the only route open to an owner who cannot publish a file on their own host: the card it names
219
+ may live anywhere.
220
+
221
+ Resolving it means a DNS query and an HTTP fetch, and this package has no dependencies, so it ships
222
+ only the pure half. Do the lookup with whatever resolver you already have and hand the TXT values
223
+ in:
224
+
225
+ ```ts
226
+ import { parseDelegationPointer, pointerRecordName, verifyPublishedDelegation } from '@proof-holdings/delegation-verifier';
227
+ import { resolveTxt } from 'node:dns/promises';
228
+
229
+ // `resolveTxt` THROWS on NXDOMAIN, and "the domain publishes nothing" is a different fact from
230
+ // "the lookup failed" — collapsing them reports a network blip as "not delegated".
231
+ let records: string[][] = [];
232
+ try {
233
+ records = await resolveTxt(pointerRecordName('example.com')); // '_mcp.example.com'
234
+ } catch (error) {
235
+ // ENOTFOUND is "no such name", ENODATA is "the name exists with no TXT" — both mean the domain
236
+ // publishes no pointer. Anything else is a lookup FAILURE, which is a different fact.
237
+ const code = (error as NodeJS.ErrnoException).code;
238
+ if (code !== 'ENOTFOUND' && code !== 'ENODATA') throw error;
239
+ }
240
+
241
+ const pointer = parseDelegationPointer(records.map((chunks) => chunks.join('')));
242
+ if (!pointer.ok) throw new Error(pointer.reason);
243
+
244
+ // `redirect: 'error'` matters: the grammar guarantees https at the START of the fetch, not at the
245
+ // end of a redirect chain, and the card is the thing carrying the delegation.
246
+ const response = await fetch(pointer.card, { redirect: 'error' });
247
+ if (!response.ok) throw new Error(`card fetch failed: ${response.status}`);
248
+ const card = await response.json();
249
+ const result = await verifyPublishedDelegation(card, {
250
+ trustedIssuers,
251
+ delegate: { type: 'purl', value: 'pkg:npm/example-mcp' }, // resolved by YOU, not read from the card
252
+ expectedPrincipal: 'example.com', // the domain whose pointer you followed
253
+ });
254
+ ```
255
+
256
+ `parseDelegationPointer` answers `{ ok: true, card }` or `{ ok: false, reason }`, where `reason` is
257
+ one of `no_pointer`, `version_not_first`, `missing_card`, `duplicate_field`, `invalid_card_url`,
258
+ `ambiguous_pointer`, `token_in_dns`. Two behaviours worth knowing:
259
+
260
+ - A record whose first field is not `v=proofdlg1` is **not ours** and is skipped silently. The MCP
261
+ discovery draft (`draft-morrison-mcp-dns-discovery`) publishes `v=mcp1` at the same name, and the
262
+ two coexist by ignoring each other.
263
+ - A record of ours that is malformed is a refusal **even when a well-formed one sits beside it**, and
264
+ two well-formed records naming different cards are `ambiguous_pointer`. A zone in a contradictory
265
+ state is not a zone to guess about — the same reason `verifyPublishedDelegation` refuses a card
266
+ that publishes two different tokens.
267
+
268
+ A pointer is discovery, never evidence. Following one tells you where to look; whether the delegation
269
+ is real is still decided by `verifyPublishedDelegation` against the issuer's keys and status.
270
+
271
+ ## License
272
+
273
+ MIT
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Where a delegation may be published, and how to read it back.
3
+ *
4
+ * ⚠️ SECOND LOCKSTEP COPY of two issuer-side pieces: the extraction half of
5
+ * `scripts/delegation-publication-shared.ts`, and the pointer parser in
6
+ * `src/services/delegationPointer/pointer.ts`. Both are pinned by the drift suite
7
+ * `src/__tests__/drift/delegation-verifier.test.ts` in that repository — a carrier the
8
+ * publisher writes to and the verifier does not read makes a real delegation invisible.
9
+ *
10
+ * FOUR sanctioned carriers, and the fourth is shaped unlike the others. Three are LOCATIONS
11
+ * INSIDE A CARD, each holding the publication itself, because three registries reserve
12
+ * different keys:
13
+ * - MCP server card: `_meta["holdings.proof/delegation"]`
14
+ * - MCP official registry: nested under `_meta["io.modelcontextprotocol.registry/publisher-provided"]`
15
+ * - A2A agent card: `capabilities.extensions[]` with `uri: "https://proof.holdings/delegation"`
16
+ * (A2A cards have no `_meta`)
17
+ * The fourth is a DNS record — `_mcp.<domain>` TXT — which holds NO publication at all: it holds
18
+ * the ADDRESS of a card that does. The first three are read from an artifact one already has; the
19
+ * pointer is read from a domain, and leads to the card. Format:
20
+ * `docs/delegation-dns-pointer.md` in the issuer repository.
21
+ *
22
+ * Deliberately free of any crypto import: a consumer that only wants to know whether a card
23
+ * claims a delegation at all pays nothing for the verification machinery. For the same reason
24
+ * pointer RESOLUTION — the DNS query and the card fetch — is not here: it is Node-only I/O and
25
+ * lives in the issuer repository, while the parsing below stays pure.
26
+ */
27
+ export declare const DELEGATION_META_KEY = "holdings.proof/delegation";
28
+ export declare const DELEGATION_EXTENSION_URI = "https://proof.holdings/delegation";
29
+ export declare const REGISTRY_PUBLISHER_PROVIDED_KEY = "io.modelcontextprotocol.registry/publisher-provided";
30
+ export interface FoundPublication {
31
+ location: string;
32
+ value: unknown;
33
+ }
34
+ /** Every delegation publication a card carries, across all sanctioned locations. */
35
+ export declare function extractDelegationPublications(card: unknown): FoundPublication[];
36
+ export declare const POINTER_VERSION = "proofdlg1";
37
+ export declare const POINTER_RECORD_PREFIX = "_mcp";
38
+ export type PointerRejection = 'no_pointer' | 'version_not_first' | 'missing_card' | 'duplicate_field' | 'invalid_card_url' | 'ambiguous_pointer' | 'token_in_dns';
39
+ export type PointerParseResult = {
40
+ ok: true;
41
+ card: string;
42
+ } | {
43
+ ok: false;
44
+ reason: PointerRejection;
45
+ };
46
+ /** The DNS name a domain publishes its pointer at. */
47
+ export declare function pointerRecordName(domain: string): string;
48
+ /**
49
+ * How much of a segment is decoded once the probe matches, and therefore the LONGEST header this
50
+ * scan can recognise. It is a real, documented boundary rather than an implementation detail: a
51
+ * token whose header exceeds it is not detected. 4096 characters is ~3 KB of decoded JSON, which
52
+ * covers an ordinary header (~40-80 characters) and one carrying an `x5c` certificate chain, while
53
+ * keeping the walk linear — without it, decoding "the rest of the segment" at every offset is
54
+ * O(L²), and a review measured 4193 ms of blocked processing on one 64 KB value an attacker
55
+ * chooses. See `docs/delegation-dns-pointer.md` for the boundary as published.
56
+ */
57
+ export declare const MAX_HEADER_CHARS = 4096;
58
+ /** Does this text carry a JWT anywhere inside it? */
59
+ export declare function carriesJwt(text: string): boolean;
60
+ /**
61
+ * Read the pointer out of the TXT values published at `_mcp.<domain>`.
62
+ *
63
+ * Several records may share that name — the MCP discovery draft permits it, so a zone may hold a
64
+ * `v=mcp1` record beside ours. Records that are not ours are skipped silently. Any record of ours
65
+ * that is malformed is a refusal even when another of ours is fine: a zone holding a broken proof
66
+ * record is in an unknown state, and reading the other one is guessing which the owner meant. Two
67
+ * well-formed records of ours naming DIFFERENT cards is `ambiguous_pointer`, for the same reason
68
+ * `verifyPublishedDelegation` refuses a card publishing two different tokens.
69
+ */
70
+ export declare function parseDelegationPointer(txtValues: string[]): PointerParseResult;
71
+ /** The distinct delegation tokens a card publishes. */
72
+ export declare function collectPublishedTokens(card: unknown): string[];
73
+ //# sourceMappingURL=carriers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carriers.d.ts","sourceRoot":"","sources":["../src/carriers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAC/D,eAAO,MAAM,wBAAwB,sCAAsC,CAAC;AAC5E,eAAO,MAAM,+BAA+B,wDAAwD,CAAC;AAErG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAMD,oFAAoF;AACpF,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,OAAO,GAAG,gBAAgB,EAAE,CA4B/E;AAQD,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,qBAAqB,SAAS,CAAC;AAE5C,MAAM,MAAM,gBAAgB,GACxB,YAAY,GACZ,mBAAmB,GACnB,cAAc,GACd,iBAAiB,GACjB,kBAAkB,GAClB,mBAAmB,GACnB,cAAc,CAAC;AAEnB,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEtG,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD;AAwED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAmChD;AAoCD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,kBAAkB,CAyB9E;AAED,uDAAuD;AACvD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,CAQ9D"}
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Where a delegation may be published, and how to read it back.
3
+ *
4
+ * ⚠️ SECOND LOCKSTEP COPY of two issuer-side pieces: the extraction half of
5
+ * `scripts/delegation-publication-shared.ts`, and the pointer parser in
6
+ * `src/services/delegationPointer/pointer.ts`. Both are pinned by the drift suite
7
+ * `src/__tests__/drift/delegation-verifier.test.ts` in that repository — a carrier the
8
+ * publisher writes to and the verifier does not read makes a real delegation invisible.
9
+ *
10
+ * FOUR sanctioned carriers, and the fourth is shaped unlike the others. Three are LOCATIONS
11
+ * INSIDE A CARD, each holding the publication itself, because three registries reserve
12
+ * different keys:
13
+ * - MCP server card: `_meta["holdings.proof/delegation"]`
14
+ * - MCP official registry: nested under `_meta["io.modelcontextprotocol.registry/publisher-provided"]`
15
+ * - A2A agent card: `capabilities.extensions[]` with `uri: "https://proof.holdings/delegation"`
16
+ * (A2A cards have no `_meta`)
17
+ * The fourth is a DNS record — `_mcp.<domain>` TXT — which holds NO publication at all: it holds
18
+ * the ADDRESS of a card that does. The first three are read from an artifact one already has; the
19
+ * pointer is read from a domain, and leads to the card. Format:
20
+ * `docs/delegation-dns-pointer.md` in the issuer repository.
21
+ *
22
+ * Deliberately free of any crypto import: a consumer that only wants to know whether a card
23
+ * claims a delegation at all pays nothing for the verification machinery. For the same reason
24
+ * pointer RESOLUTION — the DNS query and the card fetch — is not here: it is Node-only I/O and
25
+ * lives in the issuer repository, while the parsing below stays pure.
26
+ */
27
+ export const DELEGATION_META_KEY = 'holdings.proof/delegation';
28
+ export const DELEGATION_EXTENSION_URI = 'https://proof.holdings/delegation';
29
+ export const REGISTRY_PUBLISHER_PROVIDED_KEY = 'io.modelcontextprotocol.registry/publisher-provided';
30
+ function isRecord(value) {
31
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
32
+ }
33
+ /** Every delegation publication a card carries, across all sanctioned locations. */
34
+ export function extractDelegationPublications(card) {
35
+ if (!isRecord(card))
36
+ return [];
37
+ const found = [];
38
+ const meta = card._meta;
39
+ if (isRecord(meta)) {
40
+ if (meta[DELEGATION_META_KEY] !== undefined) {
41
+ found.push({ location: `_meta["${DELEGATION_META_KEY}"]`, value: meta[DELEGATION_META_KEY] });
42
+ }
43
+ const publisherProvided = meta[REGISTRY_PUBLISHER_PROVIDED_KEY];
44
+ if (isRecord(publisherProvided) && publisherProvided[DELEGATION_META_KEY] !== undefined) {
45
+ found.push({
46
+ location: `_meta["${REGISTRY_PUBLISHER_PROVIDED_KEY}"]["${DELEGATION_META_KEY}"]`,
47
+ value: publisherProvided[DELEGATION_META_KEY],
48
+ });
49
+ }
50
+ }
51
+ const capabilities = card.capabilities;
52
+ if (isRecord(capabilities) && Array.isArray(capabilities.extensions)) {
53
+ capabilities.extensions.forEach((extension, index) => {
54
+ if (isRecord(extension) && extension.uri === DELEGATION_EXTENSION_URI) {
55
+ found.push({ location: `capabilities.extensions[${index}]`, value: extension.params });
56
+ }
57
+ });
58
+ }
59
+ return found;
60
+ }
61
+ // ---------------------------------------------------------------------------------------------
62
+ // The DNS pointer (fourth carrier). LOCKSTEP COPY of `src/services/delegationPointer/pointer.ts`
63
+ // in the issuer repository; the build half is deliberately absent, since a verifier never
64
+ // publishes. Format and rejection vocabulary: `docs/delegation-dns-pointer.md`.
65
+ // ---------------------------------------------------------------------------------------------
66
+ export const POINTER_VERSION = 'proofdlg1';
67
+ export const POINTER_RECORD_PREFIX = '_mcp';
68
+ /** The DNS name a domain publishes its pointer at. */
69
+ export function pointerRecordName(domain) {
70
+ return `${POINTER_RECORD_PREFIX}.${domain}`;
71
+ }
72
+ /**
73
+ * A card URL is an absolute https URL with no userinfo, no query and no fragment — the same
74
+ * restrictions `canonicalizeDelegate` places on a url-typed delegate.
75
+ */
76
+ function isValidCardUrl(value) {
77
+ let url;
78
+ try {
79
+ url = new URL(value);
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ return (url.protocol === 'https:' &&
85
+ url.username === '' &&
86
+ url.password === '' &&
87
+ url.search === '' &&
88
+ url.hash === '');
89
+ }
90
+ /**
91
+ * Three base64url runs separated by dots is only a CANDIDATE — `discovery.mcp-servers.engineering`
92
+ * matches that shape too. What settles it is decoding: a JWT's first segment is a JSON object
93
+ * carrying `alg`, and a hostname label is not.
94
+ *
95
+ * Every CONSECUTIVE triple is examined, deliberately. A scan over non-overlapping regex matches
96
+ * misses a token whenever anything dotted precedes it, because the earlier match consumes the
97
+ * token's header as its own third run — `abcd.efgh.<token>` then reads as clean. A code review
98
+ * found that live, and the fixtures meant to prove the rule all happened to place the token where
99
+ * nothing preceded it.
100
+ */
101
+ const RUN_SEPARATOR = /[^A-Za-z0-9_.-]+/;
102
+ const MIN_HEADER_RUN = 4;
103
+ const MIN_PAYLOAD_RUN = 4;
104
+ const MIN_SIGNATURE_RUN = 2;
105
+ function decodeBase64Url(segment) {
106
+ const padded = segment.replace(/-/g, '+').replace(/_/g, '/');
107
+ const remainder = padded.length % 4;
108
+ if (remainder === 1)
109
+ return null;
110
+ try {
111
+ return atob(remainder === 0 ? padded : padded + '='.repeat(4 - remainder));
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * Two facts about text that base64url-decodes to a JSON object whose FIRST BYTE is `{`, both
119
+ * checked numerically rather than assumed (an earlier attempt anchored on the prefix `ey`, which
120
+ * is wrong: `{\n` encodes to `ewo`):
121
+ *
122
+ * - its first character is always `e` — the high six bits of `0x7B` are `011110`, index 30;
123
+ * - its first FOUR characters decode to three bytes whose first byte is `{`.
124
+ *
125
+ * The `{`-first qualifier is the honest statement of the limit, not a technicality: a header that
126
+ * begins with LEGAL JSON whitespace (` {"alg":…}`) is not seen. That is unreachable for a genuine
127
+ * token — the header is signing input, so it cannot be re-spaced without breaking the signature —
128
+ * and it fails CLOSED for a reader implementing this spec, but the rule has to say what it checks.
129
+ *
130
+ * The four-character probe is what keeps the offset walk linear with a small constant. Anchoring
131
+ * on `e` alone is not enough: a value made of 65 000 `e`s passes that filter at every offset, and a
132
+ * review's cost case measured 242 ms against this version's 6.6 ms on exactly that shape — a 37x
133
+ * gap, not the ~3x an average across mixed shapes suggested. The probe costs
134
+ * one 4-character decode per offset and rejects it immediately.
135
+ */
136
+ const HEADER_FIRST_CHAR = 'e';
137
+ const HEADER_PROBE_CHARS = 4;
138
+ const JSON_OBJECT_OPEN = '{';
139
+ /**
140
+ * How much of a segment is decoded once the probe matches, and therefore the LONGEST header this
141
+ * scan can recognise. It is a real, documented boundary rather than an implementation detail: a
142
+ * token whose header exceeds it is not detected. 4096 characters is ~3 KB of decoded JSON, which
143
+ * covers an ordinary header (~40-80 characters) and one carrying an `x5c` certificate chain, while
144
+ * keeping the walk linear — without it, decoding "the rest of the segment" at every offset is
145
+ * O(L²), and a review measured 4193 ms of blocked processing on one 64 KB value an attacker
146
+ * chooses. See `docs/delegation-dns-pointer.md` for the boundary as published.
147
+ */
148
+ export const MAX_HEADER_CHARS = 4096;
149
+ /** Does this text carry a JWT anywhere inside it? */
150
+ export function carriesJwt(text) {
151
+ for (const run of text.split(RUN_SEPARATOR)) {
152
+ const segments = run.split('.');
153
+ for (let index = 0; index + 2 < segments.length; index++) {
154
+ if (segments[index + 1].length < MIN_PAYLOAD_RUN ||
155
+ segments[index + 2].length < MIN_SIGNATURE_RUN) {
156
+ continue;
157
+ }
158
+ // The header may START anywhere inside its segment, not only at a run boundary: `-`, `_`
159
+ // and alphanumerics are all base64url-legal, so `delegation-<jwt>` puts the prefix and the
160
+ // header in ONE segment and decoding the whole thing fails. A review found that after the
161
+ // first fix, on fixtures that all happened to place a `/` or `=` immediately before the
162
+ // token.
163
+ const header = segments[index];
164
+ for (let start = 0; header.length - start >= MIN_HEADER_RUN; start++) {
165
+ if (header[start] !== HEADER_FIRST_CHAR)
166
+ continue;
167
+ const probe = decodeBase64Url(header.slice(start, start + HEADER_PROBE_CHARS));
168
+ if (probe === null || probe[0] !== JSON_OBJECT_OPEN)
169
+ continue;
170
+ const decoded = decodeBase64Url(header.slice(start, start + MAX_HEADER_CHARS));
171
+ if (decoded === null)
172
+ continue;
173
+ try {
174
+ const parsed = JSON.parse(decoded);
175
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) && 'alg' in parsed) {
176
+ return true;
177
+ }
178
+ }
179
+ catch {
180
+ // Not a JWT header; keep looking.
181
+ }
182
+ }
183
+ }
184
+ }
185
+ return false;
186
+ }
187
+ function parseOneRecord(raw) {
188
+ const fields = raw
189
+ .split(';')
190
+ .map((field) => field.trim())
191
+ .filter((field) => field.length > 0);
192
+ const versionIndex = fields.indexOf(`v=${POINTER_VERSION}`);
193
+ if (versionIndex === -1)
194
+ return { kind: 'not_ours' };
195
+ if (versionIndex !== 0)
196
+ return { kind: 'bad', reason: 'version_not_first' };
197
+ const seen = new Set(['v']);
198
+ let card;
199
+ for (const field of fields.slice(1)) {
200
+ const separator = field.indexOf('=');
201
+ if (separator <= 0)
202
+ continue;
203
+ const key = field.slice(0, separator);
204
+ if (!/^[a-z0-9-]+$/.test(key))
205
+ continue;
206
+ if (key !== 'card' && key !== 'v')
207
+ continue;
208
+ if (seen.has(key))
209
+ return { kind: 'bad', reason: 'duplicate_field' };
210
+ seen.add(key);
211
+ card = field.slice(separator + 1);
212
+ }
213
+ if (card === undefined)
214
+ return { kind: 'bad', reason: 'missing_card' };
215
+ if (!isValidCardUrl(card))
216
+ return { kind: 'bad', reason: 'invalid_card_url' };
217
+ return { kind: 'ok', card };
218
+ }
219
+ /**
220
+ * Read the pointer out of the TXT values published at `_mcp.<domain>`.
221
+ *
222
+ * Several records may share that name — the MCP discovery draft permits it, so a zone may hold a
223
+ * `v=mcp1` record beside ours. Records that are not ours are skipped silently. Any record of ours
224
+ * that is malformed is a refusal even when another of ours is fine: a zone holding a broken proof
225
+ * record is in an unknown state, and reading the other one is guessing which the owner meant. Two
226
+ * well-formed records of ours naming DIFFERENT cards is `ambiguous_pointer`, for the same reason
227
+ * `verifyPublishedDelegation` refuses a card publishing two different tokens.
228
+ */
229
+ export function parseDelegationPointer(txtValues) {
230
+ // Checked across the whole set BEFORE anything is believed about any record: a token in this
231
+ // zone's `_mcp` name is a bearer artifact loose in a channel with cached copies nobody controls,
232
+ // whoever put it there.
233
+ if (txtValues.some((value) => carriesJwt(value)))
234
+ return { ok: false, reason: 'token_in_dns' };
235
+ const ours = [];
236
+ for (const value of txtValues) {
237
+ const parsed = parseOneRecord(value);
238
+ if (parsed.kind !== 'not_ours')
239
+ ours.push(parsed);
240
+ }
241
+ if (ours.length === 0)
242
+ return { ok: false, reason: 'no_pointer' };
243
+ // With two malformed records of ours, the FIRST in the array decides which reason is reported,
244
+ // and resolvers rotate RRsets — so two runs against one zone can name different reasons. Both
245
+ // copies agree for any given input (which is what the lockstep suite pins) and both answers are
246
+ // refusals; only the diagnostic label moves.
247
+ const broken = ours.find((entry) => entry.kind === 'bad');
248
+ if (broken)
249
+ return { ok: false, reason: broken.reason };
250
+ const cards = new Set(ours.map((entry) => entry.card));
251
+ if (cards.size > 1)
252
+ return { ok: false, reason: 'ambiguous_pointer' };
253
+ return { ok: true, card: [...cards][0] };
254
+ }
255
+ /** The distinct delegation tokens a card publishes. */
256
+ export function collectPublishedTokens(card) {
257
+ const tokens = new Set();
258
+ for (const publication of extractDelegationPublications(card)) {
259
+ if (isRecord(publication.value) && typeof publication.value.token === 'string' && publication.value.token) {
260
+ tokens.add(publication.value.token);
261
+ }
262
+ }
263
+ return [...tokens];
264
+ }
265
+ //# sourceMappingURL=carriers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carriers.js","sourceRoot":"","sources":["../src/carriers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AAC/D,MAAM,CAAC,MAAM,wBAAwB,GAAG,mCAAmC,CAAC;AAC5E,MAAM,CAAC,MAAM,+BAA+B,GAAG,qDAAqD,CAAC;AAOrG,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,6BAA6B,CAAC,IAAa;IACzD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/B,MAAM,KAAK,GAAuB,EAAE,CAAC;IAErC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;IACxB,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,mBAAmB,CAAC,KAAK,SAAS,EAAE,CAAC;YAC5C,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,UAAU,mBAAmB,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;QAChG,CAAC;QACD,MAAM,iBAAiB,GAAG,IAAI,CAAC,+BAA+B,CAAC,CAAC;QAChE,IAAI,QAAQ,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,CAAC,mBAAmB,CAAC,KAAK,SAAS,EAAE,CAAC;YACxF,KAAK,CAAC,IAAI,CAAC;gBACT,QAAQ,EAAE,UAAU,+BAA+B,OAAO,mBAAmB,IAAI;gBACjF,KAAK,EAAE,iBAAiB,CAAC,mBAAmB,CAAC;aAC9C,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;IACvC,IAAI,QAAQ,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;QACrE,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE;YACnD,IAAI,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,GAAG,KAAK,wBAAwB,EAAE,CAAC;gBACtE,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,2BAA2B,KAAK,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;YACzF,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gGAAgG;AAChG,iGAAiG;AACjG,0FAA0F;AAC1F,gFAAgF;AAChF,gGAAgG;AAEhG,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAC3C,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAa5C,sDAAsD;AACtD,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,OAAO,GAAG,qBAAqB,IAAI,MAAM,EAAE,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc,CAAC,KAAa;IACnC,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,CACL,GAAG,CAAC,QAAQ,KAAK,QAAQ;QACzB,GAAG,CAAC,QAAQ,KAAK,EAAE;QACnB,GAAG,CAAC,QAAQ,KAAK,EAAE;QACnB,GAAG,CAAC,MAAM,KAAK,EAAE;QACjB,GAAG,CAAC,IAAI,KAAK,EAAE,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,aAAa,GAAG,kBAAkB,CAAC;AACzC,MAAM,cAAc,GAAG,CAAC,CAAC;AACzB,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAE5B,SAAS,eAAe,CAAC,OAAe;IACtC,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IACpC,IAAI,SAAS,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAC9B,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAC7B,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,qDAAqD;AACrD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAChC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YACzD,IACE,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,eAAe;gBAC5C,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,iBAAiB,EAC9C,CAAC;gBACD,SAAS;YACX,CAAC;YACD,yFAAyF;YACzF,2FAA2F;YAC3F,0FAA0F;YAC1F,wFAAwF;YACxF,SAAS;YACT,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC/B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,KAAK,IAAI,cAAc,EAAE,KAAK,EAAE,EAAE,CAAC;gBACrE,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,iBAAiB;oBAAE,SAAS;gBAClD,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,kBAAkB,CAAC,CAAC,CAAC;gBAC/E,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,gBAAgB;oBAAE,SAAS;gBAE9D,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAC;gBAC/E,IAAI,OAAO,KAAK,IAAI;oBAAE,SAAS;gBAC/B,IAAI,CAAC;oBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;oBAC5C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;wBAC/F,OAAO,IAAI,CAAC;oBACd,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,kCAAkC;gBACpC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAOD,SAAS,cAAc,CAAC,GAAW;IACjC,MAAM,MAAM,GAAG,GAAG;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAEvC,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,eAAe,EAAE,CAAC,CAAC;IAC5D,IAAI,YAAY,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IACrD,IAAI,YAAY,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAE5E,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,IAAI,IAAwB,CAAC;IAE7B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,SAAS,IAAI,CAAC;YAAE,SAAS;QAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QACtC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,SAAS;QACxC,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,GAAG;YAAE,SAAS;QAC5C,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;QACrE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IACpC,CAAC;IAED,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACvE,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC9E,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAmB;IACxD,6FAA6F;IAC7F,iGAAiG;IACjG,wBAAwB;IACxB,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAE/F,MAAM,IAAI,GAAoB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAElE,+FAA+F;IAC/F,8FAA8F;IAC9F,gGAAgG;IAChG,6CAA6C;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAsD,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC;IAC9G,IAAI,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IAExD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAE,KAAsC,CAAC,IAAI,CAAC,CAAC,CAAC;IACzF,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAEtE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3C,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,sBAAsB,CAAC,IAAa;IAClD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,KAAK,MAAM,WAAW,IAAI,6BAA6B,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9D,IAAI,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,OAAO,WAAW,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YAC1G,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC;AACrB,CAAC"}