@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
@@ -0,0 +1,106 @@
1
+ import { refusalMessage } from './refusal.js';
2
+ import { errorResult } from './result.js';
3
+ import { currentVerdict } from './verdict.js';
4
+ export const INSTALLED_MARKER = Symbol.for('proof-holdings.delegation-self-refusal.installed');
5
+ export function readInstallKind(server) {
6
+ const value = server[INSTALLED_MARKER];
7
+ if (value === 'optout')
8
+ return 'optout';
9
+ // ANY other truthy value reads as `gated`, not as absent. The marker key is a `Symbol.for`, so it
10
+ // is shared with every OTHER copy of this package in the process — and the direction that matters
11
+ // is FORWARD: a newer copy may write a kind this version has never heard of, and a strict
12
+ // two-string check would read such a server as untouched and install a second layer over an
13
+ // existing gate. (An earlier version of this comment justified the widening by a copy written
14
+ // BEFORE kinds existed. That was false and a reviewer proved it: the marker string appears in no
15
+ // commit — `git log --all -S` finds nothing — and no version of this package had been published
16
+ // at the time, so the FIRST installable version already carries kinds. The rule is right; the
17
+ // story was not.)
18
+ // Erring toward `gated` errs toward refusing, which is the direction that cannot silently ungate
19
+ // anything; the cost is a clear error where an exotic combination might have been survivable.
20
+ if (value)
21
+ return 'gated';
22
+ return undefined;
23
+ }
24
+ export function markInstalled(server, kind) {
25
+ server[INSTALLED_MARKER] = kind;
26
+ }
27
+ /**
28
+ * Refuses a second call that cannot cover the registrar a first call already handed out.
29
+ *
30
+ * The two states fail for OPPOSITE reasons, which is why one summary cannot serve both. After
31
+ * `gated`, a second call would install over a working gate. After `optout`, the raw registrar stays
32
+ * perfectly VALID and simply bypasses whatever is installed next — nothing invalidated it. The
33
+ * message below is picked from the kind for that reason: claiming an installed layer where only a
34
+ * raw registrar was handed out sends the reader looking for something that does not exist.
35
+ */
36
+ export function assertNotAlreadyInstalled(server, entryPoint) {
37
+ const kind = readInstallKind(server);
38
+ if (kind === undefined)
39
+ return;
40
+ if (kind === 'optout') {
41
+ throw new Error(`${entryPoint}: a previous call with no token already handed out this server's RAW ` +
42
+ 'registrar. Whatever is installed now cannot cover it — tools registered through the ' +
43
+ 'registrar that is already out would bypass it. Configure the token before the first ' +
44
+ 'call, or keep using the un-gated one.');
45
+ }
46
+ // No per-case CONSEQUENCE in the text. The previous tail ("running both leaves the showcase
47
+ // gated by it") held only where a showcase is registered after a gate exists, and was false in
48
+ // guard→guard (no showcase anywhere) and install→guard (the showcase is already registered,
49
+ // un-gated, and a later gate cannot retroactively cover it). What IS true in all four gated
50
+ // cells is only the state and the instruction, so that is all this says.
51
+ throw new Error(`${entryPoint}: this server already carries the Proof self-refusal layer. Install it exactly ` +
52
+ 'ONCE — call either guardDelegation or installProofLayer, never both (installProofLayer ' +
53
+ 'includes the gate).');
54
+ }
55
+ /**
56
+ * The shared precondition both entry points check: the SDK must expose `_registeredTools`, and
57
+ * nothing may be registered yet. Placed late, this package does nothing — `.bind()` snapshots the
58
+ * function value — so refusing to run is the only honest answer to "installed too late".
59
+ */
60
+ export function assertInstallableBeforeRegistration(server, entryPoint) {
61
+ // The entry point is a PARAMETER because this message is read by a publisher staring at a
62
+ // startup crash: naming `guardDelegation` when they called `installProofLayer` sends them to fix
63
+ // a function they never invoked — the same ambiguity SC-13 exists to remove at the other
64
+ // precondition.
65
+ if (server._registeredTools === undefined) {
66
+ throw new Error(`${entryPoint}: the installed @modelcontextprotocol/sdk McpServer does not expose ` +
67
+ '_registeredTools — this package cannot verify no tools were registered before the gate ' +
68
+ 'and refuses to run silently unguarded. Pin a known-compatible SDK version.');
69
+ }
70
+ const alreadyRegistered = Object.keys(server._registeredTools);
71
+ if (alreadyRegistered.length > 0) {
72
+ throw new Error(`${entryPoint} must run before any server.tool()/registerTool() call — ` +
73
+ `${alreadyRegistered.length} tool(s) already registered: ${alreadyRegistered.join(', ')}`);
74
+ }
75
+ }
76
+ /**
77
+ * Wraps a registrar (`server.tool` or `server.registerTool` — both take the handler as their
78
+ * final positional argument) so every call through it is gated on `currentVerdict`.
79
+ *
80
+ * The dispatch check is a deliberate ALLOW-LIST (`kind === 'valid'` runs the handler; anything
81
+ * else, including a shape `currentVerdict`'s return type does not admit but that could reach here
82
+ * through some future bug, refuses) rather than a deny-list keyed on `'refused'` — a security gate
83
+ * must default to denying what it does not positively recognize as authorized.
84
+ *
85
+ * The handler's own arguments are passed through OPAQUELY: never read, indexed, logged or
86
+ * serialized. This package runs inside someone else's production process, where those arguments
87
+ * are that publisher's user data — `src/__tests__/drift/mcp-showcase.test.ts` pins the property
88
+ * behaviourally rather than trusting this comment.
89
+ */
90
+ export function wrapRegistrar(original, opts) {
91
+ return (...args) => {
92
+ const handler = args[args.length - 1];
93
+ if (typeof handler !== 'function') {
94
+ return original(...args);
95
+ }
96
+ const gatedHandler = async (...handlerArgs) => {
97
+ const verdict = await currentVerdict(opts);
98
+ if (verdict.kind === 'valid') {
99
+ return handler(...handlerArgs);
100
+ }
101
+ return errorResult(refusalMessage(opts.principal, verdict.reason));
102
+ };
103
+ return original(...args.slice(0, -1), gatedHandler);
104
+ };
105
+ }
106
+ //# sourceMappingURL=registrar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registrar.js","sourceRoot":"","sources":["../src/registrar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /** Matches the MCP `CallToolResult` shape (content blocks + isError) without a runtime SDK import. */
2
+ export interface ToolResult {
3
+ content: Array<{
4
+ type: 'text';
5
+ text: string;
6
+ }>;
7
+ isError: boolean;
8
+ }
9
+ export declare function errorResult(message: string): ToolResult;
10
+ /**
11
+ * A successful result carrying structured data. The MCP content model has no JSON block, so the
12
+ * repo-wide convention (mcp/src/types.ts `jsonResult`) is a single pretty-printed text block —
13
+ * mirrored here rather than imported, since this package cannot depend on the MCP server.
14
+ */
15
+ export declare function jsonResult(data: unknown): ToolResult;
16
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,CAEpD"}
package/dist/result.js ADDED
@@ -0,0 +1,12 @@
1
+ export function errorResult(message) {
2
+ return { content: [{ type: 'text', text: message }], isError: true };
3
+ }
4
+ /**
5
+ * A successful result carrying structured data. The MCP content model has no JSON block, so the
6
+ * repo-wide convention (mcp/src/types.ts `jsonResult`) is a single pretty-printed text block —
7
+ * mirrored here rather than imported, since this package cannot depend on the MCP server.
8
+ */
9
+ export function jsonResult(data) {
10
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], isError: false };
11
+ }
12
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC9F,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Hard ceiling on how many consecutive unresolved polls may be bridged by serving the last good
3
+ * verdict before the gate refuses outright. This is a library constant, not a GuardOptions field
4
+ * — SEC-DLG-02 (b-brainstorm-delegation-revocation) found an unbounded grace window CRITICAL, and
5
+ * the recorded fix was a bound the caller cannot raise, not a documented recommendation.
6
+ */
7
+ export declare const MAX_GRACE_FAILURES = 3;
8
+ /**
9
+ * Returns baseMs plus a jitter in [1, baseMs * JITTER_FRACTION), guaranteeing the result is never
10
+ * an exact multiple of 60_000 when baseMs is (the default case) — a fleet of installs polling on
11
+ * the wall-clock minute is a self-inflicted thundering herd against /proofs/validate.
12
+ */
13
+ export declare function nextIntervalMs(baseMs?: number): number;
14
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAKpC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,GAAE,MAAiC,GAAG,MAAM,CAIhF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Hard ceiling on how many consecutive unresolved polls may be bridged by serving the last good
3
+ * verdict before the gate refuses outright. This is a library constant, not a GuardOptions field
4
+ * — SEC-DLG-02 (b-brainstorm-delegation-revocation) found an unbounded grace window CRITICAL, and
5
+ * the recorded fix was a bound the caller cannot raise, not a documented recommendation.
6
+ */
7
+ export const MAX_GRACE_FAILURES = 3;
8
+ const DEFAULT_POLL_INTERVAL_MS = 60_000;
9
+ const JITTER_FRACTION = 0.2;
10
+ /**
11
+ * Returns baseMs plus a jitter in [1, baseMs * JITTER_FRACTION), guaranteeing the result is never
12
+ * an exact multiple of 60_000 when baseMs is (the default case) — a fleet of installs polling on
13
+ * the wall-clock minute is a self-inflicted thundering herd against /proofs/validate.
14
+ */
15
+ export function nextIntervalMs(baseMs = DEFAULT_POLL_INTERVAL_MS) {
16
+ const jitterCeiling = Math.max(1, Math.floor(baseMs * JITTER_FRACTION));
17
+ const jitter = 1 + Math.floor(Math.random() * jitterCeiling);
18
+ return baseMs + jitter;
19
+ }
20
+ //# sourceMappingURL=schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB,wBAAwB;IACtE,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC;IAC7D,OAAO,MAAM,GAAG,MAAM,CAAC;AACzB,CAAC"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Hard timeout for a whole showcase call — not for each HTTP request inside it.
3
+ *
4
+ * Scope, stated because the name does not carry it: this bounds the two showcase tools that call
5
+ * out on their OWN behalf (`proof_verify_delegation`, `proof_connect`).
6
+ * `proof_check_this_server` is deliberately outside it — it reads the gate's shared verdict
7
+ * (SC-3), so its network cost is `poll.ts`'s 10s budget, the same one every gated publisher tool
8
+ * already pays, and wrapping it would fork the grace state machine `schedule.ts` owns.
9
+ *
10
+ * Deliberately BELOW both library defaults it sits in front of — `@proof-holdings/delegation-verifier`
11
+ * defaults to 5000ms and `poll.ts` to 10000ms — and applied to the ENTIRE operation, because the
12
+ * verifier's own budget is PER FETCH and one verification walks up to `SHOWCASE_MAX_LEGS` requests
13
+ * sequentially. Giving both bounds the same number would permit a ~9s user-facing stall, which is
14
+ * exactly the "our slowness is indistinguishable from the publisher's code" failure this constant
15
+ * exists to prevent. Pinned by `breaker.test.ts`.
16
+ */
17
+ export declare const SHOWCASE_TIMEOUT_MS = 4500;
18
+ /**
19
+ * How many sequential HTTP legs one verification can take: JWKS → status list → status endpoint.
20
+ *
21
+ * THREE, not two. `checkDelegationStatus` (packages/delegation-verifier/src/status.ts) falls
22
+ * through to the status ENDPOINT whenever the status list cannot answer, so the third leg is
23
+ * reachable on a live issuer. An earlier version of this file derived the per-request budget from
24
+ * a two-leg walk while the comment above it said three — and at two legs the arithmetic was a
25
+ * photo finish (2 × 1500 = 3000, exactly the whole-call bound), so any parse or verify time
26
+ * between them pushed a healthy-but-slow issuer over the deadline and charged it as a failure.
27
+ */
28
+ export declare const SHOWCASE_MAX_LEGS = 3;
29
+ /**
30
+ * The budget handed to a LIBRARY that applies it per request, derived from the leg count PLUS ONE
31
+ * so the whole-call bound is a real ceiling with headroom rather than a photo finish.
32
+ *
33
+ * Dividing by the leg count exactly (the first attempt at this fix) reproduced the very defect the
34
+ * comment above rejects: `3 × 1000 = 3000` IS the whole-call bound, leaving zero milliseconds for
35
+ * the work BETWEEN the legs — JWKS parsing, JWT signature verification, JSON decoding — all of
36
+ * which sit inside the same deadline, because the timer starts before `fn` is invoked. The `+ 1`
37
+ * is that work's share.
38
+ *
39
+ * It also matters which way the arithmetic errs. A per-fetch budget squeezed too tight aborts a leg
40
+ * against a merely-average issuer, the verifier reports `jwks_unavailable`, `isIssuerUnreachable`
41
+ * charges it to the breaker, and three of those deny verification of EVERY artifact for a cooldown
42
+ * — a false `issuer_unreachable_cooldown` against a live service. So the total was raised (still
43
+ * strictly below the verifier's 5000ms and `poll.ts`'s 10000ms, which SC-7 requires) rather than
44
+ * the per-request budget being cut further.
45
+ */
46
+ export declare const SHOWCASE_PER_REQUEST_TIMEOUT_MS: number;
47
+ export type BreakerResult<T> = {
48
+ ok: true;
49
+ value: T;
50
+ } | {
51
+ ok: false;
52
+ tripped: true;
53
+ } | {
54
+ ok: false;
55
+ tripped: false;
56
+ error: unknown;
57
+ };
58
+ export interface RunOptions<T> {
59
+ /**
60
+ * Decides whether a RETURNED value counts as a failure.
61
+ *
62
+ * Required because "did it throw" is the wrong question for this package's own dependencies:
63
+ * `@proof-holdings/delegation-verifier` reports an unreachable issuer by RETURNING
64
+ * `{valid: false, outcome: 'unconfirmed'}` rather than throwing, so a throw-only breaker read a
65
+ * dead issuer as a success, reset its counter on every call, and never opened — measured in code
66
+ * review as 10 outbound attempts with 0 trips. Without this hook SC-7 is unmet for the very tool
67
+ * that makes the most outbound calls.
68
+ */
69
+ isFailure?: (value: T) => boolean;
70
+ }
71
+ export interface Breaker {
72
+ run<T>(fn: (signal: AbortSignal) => Promise<T>, options?: RunOptions<T>): Promise<BreakerResult<T>>;
73
+ }
74
+ export interface BreakerOptions {
75
+ failureThreshold?: number;
76
+ cooldownMs?: number;
77
+ timeoutMs?: number;
78
+ /** Injected clock. Tests advance it directly rather than reaching for fake global timers. */
79
+ now?: () => number;
80
+ }
81
+ /**
82
+ * A per-installation circuit breaker for the showcase's outbound calls.
83
+ *
84
+ * State lives in this closure, NOT on disk: the cache directory belongs to the self-refusal grace
85
+ * state machine, and writing showcase state into it would move `lastCheckedAtMs` outside the
86
+ * jittered schedule `schedule.ts` exists to keep (the SEC-DLG-02 fix). The showcase therefore does
87
+ * not inherit the grace window and needs its own failure accounting — which is exactly what SC-7
88
+ * asks for.
89
+ */
90
+ export declare function createBreaker(options?: BreakerOptions): Breaker;
91
+ //# sourceMappingURL=breaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breaker.d.ts","sourceRoot":"","sources":["../../src/showcase/breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,+BAA+B,QAA4D,CAAC;AAKzG,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAElD,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;CACnC;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;CACrG;AAED,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AASD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CA+EnE"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Hard timeout for a whole showcase call — not for each HTTP request inside it.
3
+ *
4
+ * Scope, stated because the name does not carry it: this bounds the two showcase tools that call
5
+ * out on their OWN behalf (`proof_verify_delegation`, `proof_connect`).
6
+ * `proof_check_this_server` is deliberately outside it — it reads the gate's shared verdict
7
+ * (SC-3), so its network cost is `poll.ts`'s 10s budget, the same one every gated publisher tool
8
+ * already pays, and wrapping it would fork the grace state machine `schedule.ts` owns.
9
+ *
10
+ * Deliberately BELOW both library defaults it sits in front of — `@proof-holdings/delegation-verifier`
11
+ * defaults to 5000ms and `poll.ts` to 10000ms — and applied to the ENTIRE operation, because the
12
+ * verifier's own budget is PER FETCH and one verification walks up to `SHOWCASE_MAX_LEGS` requests
13
+ * sequentially. Giving both bounds the same number would permit a ~9s user-facing stall, which is
14
+ * exactly the "our slowness is indistinguishable from the publisher's code" failure this constant
15
+ * exists to prevent. Pinned by `breaker.test.ts`.
16
+ */
17
+ export const SHOWCASE_TIMEOUT_MS = 4_500;
18
+ /**
19
+ * How many sequential HTTP legs one verification can take: JWKS → status list → status endpoint.
20
+ *
21
+ * THREE, not two. `checkDelegationStatus` (packages/delegation-verifier/src/status.ts) falls
22
+ * through to the status ENDPOINT whenever the status list cannot answer, so the third leg is
23
+ * reachable on a live issuer. An earlier version of this file derived the per-request budget from
24
+ * a two-leg walk while the comment above it said three — and at two legs the arithmetic was a
25
+ * photo finish (2 × 1500 = 3000, exactly the whole-call bound), so any parse or verify time
26
+ * between them pushed a healthy-but-slow issuer over the deadline and charged it as a failure.
27
+ */
28
+ export const SHOWCASE_MAX_LEGS = 3;
29
+ /**
30
+ * The budget handed to a LIBRARY that applies it per request, derived from the leg count PLUS ONE
31
+ * so the whole-call bound is a real ceiling with headroom rather than a photo finish.
32
+ *
33
+ * Dividing by the leg count exactly (the first attempt at this fix) reproduced the very defect the
34
+ * comment above rejects: `3 × 1000 = 3000` IS the whole-call bound, leaving zero milliseconds for
35
+ * the work BETWEEN the legs — JWKS parsing, JWT signature verification, JSON decoding — all of
36
+ * which sit inside the same deadline, because the timer starts before `fn` is invoked. The `+ 1`
37
+ * is that work's share.
38
+ *
39
+ * It also matters which way the arithmetic errs. A per-fetch budget squeezed too tight aborts a leg
40
+ * against a merely-average issuer, the verifier reports `jwks_unavailable`, `isIssuerUnreachable`
41
+ * charges it to the breaker, and three of those deny verification of EVERY artifact for a cooldown
42
+ * — a false `issuer_unreachable_cooldown` against a live service. So the total was raised (still
43
+ * strictly below the verifier's 5000ms and `poll.ts`'s 10000ms, which SC-7 requires) rather than
44
+ * the per-request budget being cut further.
45
+ */
46
+ export const SHOWCASE_PER_REQUEST_TIMEOUT_MS = Math.floor(SHOWCASE_TIMEOUT_MS / (SHOWCASE_MAX_LEGS + 1));
47
+ const DEFAULT_FAILURE_THRESHOLD = 3;
48
+ const DEFAULT_COOLDOWN_MS = 60_000;
49
+ class ShowcaseTimeoutError extends Error {
50
+ constructor(timeoutMs) {
51
+ super(`showcase call exceeded its ${timeoutMs}ms budget`);
52
+ this.name = 'ShowcaseTimeoutError';
53
+ }
54
+ }
55
+ /**
56
+ * A per-installation circuit breaker for the showcase's outbound calls.
57
+ *
58
+ * State lives in this closure, NOT on disk: the cache directory belongs to the self-refusal grace
59
+ * state machine, and writing showcase state into it would move `lastCheckedAtMs` outside the
60
+ * jittered schedule `schedule.ts` exists to keep (the SEC-DLG-02 fix). The showcase therefore does
61
+ * not inherit the grace window and needs its own failure accounting — which is exactly what SC-7
62
+ * asks for.
63
+ */
64
+ export function createBreaker(options = {}) {
65
+ const failureThreshold = options.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
66
+ const cooldownMs = options.cooldownMs ?? DEFAULT_COOLDOWN_MS;
67
+ const timeoutMs = options.timeoutMs ?? SHOWCASE_TIMEOUT_MS;
68
+ const now = options.now ?? Date.now;
69
+ let consecutiveFailures = 0;
70
+ let openUntilMs = 0;
71
+ function recordFailure() {
72
+ consecutiveFailures++;
73
+ if (consecutiveFailures >= failureThreshold) {
74
+ openUntilMs = now() + cooldownMs;
75
+ }
76
+ }
77
+ return {
78
+ async run(fn, runOptions = {}) {
79
+ if (openUntilMs > now()) {
80
+ return { ok: false, tripped: true };
81
+ }
82
+ // The window elapsed: forget the streak that opened it, so one more failure does not
83
+ // instantly re-open on a stale count.
84
+ if (openUntilMs !== 0) {
85
+ openUntilMs = 0;
86
+ consecutiveFailures = 0;
87
+ }
88
+ const controller = new AbortController();
89
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
90
+ const deadline = new Promise((_resolve, reject) => {
91
+ controller.signal.addEventListener('abort', () => reject(new ShowcaseTimeoutError(timeoutMs)), { once: true });
92
+ });
93
+ try {
94
+ // Bounds the WHOLE call, not each request inside it — a callee that ignores the signal
95
+ // (the verifier applies its own per-fetch budget instead) is still cut off here.
96
+ const value = await Promise.race([fn(controller.signal), deadline]);
97
+ // A throwing `isFailure` is a bug in THIS package, so it is neither charged to the issuer
98
+ // (which would open the breaker against a service that answered fine) nor allowed to CLEAR
99
+ // the streak — a predicate that threw on exactly the dead-issuer shape would otherwise
100
+ // erase real failures and hold the breaker closed forever, which is the original defect
101
+ // wearing a new disguise. It is also logged: `verdict.ts` uses the same channel, and a
102
+ // silent swallow here would hide the one condition that makes the breaker inert.
103
+ let predicateThrew = false;
104
+ let predicateSaysFailure = false;
105
+ try {
106
+ predicateSaysFailure = runOptions.isFailure?.(value) === true;
107
+ }
108
+ catch (error) {
109
+ predicateThrew = true;
110
+ console.error(`delegation-self-refusal: showcase isFailure predicate threw (failure count left ` +
111
+ `untouched): ${error instanceof Error ? error.message : String(error)}`);
112
+ }
113
+ if (predicateSaysFailure) {
114
+ recordFailure();
115
+ return { ok: false, tripped: false, error: value };
116
+ }
117
+ if (!predicateThrew) {
118
+ consecutiveFailures = 0;
119
+ }
120
+ return { ok: true, value };
121
+ }
122
+ catch (error) {
123
+ recordFailure();
124
+ return { ok: false, tripped: false, error };
125
+ }
126
+ finally {
127
+ clearTimeout(timer);
128
+ }
129
+ },
130
+ };
131
+ }
132
+ //# sourceMappingURL=breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breaker.js","sourceRoot":"","sources":["../../src/showcase/breaker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { Breaker } from './breaker.js';
2
+ import type { FetchLike } from './marked-fetch.js';
3
+ export interface ConnectInfo {
4
+ message: string;
5
+ /**
6
+ * The hosted server's own URL — the route that needs no install and no API key to start. Optional
7
+ * because an older issuer deployment answers without it; absent must stay absent rather than be
8
+ * filled in with a guess, since a fabricated URL would point the agent at a host that may serve
9
+ * nothing.
10
+ */
11
+ remote_url?: string;
12
+ remote_config?: unknown;
13
+ install_command: string;
14
+ /** Why the npm route is the second choice while the published build lags the current surface. */
15
+ install_caveat?: string;
16
+ docs_url: string;
17
+ client_config?: unknown;
18
+ updated_at?: string;
19
+ source: 'live' | 'offline_fallback';
20
+ }
21
+ /**
22
+ * The text `proof_connect` falls back to when the issuer cannot be reached.
23
+ *
24
+ * It is deliberately marked `offline_fallback` in the payload rather than served silently: this
25
+ * copy is FROZEN into the publisher's `node_modules` at install time, and the live answer changes
26
+ * between the parts of the single-MCP plan (a remote server arrives in part 3). An agent reading
27
+ * a stale instruction with no signal that it is stale would confidently tell a user to do the
28
+ * wrong thing.
29
+ */
30
+ export declare const OFFLINE_FALLBACK: ConnectInfo;
31
+ /**
32
+ * Fetches the current connection instructions at CALL time (SC-6), through the showcase breaker
33
+ * so a slow or dead issuer costs the publisher's user one bounded wait rather than one per call.
34
+ *
35
+ * Never throws and never caches: two calls in a row do exactly the same thing, which is what
36
+ * "harmless on repeat" means for a tool an agent may call speculatively.
37
+ */
38
+ export declare function fetchConnectInfo(baseUrl: string, breaker: Breaker, fetchImpl: FetchLike): Promise<ConnectInfo>;
39
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/showcase/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,iGAAiG;IACjG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAAC;CACrC;AAoBD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAc9B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,WAAW,CAAC,CAwDtB"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Cap on the connect payload this will accept.
3
+ *
4
+ * The live payload measures ~800 bytes, so 64 KiB is ~80× headroom. The verifier's own
5
+ * `MAX_REMOTE_BODY_BYTES` is 512 KiB because it reads artifacts that can legitimately be large (a
6
+ * JWKS, a status list); nothing here can be, so the cap is tighter. A local constant rather than an
7
+ * import: it is not exported from the verifier's `index.ts`, and this package must not reach into
8
+ * another one's internals.
9
+ *
10
+ * TWO BOUNDS the name does not carry, both shared verbatim with the verifier's version of this
11
+ * check and recorded so `_BYTES` is not later read as a guarantee: `body.length` counts UTF-16 code
12
+ * units, so a 64 Ki-CHARACTER body can be ~192 KiB of UTF-8; and the second check runs after
13
+ * `response.text()` has already buffered, so it refuses the payload rather than preventing it being
14
+ * held. The declared-length check above it is the only pre-transfer refusal, and the real bound on
15
+ * what can be buffered is the breaker's 4.5s deadline.
16
+ */
17
+ const MAX_CONNECT_BODY_BYTES = 64 * 1024;
18
+ /**
19
+ * The text `proof_connect` falls back to when the issuer cannot be reached.
20
+ *
21
+ * It is deliberately marked `offline_fallback` in the payload rather than served silently: this
22
+ * copy is FROZEN into the publisher's `node_modules` at install time, and the live answer changes
23
+ * between the parts of the single-MCP plan (a remote server arrives in part 3). An agent reading
24
+ * a stale instruction with no signal that it is stale would confidently tell a user to do the
25
+ * wrong thing.
26
+ */
27
+ export const OFFLINE_FALLBACK = {
28
+ message: 'Proof (proof.holdings) verifies real-world control — domains, phone numbers, and human approval — and issues signed proof tokens an agent can check. The full Proof MCP server adds the rest of the platform to this client. The quickest way in is the hosted server at https://api.proof.holdings/mcp — nothing to install and no API key needed to start. This text was packaged with the Proof layer and may be out of date; proof.holdings/docs/mcp always has the current instructions.',
29
+ // The production address, deliberately: a copy frozen into node_modules cannot know which
30
+ // deployment it will be read beside, and this is the same posture `docs_url` already takes.
31
+ remote_url: 'https://api.proof.holdings/mcp',
32
+ remote_config: {
33
+ mcpServers: { proof: { type: 'http', url: 'https://api.proof.holdings/mcp' } },
34
+ },
35
+ install_command: 'npx -y @proof-holdings/mcp-server',
36
+ install_caveat: 'The @proof-holdings/mcp-server build currently on npm predates the delegation tools and the keyless public mode, so it exposes an older and smaller surface. Until it is republished, remote_url is the way to reach the current server.',
37
+ docs_url: 'https://proof.holdings/docs/mcp',
38
+ source: 'offline_fallback',
39
+ };
40
+ /**
41
+ * Fetches the current connection instructions at CALL time (SC-6), through the showcase breaker
42
+ * so a slow or dead issuer costs the publisher's user one bounded wait rather than one per call.
43
+ *
44
+ * Never throws and never caches: two calls in a row do exactly the same thing, which is what
45
+ * "harmless on repeat" means for a tool an agent may call speculatively.
46
+ */
47
+ export async function fetchConnectInfo(baseUrl, breaker, fetchImpl) {
48
+ const origin = baseUrl.replace(/\/+$/, '');
49
+ const outcome = await breaker.run(async (signal) => {
50
+ const response = await fetchImpl(`${origin}/api/v1/mcp/connect`, {
51
+ method: 'GET',
52
+ signal,
53
+ // The same posture `packages/delegation-verifier/src/status.ts` takes on its remote reads,
54
+ // adopted here rather than left as an unexplained difference — and the reason is sharper on
55
+ // this path: what comes back becomes INSTRUCTIONS in the agent's context. `baseUrl` is the
56
+ // publisher's to configure, so an open redirect on it would carry this read off the origin
57
+ // they chose, with the answer still presented as ours.
58
+ redirect: 'error',
59
+ });
60
+ if (!response.ok) {
61
+ throw new Error(`connect info responded ${response.status}`);
62
+ }
63
+ // Declared length first (cheap, refuses before the transfer), then the real one, because
64
+ // `content-length` is absent under chunked encoding and is a claim either way.
65
+ const declaredLength = Number(response.headers.get('content-length'));
66
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_CONNECT_BODY_BYTES) {
67
+ throw new Error('connect info response is too large');
68
+ }
69
+ const body = await response.text();
70
+ if (body.length > MAX_CONNECT_BODY_BYTES) {
71
+ throw new Error('connect info response is too large');
72
+ }
73
+ return JSON.parse(body);
74
+ });
75
+ if (!outcome.ok) {
76
+ return OFFLINE_FALLBACK;
77
+ }
78
+ const body = outcome.value;
79
+ if (typeof body?.message !== 'string' || typeof body?.install_command !== 'string') {
80
+ return OFFLINE_FALLBACK;
81
+ }
82
+ // Field-by-field rather than a spread: what comes back becomes instructions in the agent's
83
+ // context, so the shape an issuer can put there is the one this package chose to carry. The cost
84
+ // is that a new issuer-side field is inert until it is added here — which is why the remote
85
+ // route below had to be added explicitly rather than arriving on its own.
86
+ return {
87
+ message: body.message,
88
+ ...(typeof body.remote_url === 'string' ? { remote_url: body.remote_url } : {}),
89
+ ...(body.remote_config !== undefined ? { remote_config: body.remote_config } : {}),
90
+ install_command: body.install_command,
91
+ ...(typeof body.install_caveat === 'string' ? { install_caveat: body.install_caveat } : {}),
92
+ docs_url: typeof body.docs_url === 'string' ? body.docs_url : OFFLINE_FALLBACK.docs_url,
93
+ ...(body.client_config !== undefined ? { client_config: body.client_config } : {}),
94
+ ...(typeof body.updated_at === 'string' ? { updated_at: body.updated_at } : {}),
95
+ source: 'live',
96
+ };
97
+ }
98
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../../src/showcase/connect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The showcase tool descriptions, in their own module with ZERO external imports.
3
+ *
4
+ * Separated from `tools.ts` (which pulls in `zod` and the verifier) so the drift suite in the
5
+ * issuer's repository can import and assert the REAL strings rather than text-scanning for them.
6
+ * A description is what an agent reads instead of the schema, so it is the part most worth a guard
7
+ * and the part a text scan checks least honestly.
8
+ */
9
+ /**
10
+ * The cross-reference every showcase description carries (SC-5).
11
+ *
12
+ * Three tools with different names do not, on their own, tell anyone that a fuller server exists —
13
+ * they just look like three unrelated tools, which HIDES the duplication rather than resolving it.
14
+ * Naming the delta is what turns the showcase from a second surface into a doorway.
15
+ */
16
+ export declare function showcaseFooter(principal: string): string;
17
+ /**
18
+ * The short form of the consequence measured in `docs/agent-instruction-compliance.md` (Finding
19
+ * 2): naming what goes wrong for the reader, not explaining the mechanism, is what moved
20
+ * compliance (`t3` at 9/10 against 3/10 for each softer wording that only explained or instructed).
21
+ *
22
+ * That result was measured on the description of `send_email` — a tool the agent was ALREADY
23
+ * calling for its own task. These three tools are not: an agent busy with an unrelated task has no
24
+ * reason to call any of them, so this exact configuration — the phrase on a tool the agent's task
25
+ * never touches — has never been measured. The cost of carrying it anyway is low (our own words in
26
+ * our own tools, no publisher consent needed, no third party harmed), which is why it ships. But
27
+ * that cost argument is not a measurement, and this is a hypothesis, not a confirmed result.
28
+ */
29
+ export declare function showcaseConsequence(principal: string): string;
30
+ export declare function describeCheckThisServer(principal: string): string;
31
+ /**
32
+ * The identity paragraph states the same three rules as the full server's `verify_delegation`
33
+ * description (`mcp/src/tools/delegation-verify.ts`), and the issuer's
34
+ * `src/__tests__/drift/mcp-showcase.test.ts` holds THOSE THREE STATEMENTS across both — not the
35
+ * paragraph. The wordings are deliberately not identical (this one keeps the positive example
36
+ * inline), so do not read the guard as pinning equality: what it forbids is either side losing a
37
+ * rule, not either side rephrasing.
38
+ *
39
+ * It is here in full rather than summarised because this is the description an agent reads when it
40
+ * meets Proof inside a publisher's server, and the shorter version it used to carry is what let a
41
+ * live agent take the artifact name out of the checked artifact's own `package.json` and accuse a
42
+ * real publisher of acting without authority.
43
+ */
44
+ export declare function describeVerifyDelegation(principal: string): string;
45
+ export declare function describeConnect(principal: string): string;
46
+ //# sourceMappingURL=descriptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"descriptions.d.ts","sourceRoot":"","sources":["../../src/showcase/descriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CASjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAqBlE;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQzD"}