@proof-holdings/delegation-self-refusal 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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/dist/cache.d.ts +10 -0
  4. package/dist/cache.d.ts.map +1 -0
  5. package/dist/cache.js +58 -0
  6. package/dist/cache.js.map +1 -0
  7. package/dist/guard.d.ts +19 -0
  8. package/dist/guard.d.ts.map +1 -0
  9. package/dist/guard.js +57 -0
  10. package/dist/guard.js.map +1 -0
  11. package/dist/index.d.ts +34 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +30 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/install.d.ts +8 -0
  16. package/dist/install.d.ts.map +1 -0
  17. package/dist/install.js +123 -0
  18. package/dist/install.js.map +1 -0
  19. package/dist/poll.d.ts +10 -0
  20. package/dist/poll.d.ts.map +1 -0
  21. package/dist/poll.js +60 -0
  22. package/dist/poll.js.map +1 -0
  23. package/dist/refusal.d.ts +92 -0
  24. package/dist/refusal.d.ts.map +1 -0
  25. package/dist/refusal.js +188 -0
  26. package/dist/refusal.js.map +1 -0
  27. package/dist/registrar.d.ts +77 -0
  28. package/dist/registrar.d.ts.map +1 -0
  29. package/dist/registrar.js +106 -0
  30. package/dist/registrar.js.map +1 -0
  31. package/dist/result.d.ts +16 -0
  32. package/dist/result.d.ts.map +1 -0
  33. package/dist/result.js +12 -0
  34. package/dist/result.js.map +1 -0
  35. package/dist/schedule.d.ts +14 -0
  36. package/dist/schedule.d.ts.map +1 -0
  37. package/dist/schedule.js +20 -0
  38. package/dist/schedule.js.map +1 -0
  39. package/dist/showcase/breaker.d.ts +91 -0
  40. package/dist/showcase/breaker.d.ts.map +1 -0
  41. package/dist/showcase/breaker.js +132 -0
  42. package/dist/showcase/breaker.js.map +1 -0
  43. package/dist/showcase/connect.d.ts +39 -0
  44. package/dist/showcase/connect.d.ts.map +1 -0
  45. package/dist/showcase/connect.js +98 -0
  46. package/dist/showcase/connect.js.map +1 -0
  47. package/dist/showcase/descriptions.d.ts +46 -0
  48. package/dist/showcase/descriptions.d.ts.map +1 -0
  49. package/dist/showcase/descriptions.js +86 -0
  50. package/dist/showcase/descriptions.js.map +1 -0
  51. package/dist/showcase/instructions.d.ts +13 -0
  52. package/dist/showcase/instructions.d.ts.map +1 -0
  53. package/dist/showcase/instructions.js +20 -0
  54. package/dist/showcase/instructions.js.map +1 -0
  55. package/dist/showcase/marked-fetch.d.ts +37 -0
  56. package/dist/showcase/marked-fetch.d.ts.map +1 -0
  57. package/dist/showcase/marked-fetch.js +48 -0
  58. package/dist/showcase/marked-fetch.js.map +1 -0
  59. package/dist/showcase/tools.d.ts +49 -0
  60. package/dist/showcase/tools.d.ts.map +1 -0
  61. package/dist/showcase/tools.js +198 -0
  62. package/dist/showcase/tools.js.map +1 -0
  63. package/dist/types.d.ts +59 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +2 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist/verdict.d.ts +47 -0
  68. package/dist/verdict.d.ts.map +1 -0
  69. package/dist/verdict.js +89 -0
  70. package/dist/verdict.js.map +1 -0
  71. package/package.json +69 -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,264 @@
