@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 +21 -0
- package/README.md +273 -0
- package/dist/carriers.d.ts +73 -0
- package/dist/carriers.d.ts.map +1 -0
- package/dist/carriers.js +265 -0
- package/dist/carriers.js.map +1 -0
- package/dist/delegate.d.ts +34 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +156 -0
- package/dist/delegate.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/jwks.d.ts +47 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +185 -0
- package/dist/jwks.js.map +1 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +63 -0
- package/dist/jwt.js.map +1 -0
- package/dist/publication.d.ts +19 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +39 -0
- package/dist/publication.js.map +1 -0
- package/dist/status.d.ts +86 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +321 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +35 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +11 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +240 -0
- package/dist/verify.js.map +1 -0
- package/package.json +59 -0
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"}
|
package/dist/carriers.js
ADDED
|
@@ -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"}
|