1
+ # @proof-holdings/delegation-self-refusal
2
+
3
+ A delegated MCP server enforces its own authorization. This package holds your Proof of
4
+ Delegation token, polls the issuer on a jittered interval, caches the answer on disk, and gates
5
+ your own tool dispatch — so a consumer who never checks anything still cannot get a tool call out
6
+ of a server whose authorization has been revoked, suspended, or has expired.
7
+
8
+ ```bash
9
+ npm install @proof-holdings/delegation-self-refusal
10
+ ```
11
+
12
+ Node ≥ 18. One runtime dependency (`@proof-holdings/delegation-verifier`, itself dependency-free)
13
+ and one peer dependency (`zod`, which your MCP SDK already installs). The GATE uses neither — but
14
+ be precise about what that means: both are loaded when you `import` from this package at all,
15
+ because the package index re-exports the optional Proof layer described below. In practice this
16
+ costs nothing (every MCP SDK install already brings `zod`), but "the gate needs no dependencies"
17
+ would be false as written.
18
+
19
+ ## What this is not
20
+
21
+ This is **not** a consumer-side verifier. `@proof-holdings/delegation-verifier` is the
22
+ issuer-agnostic package a caller runs to check *your* delegation before trusting you. This
23
+ package is the opposite direction: it runs inside *your own* server so that when your own
24
+ delegation stops being valid, your server stops itself — whether or not the caller ever asked.
25
+
26
+ ## Usage
27
+
28
+ Replace the one line where you bind your tool registrar:
29
+
30
+ ```ts
31
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
32
+ import { guardDelegation } from '@proof-holdings/delegation-self-refusal';
33
+
34
+ const server = new McpServer({ name: 'my-server', version: '1.0.0' });
35
+
36
+ // Was: const tool = server.tool.bind(server);
37
+ const tool = guardDelegation(server, {
38
+ token: process.env.DELEGATION_TOKEN, // omit/empty to disable the gate entirely
39
+ principal: 'example.com', // the domain named in the refusal message
40
+ artifactType: 'url', // 'url' (you operate the server) or 'purl' (consumers install it)
41
+ });
42
+
43
+ registerMyTools(tool, http); // unchanged — every module still receives a ToolRegistrar
44
+ ```
45
+
46
+ `guardDelegation` **must run before any `server.tool()`/`registerTool()` call.** Registering a
47
+ tool first and calling `guardDelegation` with a token afterward throws — a gate installed too late
48
+ to see a call it was supposed to guard would otherwise silently protect nothing. With NO token the
49
+ opt-out returns before that check runs and nothing is thrown, because nothing was going to be
50
+ gated; that path is silent by design and is the one state this rule cannot warn you about.
51
+
52
+ ## Optional: install the Proof layer instead
53
+
54
+ `installProofLayer` is the alternative to `guardDelegation` for a publisher who also wants their
55
+ users to be able to see Proof from inside the server they already have. It installs the same gate
56
+ AND registers three tools:
57
+
58
+ | Tool | What it does |
59
+ | --- | --- |
60
+ | `proof_check_this_server` | Reports whether THIS server's delegation is valid right now — and keeps answering while every other tool is refusing |
61
+ | `proof_verify_delegation` | Verifies someone ELSE's Proof of Delegation. No API key |
62
+ | `proof_connect` | Returns the current instructions for connecting the client to the full Proof MCP server |
63
+
64
+ ```ts
65
+ import { installProofLayer } from '@proof-holdings/delegation-self-refusal';
66
+
67
+ const tool = installProofLayer(server, {
68
+ token: process.env.DELEGATION_TOKEN,
69
+ principal: 'example.com',
70
+ artifactType: 'url',
71
+ });
72
+ ```
73
+
74
+ Call it **instead of** `guardDelegation`, not in addition to it — it includes the gate, and a
75
+ second install throws. The same "before any registration" rule applies, and here it applies
76
+ UNCONDITIONALLY: `installProofLayer` runs the check with or without a token, since it registers the
77
+ three tools either way.
78
+
79
+ Two properties are deliberate and worth knowing:
80
+
81
+ - **`proof_check_this_server` is not gated.** It is the tool that reports a revocation, so gating
82
+ it would kill it in exactly the situation it exists for. It is registered through the original
83
+ registrar before the gate is installed. **Whenever a token is configured**, that un-gated
84
+ registrar is never returned or exported, because routing your own tool through it would silently
85
+ disable your self-enforcement while the installation still looked correct from outside. With no
86
+ token there is no gate to bypass, and the call returns exactly that raw registrar — the same
87
+ thing `guardDelegation`'s opt-out hands back.
88
+ - **The two outbound showcase tools call out on their own budget.** `proof_verify_delegation` and
89
+ `proof_connect` are bounded at 4.5s end to end per call — below the verifier's own 5s default —
90
+ because unlike the gate they have no grace window and run synchronously inside your users'
91
+ calls. `proof_connect` makes ONE request and that figure is its only bound.
92
+ `proof_verify_delegation` walks up to three sequential requests, so it additionally caps each
93
+ individual fetch at 1125 ms, derived from the total rather than equal to it. Each of the two has
94
+ its OWN circuit breaker, so a dead issuer cannot be hidden by the other tool's successes.
95
+ `proof_check_this_server` is deliberately outside all of this: it reads the same verdict the gate
96
+ reads, so any poll it triggers runs on the gate's 10s timeout and its grace schedule, not on the
97
+ showcase budget. If proof.holdings is unreachable, `proof_connect`
98
+ serves a packaged copy of the instructions clearly labelled `offline_fallback`, and
99
+ `proof_verify_delegation` answers `unconfirmed` — never a negative verdict about the artifact.
100
+
101
+ The calls those two tools make carry an `X-Proof-Surface: showcase/<version>` header so
102
+ proof.holdings can count them — a count of tool CALLS, not of installations. `installProofLayer`'s
103
+ own periodic status poll carries the SAME header with a different, textually disjoint value,
104
+ `layer/<version>`, never a `showcase/`-prefixed one: that is the denominator, one signal per
105
+ installation that carries the showcase rather than one per call. A `guardDelegation` installation's
106
+ poll carries no header at all and is indistinguishable from any other caller of proof.holdings'
107
+ public status endpoint.
108
+
109
+ What rides alongside the header differs by call, and none of them carries anything beyond what
110
+ that call was already sending. The layer poll always carries THIS installation's own delegation
111
+ token (`pollDelegationStatus` posts `{proof_token: token}`) — there the marker binds "this install
112
+ carries the showcase" to a request that already identifies the installation, and adds nothing else.
113
+ `proof_connect`'s marked call is a bare `GET` with no body at all — the header is everything it
114
+ adds, not even a token rides with it. `proof_verify_delegation`'s marked call carries whatever card
115
+ or token the CALLER supplied, to check SOMEONE ELSE's delegation — never this installation's own,
116
+ and no different from what that call already sent before this header existed. In every case:
117
+ no identity, no argument, nothing about you or your users is added by the marker — it rides calls
118
+ that were happening anyway, and no request is made on its own.
119
+
120
+ With no `token`, the three tools are still installed and nothing is gated;
121
+ `proof_check_this_server` then reports `configured: false` rather than presenting the absence of a
122
+ delegation as a valid one.
123
+
124
+ When the delegation is NOT valid, `proof_check_this_server` returns `reason`, `message` and
125
+ `refusal_message` about it — named rather than counted, because a number typed beside a list drifts
126
+ the moment the list grows and nothing here would go red.
127
+ `reason` is the issuer's machine code and `message` its own sentence — but both are OURS more
128
+ often than that suggests. This package writes `message` in the two unreachable branches (a cold
129
+ start and an exhausted grace window), and it substitutes either field INDEPENDENTLY when the issuer
130
+ omits it — `invalid` for a missing reason, "This delegation is not valid" for a missing message. Both are
131
+ TRUNCATED past a bound (code-sized for `reason`, prose-sized for `message`), because whatever the
132
+ issuer sends lands in the calling agent's context. `refusal_message` beside them is never
133
+ substituted and never the issuer's: it is the exact sentence your gated tools are returning to
134
+ their callers right now, from the same function, not a paraphrase.
135
+
136
+ So `invalid` has more than one source and they read differently: usually the issuer asserting it,
137
+ with a specific `message` (bad signature, unsupported schema version, missing claims) — read that
138
+ message, it is the diagnostic. Only when `message` is our substituted sentence does `invalid` mean
139
+ "the issuer did not tell us why". The two fields are substituted INDEPENDENTLY, so there is a third
140
+ cell the split above would hide: a reply carrying a real `message` and no `reason` at all shows our
141
+ substituted `invalid` beside the issuer's own sentence. The instruction does not change — read the
142
+ message — which is why the reading rule is stated per field rather than per pair. That is what makes
143
+ this tool a usable self-check: you read what your users read, without waiting for one of them to
144
+ tell you.
145
+
146
+ ### What the Proof layer does NOT gate
147
+
148
+ All three showcase tools keep answering when your delegation is revoked — not just
149
+ `proof_check_this_server`. That is deliberate for the one that reports the revocation, and the
150
+ other two inherit it because they share the same un-gated registration. So a revoked server keeps
151
+ offering a working third-party verification tool and a working invitation; only
152
+ `proof_check_this_server` discloses that the server's own authorization is gone.
153
+
154
+ If you render your own status surface, `currentVerdict` / `resolveOptions` give you the same
155
+ verdict the gate enforces, and `refusalMessage(principal, reason)` / `REFUSAL_DETAILS_URL` give you
156
+ the sentence your callers are receiving — so you do not have to write a second one that drifts from
157
+ it.
158
+
159
+ ## What a refusal looks like
160
+
161
+ A gated tool call, when the delegation is not currently valid, returns an MCP tool error result
162
+ instead of running your handler. This gate is silent on success and speaks only when it refuses.
163
+
164
+ With `guardDelegation` alone, that refusal text is the first thing most readers ever see from
165
+ Proof — it names Proof, names you, and gives one action. With `installProofLayer` it is not: the
166
+ three showcase tools' own descriptions already name Proof and the same revocation risk before any
167
+ call is ever refused.
168
+
169
+ **When the issuer answered and the answer was no**, each reason has its OWN lead clause — the
170
+ states are different facts and a reader who is told "revoked" about a pause acts on the wrong one.
171
+ `revoked`:
172
+
173
+ ```
174
+ example.com's authorization to run this was revoked, and Proof (proof.holdings) cannot confirm it
175
+ as valid — retrying will not fix this. Details: proof.holdings/delegation. If this is unexpected,
176
+ contact example.com.
177
+ ```
178
+
179
+ `suspended`, where the difference is the whole point — a pause is reversible and nothing published
180
+ has to be removed:
181
+
182
+ ```
183
+ example.com's authorization to run this is paused, and Proof (proof.holdings) cannot confirm it as
184
+ valid while the pause holds — retrying will not fix this. Details: proof.holdings/delegation. If
185
+ this is unexpected, contact example.com.
186
+ ```
187
+
188
+ `expired` says it ran out, `unknown_delegation` that there is no record of it, and `invalid` that
189
+ Proof does not consider it valid; each keeps the same closing link and action.
190
+
191
+ **When we could not ask at all** the wording is deliberately different, and the difference is the
192
+ point — this is a connectivity failure where your server runs, not a decision anyone made about
193
+ you, and telling the reader to contact you would send them to ask about something that did not
194
+ happen. The two cases have their own lead clause, because one has a cached answer behind it and
195
+ the other never had one:
196
+
197
+ `grace_exhausted` — we had a good answer and could not renew it:
198
+
199
+ ```
200
+ Proof (proof.holdings) has been unreachable for several consecutive checks, so example.com's
201
+ authorization to run this can no longer be confirmed — a connectivity failure here, not a decision
202
+ by example.com. Details: proof.holdings/delegation. Check outbound network access to
203
+ proof.holdings from wherever this server runs.
204
+ ```
205
+
206
+ `unresolved_at_startup` — a first run with nothing cached to extend:
207
+
208
+ ```
209
+ Proof (proof.holdings) could not be reached to confirm example.com's authorization to run this, so
210
+ this call is refused rather than assumed — a connectivity failure here, not a decision by
211
+ example.com. Details: proof.holdings/delegation. Check outbound network access to proof.holdings
212
+ from wherever this server runs.
213
+ ```
214
+
215
+ A reason this version has never seen is refused with the reason carried through (truncated past a
216
+ bound, since it reaches an agent's context) and no claim about what happened — the issuer may add one, and an unrecognized verdict fails closed here
217
+ the same way it does everywhere else in this package.
218
+
219
+ The refusal never throws through your own HTTP client, is never retried internally, and is
220
+ excluded from any retry policy your server otherwise applies to its own outbound calls — a
221
+ refusal is not treated as a transient failure by anything in this package.
222
+
223
+ ## What this cannot protect against
224
+
225
+ This package binds a **cooperative** server. It defends the honest publisher who lost
226
+ authorization — a contract that ended, a key that was rotated, a domain that was not
227
+ re-verified — because the check lives in code the publisher installed and did not remove. It
228
+ does **not** defend against a hostile operator who deletes the wiring: nothing running inside a
229
+ process can force a different, adversarial version of that process to check anything.
230
+ Verification that does not depend on the server's own cooperation is a separate concern, tracked
231
+ under Proof Holdings' own MCP checker.
232
+
233
+ Both public MCP SDK registration entry points — `server.tool()` and `server.registerTool()` — are
234
+ gated identically. A publisher who later calls the SDK's own `registeredTool.update()` to redefine
235
+ a tool's callback after registration bypasses the gate for that tool, the same way editing any
236
+ other line of the publisher's own code would; this package cannot intercept a call it is not on
237
+ the path of.
238
+
239
+ ## Network egress
240
+
241
+ For a `url` artifact (you operate the server), the poll leaves **your** infrastructure — no
242
+ different from any other outbound call your service already makes.
243
+
244
+ For a `purl` artifact (a package a consumer installs and runs themselves), the poll leaves the
245
+ **consumer's** network, every interval, for as long as their process runs. Air-gapped and
246
+ egress-filtered deployments are **permanently denied, not degraded**: the on-disk grace window
247
+ only extends the *last good answer already obtained*, and a first run has none to extend — a
248
+ brand-new install with no cached verdict and no reachable issuer refuses on its very first tool
249
+ call. There is no offline mode.
250
+
251
+ ## What defeats this without touching our code
252
+
253
+ Two things bypass this mechanism entirely, and neither requires touching the delegated server's
254
+ code:
255
+
256
+ - **Rolling the system clock backward.** The grace window is measured against wall-clock time;
257
+ an operator or a compromised host that can move the clock can extend it indefinitely.
258
+ - **Replaying a stale cache file.** The on-disk cache is a plain JSON file. Restoring an older
259
+ copy (from a backup, a snapshot, a container image built before revocation) restores whatever
260
+ verdict was cached at that point.
261
+
262
+ Neither of these requires cooperation from THIS package to defeat — they operate on the
263
+ environment underneath it, which is exactly why the scope limit above matters: this is
264
+ self-enforcement by a cooperative, unmodified process, not tamper-resistance.
@@ -0,0 +1,10 @@
1
+ import type { CacheEntry } from './types.js';
2
+ export declare function defaultCacheDir(): string;
3
+ /**
4
+ * Reads the cached verdict for this token. Returns null on a cold start (no file yet) or a
5
+ * corrupt/unreadable/unrecognized cache file — either way there is nothing usable to serve, so
6
+ * the caller falls back to a live poll.
7
+ */
8
+ export declare function readCache(cacheDir: string, token: string): CacheEntry | null;
9
+ export declare function writeCache(cacheDir: string, token: string, entry: CacheEntry): void;
10
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAyBD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAiB5E;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAGnF"}
package/dist/cache.js ADDED
@@ -0,0 +1,58 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ export function defaultCacheDir() {
6
+ return join(homedir(), '.proof-holdings', 'delegation-self-refusal');
7
+ }
8
+ function cacheFilePath(cacheDir, token) {
9
+ const digest = createHash('sha256').update(token).digest('hex');
10
+ return join(cacheDir, `${digest}.json`);
11
+ }
12
+ /**
13
+ * Validates `lastDecided` against the exact discriminated shape guard.ts's dispatch check
14
+ * assumes. A cache file is untrusted input the moment it is read back from disk (a future
15
+ * version writing a different shape, or a hand-edited file) — an unrecognized `kind` must be
16
+ * treated as corrupt, not silently trusted, since the dispatch check downstream only allows
17
+ * `kind === 'valid'` through and denies everything else.
18
+ */
19
+ function isValidLastDecided(value) {
20
+ if (value === null)
21
+ return true;
22
+ if (typeof value !== 'object')
23
+ return false;
24
+ const record = value;
25
+ if (record.kind === 'valid')
26
+ return true;
27
+ if (record.kind === 'refused') {
28
+ return typeof record.reason === 'string' && typeof record.message === 'string';
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * Reads the cached verdict for this token. Returns null on a cold start (no file yet) or a
34
+ * corrupt/unreadable/unrecognized cache file — either way there is nothing usable to serve, so
35
+ * the caller falls back to a live poll.
36
+ */
37
+ export function readCache(cacheDir, token) {
38
+ try {
39
+ const raw = readFileSync(cacheFilePath(cacheDir, token), 'utf8');
40
+ const parsed = JSON.parse(raw);
41
+ if (typeof parsed !== 'object' ||
42
+ parsed === null ||
43
+ typeof parsed.lastCheckedAtMs !== 'number' ||
44
+ typeof parsed.consecutiveUnresolved !== 'number' ||
45
+ !isValidLastDecided(parsed.lastDecided)) {
46
+ return null;
47
+ }
48
+ return parsed;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ export function writeCache(cacheDir, token, entry) {
55
+ mkdirSync(cacheDir, { recursive: true });
56
+ writeFileSync(cacheFilePath(cacheDir, token), JSON.stringify(entry), 'utf8');
57
+ }
58
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,iBAAiB,EAAE,yBAAyB,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB,EAAE,KAAa;IACpD,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,KAAc;IACxC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC;IACjF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,KAAa;IACvD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAe,CAAC;QAC7C,IACE,OAAO,MAAM,KAAK,QAAQ;YAC1B,MAAM,KAAK,IAAI;YACf,OAAO,MAAM,CAAC,eAAe,KAAK,QAAQ;YAC1C,OAAO,MAAM,CAAC,qBAAqB,KAAK,QAAQ;YAChD,CAAC,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,EACvC,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,QAAgB,EAAE,KAAa,EAAE,KAAiB;IAC3E,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,aAAa,CAAC,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { type McpServerLike, type ToolRegistrar } from './registrar.js';
2
+ import type { GuardOptions } from './types.js';
3
+ export type { McpServerLike, ToolRegistrar };
4
+ /**
5
+ * Replaces `server.tool.bind(server)` as the registrar every `registerTools(tool, http)` module
6
+ * receives. Must run before ANY `server.tool()`/`registerTool()` call — `.bind()` snapshots the
7
+ * function value, so patching `server.tool` after a publisher already captured it has no effect
8
+ * (this is why the package IS the bind point, not a side-effecting call placed elsewhere).
9
+ *
10
+ * With no `opts.token`, this is a no-op: returns the server's own bound `tool`, makes zero network
11
+ * calls, and does not touch `_registeredTools` at all (SC-7).
12
+ *
13
+ * A publisher who also wants the three Proof showcase tools inside their server calls
14
+ * `installProofLayer` (`install.ts`) INSTEAD of this — not in addition to it. The order that
15
+ * function performs cannot be reproduced by composing the two from the outside, which is why it is
16
+ * a separate entry point rather than an option here.
17
+ */
18
+ export declare function guardDelegation(server: McpServerLike, opts: GuardOptions): ToolRegistrar;
19
+ //# sourceMappingURL=guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAgDxF"}
package/dist/guard.js ADDED
@@ -0,0 +1,57 @@
1
+ import { assertInstallableBeforeRegistration, assertNotAlreadyInstalled, markInstalled, readInstallKind, wrapRegistrar, } from './registrar.js';
2
+ import { resolveOptions } from './verdict.js';
3
+ /**
4
+ * Replaces `server.tool.bind(server)` as the registrar every `registerTools(tool, http)` module
5
+ * receives. Must run before ANY `server.tool()`/`registerTool()` call — `.bind()` snapshots the
6
+ * function value, so patching `server.tool` after a publisher already captured it has no effect
7
+ * (this is why the package IS the bind point, not a side-effecting call placed elsewhere).
8
+ *
9
+ * With no `opts.token`, this is a no-op: returns the server's own bound `tool`, makes zero network
10
+ * calls, and does not touch `_registeredTools` at all (SC-7).
11
+ *
12
+ * A publisher who also wants the three Proof showcase tools inside their server calls
13
+ * `installProofLayer` (`install.ts`) INSTEAD of this — not in addition to it. The order that
14
+ * function performs cannot be reproduced by composing the two from the outside, which is why it is
15
+ * a separate entry point rather than an option here.
16
+ */
17
+ export function guardDelegation(server, opts) {
18
+ // A REPEAT of the opt-out is idempotent, not an error: it gated nothing the first time and would
19
+ // hand back the identical raw registrar, so throwing would crash a startup over a benign pair
20
+ // while asserting a layer that is not installed. Anything else — an opt-out after a gate, or a
21
+ // gated call after either — still refuses below.
22
+ if (!opts.token && readInstallKind(server) === 'optout') {
23
+ return server.tool.bind(server);
24
+ }
25
+ // Checked BEFORE the opt-out otherwise: a no-token call on a GATED server is still a caller
26
+ // running both entry points, and answering it silently would hide the same mistake.
27
+ assertNotAlreadyInstalled(server, 'guardDelegation');
28
+ if (!opts.token) {
29
+ // The opt-out HANDS OUT a registrar — the raw, un-gated one — so it counts as having installed,
30
+ // and the marker must be set here too. Without it the reverse order was silently broken:
31
+ // `guardDelegation({no token})` gave the publisher the raw `server.tool`, a later
32
+ // `installProofLayer({token})` passed both preconditions and patched the instance, and every
33
+ // tool registered through the registrar the publisher was still holding ran UNGATED under a
34
+ // revoked delegation. Measured in code review; the token-present direction was already covered
35
+ // and this one was not.
36
+ markInstalled(server, 'optout');
37
+ return server.tool.bind(server);
38
+ }
39
+ assertInstallableBeforeRegistration(server, 'guardDelegation');
40
+ const resolved = resolveOptions({ ...opts, token: opts.token });
41
+ const originalTool = server.tool.bind(server);
42
+ const wrappedTool = wrapRegistrar(originalTool, resolved);
43
+ markInstalled(server, 'gated');
44
+ // Patch the instance too — not just returning the wrapper — so any OTHER code path that later
45
+ // does its own `server.tool.bind(server)` or calls `server.tool(...)` directly still gates.
46
+ server.tool = wrappedTool;
47
+ // registerTool is a second, independent public SDK entry point that also populates
48
+ // _registeredTools — leaving it unwrapped would let a publisher register a fully unguarded
49
+ // tool through it while the pre-registration throw above stays silent (nothing was registered
50
+ // yet at guard-install time).
51
+ if (typeof server.registerTool === 'function') {
52
+ const originalRegisterTool = server.registerTool.bind(server);
53
+ server.registerTool = wrapRegistrar(originalRegisterTool, resolved);
54
+ }
55
+ return wrappedTool;
56
+ }
57
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,EACnC,yBAAyB,EACzB,aAAa,EACb,eAAe,EACf,aAAa,GAGd,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAI9C;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqB,EAAE,IAAkB;IACvE,iGAAiG;IACjG,8FAA8F;IAC9F,+FAA+F;IAC/F,iDAAiD;IACjD,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,eAAe,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;QACxD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IAED,4FAA4F;IAC5F,oFAAoF;IACpF,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAErD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAChB,gGAAgG;QAChG,yFAAyF;QACzF,kFAAkF;QAClF,6FAA6F;QAC7F,4FAA4F;QAC5F,+FAA+F;QAC/F,wBAAwB;QACxB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAChC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IAED,mCAAmC,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAE/D,MAAM,QAAQ,GAAG,cAAc,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAEhE,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,aAAa,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAE1D,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE/B,8FAA8F;IAC9F,4FAA4F;IAC5F,MAAM,CAAC,IAAI,GAAG,WAAW,CAAC;IAE1B,mFAAmF;IACnF,2FAA2F;IAC3F,8FAA8F;IAC9F,8BAA8B;IAC9B,IAAI,OAAO,MAAM,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QAC9C,MAAM,oBAAoB,GAAG,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9D,MAAM,CAAC,YAAY,GAAG,aAAa,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -0,0 +1,34 @@
1
+ export { guardDelegation } from './guard.js';
2
+ export { installProofLayer, SHOWCASE_VERSION } from './install.js';
3
+ export type { InstallProofLayerOptions } from './install.js';
4
+ export type { McpServerLike, ToolRegistrar } from './registrar.js';
5
+ export { SHOWCASE_TOOL_NAMES } from './showcase/tools.js';
6
+ export { SHOWCASE_TIMEOUT_MS } from './showcase/breaker.js';
7
+ export { SHOWCASE_SURFACE_HEADER } from './showcase/marked-fetch.js';
8
+ /**
9
+ * `currentVerdict` and `resolveOptions` are exported so a publisher can read the SAME verdict the
10
+ * gate enforces (the showcase's `proof_check_this_server` does exactly this) instead of polling
11
+ * separately — an independent poll writes the cache outside the jittered grace schedule and breaks
12
+ * the SEC-DLG-02 fix in `schedule.ts`.
13
+ *
14
+ * Note what is deliberately NOT exported: the un-gated registrar `installProofLayer` captures
15
+ * internally. Anything routed through it bypasses self-refusal entirely while the installation
16
+ * still looks correct from outside — `src/__tests__/no-ungated-registrar.test.ts` walks this
17
+ * module's whole surface to keep it that way.
18
+ */
19
+ export { currentVerdict, resolveOptions } from './verdict.js';
20
+ /**
21
+ * The refusal text itself, for the same reason `currentVerdict` is exported above: a publisher who
22
+ * reads the gate's verdict to render their own status page gets a machine `reason` and no way to
23
+ * show the sentence their callers are actually receiving — so they write their own, which is the
24
+ * divergence `proof_check_this_server`'s `refusal_message` and the README pin exist to prevent.
25
+ * Withheld for one review round on "no criterion asks for it"; the argument that the export next to
26
+ * it already serves this exact reader is the stronger one.
27
+ */
28
+ export { refusalMessage, REFUSAL_DETAILS_URL } from './refusal.js';
29
+ export type { ResolvedOptions } from './verdict.js';
30
+ export { MAX_GRACE_FAILURES, nextIntervalMs } from './schedule.js';
31
+ export { pollDelegationStatus, DEFAULT_BASE_URL } from './poll.js';
32
+ export { readCache, writeCache, defaultCacheDir } from './cache.js';
33
+ export type { ArtifactType, CacheEntry, GuardOptions, PollResult, RefusedVerdict, UnresolvedVerdict, ValidVerdict, } from './types.js';
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACnE,YAAY,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC7D,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE;;;;;;;;;;GAUG;AACH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACpE,YAAY,EACV,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,YAAY,GACb,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ export { guardDelegation } from './guard.js';
2
+ export { installProofLayer, SHOWCASE_VERSION } from './install.js';
3
+ export { SHOWCASE_TOOL_NAMES } from './showcase/tools.js';
4
+ export { SHOWCASE_TIMEOUT_MS } from './showcase/breaker.js';
5
+ export { SHOWCASE_SURFACE_HEADER } from './showcase/marked-fetch.js';
6
+ /**
7
+ * `currentVerdict` and `resolveOptions` are exported so a publisher can read the SAME verdict the
8
+ * gate enforces (the showcase's `proof_check_this_server` does exactly this) instead of polling
9
+ * separately — an independent poll writes the cache outside the jittered grace schedule and breaks
10
+ * the SEC-DLG-02 fix in `schedule.ts`.
11
+ *
12
+ * Note what is deliberately NOT exported: the un-gated registrar `installProofLayer` captures
13
+ * internally. Anything routed through it bypasses self-refusal entirely while the installation
14
+ * still looks correct from outside — `src/__tests__/no-ungated-registrar.test.ts` walks this
15
+ * module's whole surface to keep it that way.
16
+ */
17
+ export { currentVerdict, resolveOptions } from './verdict.js';
18
+ /**
19
+ * The refusal text itself, for the same reason `currentVerdict` is exported above: a publisher who
20
+ * reads the gate's verdict to render their own status page gets a machine `reason` and no way to
21
+ * show the sentence their callers are actually receiving — so they write their own, which is the
22
+ * divergence `proof_check_this_server`'s `refusal_message` and the README pin exist to prevent.
23
+ * Withheld for one review round on "no criterion asks for it"; the argument that the export next to
24
+ * it already serves this exact reader is the stronger one.
25
+ */
26
+ export { refusalMessage, REFUSAL_DETAILS_URL } from './refusal.js';
27
+ export { MAX_GRACE_FAILURES, nextIntervalMs } from './schedule.js';
28
+ export { pollDelegationStatus, DEFAULT_BASE_URL } from './poll.js';
29
+ export { readCache, writeCache, defaultCacheDir } from './cache.js';
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE;;;;;;;;;;GAUG;AACH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type McpServerLike, type ToolRegistrar } from './registrar.js';
2
+ import type { GuardOptions } from './types.js';
3
+ /** Version reported in the `X-Proof-Surface` header. Kept in lockstep with package.json by a drift test. */
4
+ export declare const SHOWCASE_VERSION = "0.1.0";
5
+ export interface InstallProofLayerOptions extends GuardOptions {
6
+ }
7
+ export declare function installProofLayer(server: McpServerLike, opts: InstallProofLayerOptions): ToolRegistrar;
8
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO/C,4GAA4G;AAC5G,eAAO,MAAM,gBAAgB,UAAU,CAAC;AAExC,MAAM,WAAW,wBAAyB,SAAQ,YAAY;CAAG;AAkEjE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,wBAAwB,GAAG,aAAa,CAyDtG"}