@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,123 @@
1
+ import { DEFAULT_BASE_URL } from './poll.js';
2
+ import { assertInstallableBeforeRegistration, assertNotAlreadyInstalled, markInstalled, wrapRegistrar, } from './registrar.js';
3
+ import { resolveOptions } from './verdict.js';
4
+ import { createBreaker } from './showcase/breaker.js';
5
+ import { createLayerSurfaceFetch, createMarkedFetch } from './showcase/marked-fetch.js';
6
+ import { registerShowcase, SHOWCASE_TOOL_NAMES } from './showcase/tools.js';
7
+ import { showcaseInstructionsParagraph } from './showcase/instructions.js';
8
+ /** Version reported in the `X-Proof-Surface` header. Kept in lockstep with package.json by a drift test. */
9
+ export const SHOWCASE_VERSION = '0.1.0';
10
+ /**
11
+ * Installs the self-refusal gate AND the three-tool Proof showcase in one call — the alternative
12
+ * to `guardDelegation` for a publisher who wants their users to see Proof inside the server they
13
+ * already have.
14
+ *
15
+ * The ORDER below is the whole reason this is a separate entry point rather than a flag, and it is
16
+ * not reproducible by composing the two halves from outside:
17
+ *
18
+ * 1. precondition — nothing registered yet, and this layer not already installed;
19
+ * 2. capture the ORIGINAL, un-gated registrars;
20
+ * 3. register the showcase through them;
21
+ * 4. append the Proof recognition paragraph to `server.server._instructions` — unconditionally,
22
+ * on both the token and no-token paths, degrading silently on either failure class (the field
23
+ * absent, or the read/write throwing) rather than aborting an otherwise-successful install;
24
+ * 5. only then patch `server.tool` / `server.registerTool` with the gate;
25
+ * 6. return the gating registrar.
26
+ *
27
+ * Registering the showcase BEFORE `guardDelegation` is impossible — it throws on any pre-existing
28
+ * tool (`registrar.ts`'s precondition), taking the publisher's server down at startup. Registering
29
+ * it AFTER means the gate covers it, and a revoked delegation kills `proof_check_this_server` —
30
+ * the one tool whose job is to report that revocation. Hence: inside, between capture and patch.
31
+ *
32
+ * The un-gated registrar captured at step 2 is never returned and never exported WHEN A TOKEN IS
33
+ * CONFIGURED. A publisher who got hold of it could route their own tool around the gate, silently
34
+ * nulling the self-enforcement while the installation still looked correct from the outside (SC-2).
35
+ * The one exception is the no-token path below, which returns exactly that registrar — correctly,
36
+ * since with no token nothing is gated at all and it is identical to `guardDelegation`'s opt-out.
37
+ * Stated rather than left as an absolute, because an unqualified claim beside a guard is the kind
38
+ * of sentence that stops being read once it is false.
39
+ *
40
+ * With no `opts.token` the showcase is still installed but NOTHING is gated — the same opt-out
41
+ * `guardDelegation` offers, and `proof_check_this_server` then reports `configured: false` rather
42
+ * than dressing the absence up as a positive verdict.
43
+ */
44
+ /**
45
+ * Appends the Proof recognition paragraph to `server.server._instructions` (SC-1..6), the SDK's
46
+ * private field the `initialize` handler reads on every connection. Never throws: called between
47
+ * `markInstalled` (already committed) and the no-token early return, a throw here would abort an
48
+ * otherwise-successful install after the gate is already live.
49
+ *
50
+ * Two failure classes, one degradation: `server.server` may simply not exist (an unrecognized SDK
51
+ * shape, or a test double) — handled by the early return; or reading/building/writing
52
+ * `_instructions` may throw (a frozen object, a future getter-only field) — handled by the
53
+ * try/catch. Both leave the server exactly as capable as it was before this call, just without the
54
+ * paragraph.
55
+ *
56
+ * Truthy check, not `typeof === 'string'` (SC-1): the SDK itself treats an empty string as absent
57
+ * (`server/index.js`'s `...(this._instructions && {instructions: this._instructions})`), so
58
+ * appending after one would silently double as "replace nothing with the paragraph" — which is the
59
+ * outcome the truthy check already produces on its own, without a special case.
60
+ */
61
+ function appendShowcaseInstructions(server, principal) {
62
+ const inner = server.server;
63
+ if (inner === undefined)
64
+ return;
65
+ try {
66
+ const existing = inner._instructions;
67
+ const paragraph = showcaseInstructionsParagraph(principal);
68
+ inner._instructions = existing ? `${existing}\n\n${paragraph}` : paragraph;
69
+ }
70
+ catch {
71
+ // Read or write threw — degrade silently, the gate above is already live.
72
+ }
73
+ }
74
+ export function installProofLayer(server, opts) {
75
+ assertNotAlreadyInstalled(server, 'installProofLayer');
76
+ assertInstallableBeforeRegistration(server, 'installProofLayer');
77
+ const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
78
+ const resolved = opts.token
79
+ ? resolveOptions({ ...opts, token: opts.token, fetchImpl: createLayerSurfaceFetch(SHOWCASE_VERSION) })
80
+ : null;
81
+ const originalTool = server.tool.bind(server);
82
+ const originalRegisterTool = typeof server.registerTool === 'function' ? server.registerTool.bind(server) : undefined;
83
+ try {
84
+ registerShowcase(originalTool, {
85
+ resolved,
86
+ principal: opts.principal,
87
+ baseUrl,
88
+ verifyBreaker: createBreaker(),
89
+ connectBreaker: createBreaker(),
90
+ markedFetch: createMarkedFetch(SHOWCASE_VERSION),
91
+ });
92
+ }
93
+ catch (error) {
94
+ // ALL-OR-NOTHING. A throw on the second or third registration would otherwise leave one or two
95
+ // Proof-named tools on a server whose `tool`/`registerTool` were never patched — so a
96
+ // publisher who defensively wraps this call in try/catch would run with ZERO self-refusal while
97
+ // advertising tools that imply otherwise. The trigger is not hypothetical: it is the duplicate
98
+ // `zod` instance SC-17 exists to prevent, failing inside the SDK's schema conversion. Rolling
99
+ // back also keeps a retry possible — otherwise the precondition check would see OUR tools and
100
+ // answer with the message written for a publisher who registered first.
101
+ for (const name of SHOWCASE_TOOL_NAMES) {
102
+ delete server._registeredTools[name];
103
+ }
104
+ throw error;
105
+ }
106
+ // The kind must match what actually happened: with no token this call installs NO gate and
107
+ // returns the RAW registrar below — which is the definition of `optout`, not `gated`. Recording
108
+ // it as gated made every later refusal on such a server assert a layer that is not there: the
109
+ // very defect the kind split was introduced to remove, surviving on the other entry point.
110
+ // The POSITION stays above the no-token return — a round-3 finding rests on it.
111
+ markInstalled(server, resolved ? 'gated' : 'optout');
112
+ appendShowcaseInstructions(server, opts.principal);
113
+ if (!resolved) {
114
+ return originalTool;
115
+ }
116
+ const wrappedTool = wrapRegistrar(originalTool, resolved);
117
+ server.tool = wrappedTool;
118
+ if (originalRegisterTool) {
119
+ server.registerTool = wrapRegistrar(originalRegisterTool, resolved);
120
+ }
121
+ return wrappedTool;
122
+ }
123
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
package/dist/poll.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { PollResult } from './types.js';
2
+ export declare const DEFAULT_BASE_URL = "https://api.proof.holdings";
3
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
4
+ /**
5
+ * Polls `POST {baseUrl}/api/v1/proofs/validate` with the delegation token and maps the response
6
+ * to a PollResult. Never sends `identifier` — a delegation has none to bind against
7
+ * (src/controllers/proofs.ts validateDelegationBranch rejects that combination outright).
8
+ */
9
+ export declare function pollDelegationStatus(token: string, baseUrl?: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<PollResult>;
10
+ //# sourceMappingURL=poll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,gBAAgB,+BAA+B,CAAC;AAC7D,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAUzC;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,MAAyB,EAClC,SAAS,GAAE,OAAO,KAAa,EAC/B,SAAS,GAAE,MAA2B,GACrC,OAAO,CAAC,UAAU,CAAC,CAkDrB"}
package/dist/poll.js ADDED
@@ -0,0 +1,60 @@
1
+ export const DEFAULT_BASE_URL = 'https://api.proof.holdings';
2
+ export const DEFAULT_TIMEOUT_MS = 10_000;
3
+ /**
4
+ * A `valid: false` reason the issuer can hand back that is NOT a definitive answer — it means
5
+ * the issuer's own registry lookup failed, not that the delegation was checked and rejected.
6
+ * Mirrors `packages/delegation-verifier/src/status.ts`, which fails the same way closed for the
7
+ * identical reason on the read-only verification side.
8
+ */
9
+ const UNRESOLVED_REASON = 'status_unavailable';
10
+ /**
11
+ * Polls `POST {baseUrl}/api/v1/proofs/validate` with the delegation token and maps the response
12
+ * to a PollResult. Never sends `identifier` — a delegation has none to bind against
13
+ * (src/controllers/proofs.ts validateDelegationBranch rejects that combination outright).
14
+ */
15
+ export async function pollDelegationStatus(token, baseUrl = DEFAULT_BASE_URL, fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS) {
16
+ let response;
17
+ try {
18
+ response = await fetchImpl(`${baseUrl}/api/v1/proofs/validate`, {
19
+ method: 'POST',
20
+ headers: { 'content-type': 'application/json' },
21
+ body: JSON.stringify({ proof_token: token }),
22
+ signal: AbortSignal.timeout(timeoutMs),
23
+ });
24
+ }
25
+ catch {
26
+ return { kind: 'unresolved' };
27
+ }
28
+ if (!response.ok) {
29
+ return { kind: 'unresolved' };
30
+ }
31
+ let body;
32
+ try {
33
+ body = await response.json();
34
+ }
35
+ catch {
36
+ return { kind: 'unresolved' };
37
+ }
38
+ if (typeof body !== 'object' || body === null) {
39
+ return { kind: 'unresolved' };
40
+ }
41
+ const parsed = body;
42
+ // Strict boolean check, not presence + truthy — mirrors
43
+ // packages/delegation-verifier/src/status.ts's `typeof body.valid !== 'boolean'` gate on the
44
+ // identical backend contract. A non-boolean `valid` is a response this package cannot trust.
45
+ if (typeof parsed.valid !== 'boolean') {
46
+ return { kind: 'unresolved' };
47
+ }
48
+ if (parsed.valid) {
49
+ return { kind: 'valid' };
50
+ }
51
+ if (parsed.reason === UNRESOLVED_REASON) {
52
+ return { kind: 'unresolved' };
53
+ }
54
+ return {
55
+ kind: 'refused',
56
+ reason: typeof parsed.reason === 'string' ? parsed.reason : 'invalid',
57
+ message: typeof parsed.message === 'string' ? parsed.message : 'This delegation is not valid',
58
+ };
59
+ }
60
+ //# sourceMappingURL=poll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poll.js","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAC7D,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,iBAAiB,GAAG,oBAAoB,CAAC;AAE/C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAa,EACb,UAAkB,gBAAgB,EAClC,YAA0B,KAAK,EAC/B,YAAoB,kBAAkB;IAEtC,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,OAAO,yBAAyB,EAAE;YAC9D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;YAC5C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAC9C,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAChC,CAAC;IAED,MAAM,MAAM,GAAG,IAA+D,CAAC;IAE/E,wDAAwD;IACxD,6FAA6F;IAC7F,6FAA6F;IAC7F,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,iBAAiB,EAAE,CAAC;QACxC,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAChC,CAAC;IAED,OAAO;QACL,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;QACrE,OAAO,EAAE,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,8BAA8B;KAC9F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * What a refused tool call says.
3
+ *
4
+ * This gate is SILENT on success and speaks only when it refuses. Under `guardDelegation` alone,
5
+ * that makes this string the first — often the only — thing a cold reader ever sees from Proof.
6
+ * Under `installProofLayer` it is not: the showcase's own tool descriptions already name Proof and
7
+ * the same revocation risk before any call is ever refused. Either way this string names Proof,
8
+ * names the publisher, gives one short link, and gives ONE action that can actually resolve the
9
+ * state it describes.
10
+ *
11
+ * The reason→phrase MAP is the load-bearing part, not decoration. `RefusedVerdict.reason` carries
12
+ * two categorically different classes of fact and one template cannot tell them apart:
13
+ *
14
+ * - the issuer ANSWERED and said no (`poll.ts` maps the `/proofs/validate` body straight
15
+ * through) — a decision, which retrying cannot change and which the publisher can explain;
16
+ * - `verdict.ts` gave up asking (`unresolved_at_startup`, `grace_exhausted`) — WE COULD NOT ASK,
17
+ * which is a connectivity failure wherever this server runs.
18
+ *
19
+ * Rendered identically, a publisher with blocked egress reads a message that looks exactly like a
20
+ * real revocation, and the one action offered ("contact the publisher") sends them to ask about a
21
+ * decision nobody made while the actual fault goes unmentioned.
22
+ */
23
+ export declare const REFUSAL_DETAILS_URL = "proof.holdings/delegation";
24
+ /**
25
+ * Where the definitive-refusal wording comes from — the issuer answered and the answer was no.
26
+ *
27
+ * The last two are DEFENSIVE rather than reachable: `poll.ts` never sends an `identifier` (a
28
+ * delegation has none to bind against), and the issuer emits `identifier_unverifiable` only when
29
+ * one was supplied, `identifier_mismatch` only from the proof branch. They are carried because the
30
+ * cost of a phrase is nothing and the cost of an unmapped reason is the fallback below, which is
31
+ * deliberately vaguer than a mapped one.
32
+ */
33
+ export declare const PUBLISHER_DECISION_REASONS: readonly ["revoked", "suspended", "expired", "unknown_delegation", "invalid", "identifier_unverifiable", "identifier_mismatch"];
34
+ /**
35
+ * Produced by `verdict.ts`, never by the issuer. Kept as its own list rather than as a `default`
36
+ * arm, because the branch is defined by what it must NOT say.
37
+ */
38
+ export declare const UNREACHABLE_REASONS: readonly ["unresolved_at_startup", "grace_exhausted"];
39
+ export declare function isUnreachableRefusal(reason: string): boolean;
40
+ export declare const UNREACHABLE_LEADS: Record<string, (principal: string) => string>;
41
+ /**
42
+ * The issuer may introduce a reason this version has never heard of — `poll.ts` passes
43
+ * `parsed.reason` through verbatim rather than validating it against a list, carving out exactly
44
+ * ONE string (`status_unavailable`) as non-definitive. Everything else arrives here as a refusal.
45
+ *
46
+ * So the call is refused — an unrecognized verdict fails CLOSED everywhere else in this package —
47
+ * but the SENTENCE claims less than a mapped one does. It deliberately omits "retrying will not fix
48
+ * this": that phrase is a statement about a state this version cannot classify, and if the issuer's
49
+ * new reason turns out to be transient, it is simply false. It names no act, and carries the reason
50
+ * through so an operator can look it up — TRUNCATED past `MAX_ISSUER_REASON`, comfortably above
51
+ * every code the issuer emits (the longest, `identifier_unverifiable`, is 23). ("raw" and "verbatim" stood here and in the README for one round after the bound
52
+ * landed, and the note written to record that claimed BOTH were fixed while the README still
53
+ * said verbatim — the same drift one file over, hidden by its own correction. Fixed the round
54
+ * after that, by a reviewer who read the document instead of the note about it.)
55
+ */
56
+ /**
57
+ * Bounds on issuer-controlled text that reaches an agent's context, sized BY WHAT EACH BOUNDS.
58
+ *
59
+ * `poll.ts` accepts any string of any length for both `reason` and `message`, and whatever it
60
+ * accepts lands in the caller's context. `showcase/connect.ts` already caps and refuses redirects
61
+ * on its remote read for exactly the "this becomes instructions" reason; these paths had none.
62
+ *
63
+ * Two constants rather than one, and the split is not tidiness — one shared number was measured
64
+ * doing real damage. `reason` is a CODE (longest the issuer emits: `identifier_unverifiable`, 23),
65
+ * `message` is free-form PROSE, and `verdict.ts` writes its own 67-character sentence for
66
+ * `grace_exhausted`. A single code-sized cap therefore truncated the one string in the whole
67
+ * reachable set that is OURS, in the unreachable branch SC-6 exists to make legible. The package
68
+ * already separates budgets this way (`SHOWCASE_TIMEOUT_MS` vs `SHOWCASE_PER_REQUEST_TIMEOUT_MS`).
69
+ *
70
+ * Both numbers are stated against the reachable set rather than chosen. Longest reason the issuer
71
+ * emits: `identifier_unverifiable`, 23. Longest MESSAGE that can reach this package: 52
72
+ * (`Invalid delegation token: unsupported schema version`, from the token-validation catch arm in
73
+ * `controllers/proofs.ts` — an earlier version of this comment said 42, having enumerated only the
74
+ * registry branches and missed the arm that emits `invalid`/`expired`). Ours: `grace_exhausted` at
75
+ * 67, growing one character per 10× failure count. So the prose bound sits ~3× above anything
76
+ * reachable.
77
+ *
78
+ * Pinned on both sides, but not where this comment first claimed: below ~68 the grace case reddens;
79
+ * ABOVE, the ceiling is the whole-payload assertion (~450), not the per-field one — that field is a
80
+ * function of the reason bound, not this one. Measured after a reviewer mutated the constant.
81
+ */
82
+ export declare const MAX_ISSUER_REASON = 64;
83
+ export declare const MAX_ISSUER_MESSAGE = 200;
84
+ /**
85
+ * Truncates by CODE POINT, not code unit: `slice` at a fixed index can cut a surrogate pair in half
86
+ * and leave a lone surrogate in the output. Unreachable against our own issuer (its codes are ASCII)
87
+ * — but this bound exists for the case of a hostile one, which is precisely the caller that would
88
+ * send such a string deliberately.
89
+ */
90
+ export declare function boundIssuerText(value: string, limit: number): string;
91
+ export declare function refusalMessage(principal: string, reason: string): string;
92
+ //# sourceMappingURL=refusal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAE/D;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,iIAQ7B,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,mBAAmB,uDAAwD,CAAC;AAEzF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE5D;AAmCD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAS3E,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpE;AAiCD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA4BxE"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * What a refused tool call says.
3
+ *
4
+ * This gate is SILENT on success and speaks only when it refuses. Under `guardDelegation` alone,
5
+ * that makes this string the first — often the only — thing a cold reader ever sees from Proof.
6
+ * Under `installProofLayer` it is not: the showcase's own tool descriptions already name Proof and
7
+ * the same revocation risk before any call is ever refused. Either way this string names Proof,
8
+ * names the publisher, gives one short link, and gives ONE action that can actually resolve the
9
+ * state it describes.
10
+ *
11
+ * The reason→phrase MAP is the load-bearing part, not decoration. `RefusedVerdict.reason` carries
12
+ * two categorically different classes of fact and one template cannot tell them apart:
13
+ *
14
+ * - the issuer ANSWERED and said no (`poll.ts` maps the `/proofs/validate` body straight
15
+ * through) — a decision, which retrying cannot change and which the publisher can explain;
16
+ * - `verdict.ts` gave up asking (`unresolved_at_startup`, `grace_exhausted`) — WE COULD NOT ASK,
17
+ * which is a connectivity failure wherever this server runs.
18
+ *
19
+ * Rendered identically, a publisher with blocked egress reads a message that looks exactly like a
20
+ * real revocation, and the one action offered ("contact the publisher") sends them to ask about a
21
+ * decision nobody made while the actual fault goes unmentioned.
22
+ */
23
+ export const REFUSAL_DETAILS_URL = 'proof.holdings/delegation';
24
+ /**
25
+ * Where the definitive-refusal wording comes from — the issuer answered and the answer was no.
26
+ *
27
+ * The last two are DEFENSIVE rather than reachable: `poll.ts` never sends an `identifier` (a
28
+ * delegation has none to bind against), and the issuer emits `identifier_unverifiable` only when
29
+ * one was supplied, `identifier_mismatch` only from the proof branch. They are carried because the
30
+ * cost of a phrase is nothing and the cost of an unmapped reason is the fallback below, which is
31
+ * deliberately vaguer than a mapped one.
32
+ */
33
+ export const PUBLISHER_DECISION_REASONS = [
34
+ 'revoked',
35
+ 'suspended',
36
+ 'expired',
37
+ 'unknown_delegation',
38
+ 'invalid',
39
+ 'identifier_unverifiable',
40
+ 'identifier_mismatch',
41
+ ];
42
+ /**
43
+ * Produced by `verdict.ts`, never by the issuer. Kept as its own list rather than as a `default`
44
+ * arm, because the branch is defined by what it must NOT say.
45
+ */
46
+ export const UNREACHABLE_REASONS = ['unresolved_at_startup', 'grace_exhausted'];
47
+ export function isUnreachableRefusal(reason) {
48
+ return UNREACHABLE_REASONS.includes(reason);
49
+ }
50
+ /**
51
+ * The lead sentence per reason. Each states WHAT happened in words a non-specialist reads
52
+ * correctly on the first pass, and each definitive one carries "retrying will not fix this" —
53
+ * the single instruction an agent needs in order to stop.
54
+ */
55
+ const PUBLISHER_DECISION_LEADS = {
56
+ revoked: (principal) => `${principal}'s authorization to run this was revoked, and Proof (proof.holdings) cannot ` +
57
+ 'confirm it as valid — retrying will not fix this.',
58
+ suspended: (principal) => `${principal}'s authorization to run this is paused, and Proof (proof.holdings) cannot ` +
59
+ 'confirm it as valid while the pause holds — retrying will not fix this.',
60
+ expired: (principal) => `${principal}'s authorization to run this has expired, and Proof (proof.holdings) cannot ` +
61
+ 'confirm it as valid — retrying will not fix this.',
62
+ unknown_delegation: (principal) => `Proof (proof.holdings) has no record of an authorization for ${principal} to run this, so ` +
63
+ 'it cannot be confirmed as valid — retrying will not fix this.',
64
+ // Deliberately NOT "rejected … as invalid". `poll.ts` synthesises `reason: 'invalid'` when the
65
+ // issuer answers `valid: false` with no reason string at all, so an attributed act here would be
66
+ // manufactured from an absent field — the same over-claim the fallback below exists to avoid, one
67
+ // arm over. "does not consider valid" is true whether the issuer named the reason or not.
68
+ invalid: (principal) => `Proof (proof.holdings) does not consider ${principal}'s authorization to run this valid — ` +
69
+ 'retrying will not fix this.',
70
+ identifier_unverifiable: (principal) => `Proof (proof.holdings) could not check ${principal}'s authorization to run this against the ` +
71
+ 'identifier it was asked to bind, so it cannot confirm it — retrying will not fix this.',
72
+ identifier_mismatch: (principal) => `${principal}'s authorization to run this names a different subject than the one it was ` +
73
+ 'checked against, so Proof (proof.holdings) cannot confirm it — retrying will not fix this.',
74
+ };
75
+ export const UNREACHABLE_LEADS = {
76
+ unresolved_at_startup: (principal) => `Proof (proof.holdings) could not be reached to confirm ${principal}'s authorization to run ` +
77
+ `this, so this call is refused rather than assumed — a connectivity failure here, not a ` +
78
+ `decision by ${principal}.`,
79
+ grace_exhausted: (principal) => 'Proof (proof.holdings) has been unreachable for several consecutive checks, so ' +
80
+ `${principal}'s authorization to run this can no longer be confirmed — a connectivity ` +
81
+ `failure here, not a decision by ${principal}.`,
82
+ };
83
+ /**
84
+ * The issuer may introduce a reason this version has never heard of — `poll.ts` passes
85
+ * `parsed.reason` through verbatim rather than validating it against a list, carving out exactly
86
+ * ONE string (`status_unavailable`) as non-definitive. Everything else arrives here as a refusal.
87
+ *
88
+ * So the call is refused — an unrecognized verdict fails CLOSED everywhere else in this package —
89
+ * but the SENTENCE claims less than a mapped one does. It deliberately omits "retrying will not fix
90
+ * this": that phrase is a statement about a state this version cannot classify, and if the issuer's
91
+ * new reason turns out to be transient, it is simply false. It names no act, and carries the reason
92
+ * through so an operator can look it up — TRUNCATED past `MAX_ISSUER_REASON`, comfortably above
93
+ * every code the issuer emits (the longest, `identifier_unverifiable`, is 23). ("raw" and "verbatim" stood here and in the README for one round after the bound
94
+ * landed, and the note written to record that claimed BOTH were fixed while the README still
95
+ * said verbatim — the same drift one file over, hidden by its own correction. Fixed the round
96
+ * after that, by a reviewer who read the document instead of the note about it.)
97
+ */
98
+ /**
99
+ * Bounds on issuer-controlled text that reaches an agent's context, sized BY WHAT EACH BOUNDS.
100
+ *
101
+ * `poll.ts` accepts any string of any length for both `reason` and `message`, and whatever it
102
+ * accepts lands in the caller's context. `showcase/connect.ts` already caps and refuses redirects
103
+ * on its remote read for exactly the "this becomes instructions" reason; these paths had none.
104
+ *
105
+ * Two constants rather than one, and the split is not tidiness — one shared number was measured
106
+ * doing real damage. `reason` is a CODE (longest the issuer emits: `identifier_unverifiable`, 23),
107
+ * `message` is free-form PROSE, and `verdict.ts` writes its own 67-character sentence for
108
+ * `grace_exhausted`. A single code-sized cap therefore truncated the one string in the whole
109
+ * reachable set that is OURS, in the unreachable branch SC-6 exists to make legible. The package
110
+ * already separates budgets this way (`SHOWCASE_TIMEOUT_MS` vs `SHOWCASE_PER_REQUEST_TIMEOUT_MS`).
111
+ *
112
+ * Both numbers are stated against the reachable set rather than chosen. Longest reason the issuer
113
+ * emits: `identifier_unverifiable`, 23. Longest MESSAGE that can reach this package: 52
114
+ * (`Invalid delegation token: unsupported schema version`, from the token-validation catch arm in
115
+ * `controllers/proofs.ts` — an earlier version of this comment said 42, having enumerated only the
116
+ * registry branches and missed the arm that emits `invalid`/`expired`). Ours: `grace_exhausted` at
117
+ * 67, growing one character per 10× failure count. So the prose bound sits ~3× above anything
118
+ * reachable.
119
+ *
120
+ * Pinned on both sides, but not where this comment first claimed: below ~68 the grace case reddens;
121
+ * ABOVE, the ceiling is the whole-payload assertion (~450), not the per-field one — that field is a
122
+ * function of the reason bound, not this one. Measured after a reviewer mutated the constant.
123
+ */
124
+ export const MAX_ISSUER_REASON = 64;
125
+ export const MAX_ISSUER_MESSAGE = 200;
126
+ /**
127
+ * Truncates by CODE POINT, not code unit: `slice` at a fixed index can cut a surrogate pair in half
128
+ * and leave a lone surrogate in the output. Unreachable against our own issuer (its codes are ASCII)
129
+ * — but this bound exists for the case of a hostile one, which is precisely the caller that would
130
+ * send such a string deliberately.
131
+ */
132
+ export function boundIssuerText(value, limit) {
133
+ // `wrapRegistrar`'s dispatch is an ALLOW-list precisely so a shape the return type does not admit
134
+ // still refuses rather than runs — and that arm then calls through here. Spreading a non-string
135
+ // throws, which would turn the fail-closed path's own refusal into a thrown error inside a
136
+ // publisher's process: the defensive posture asserted in two places and implemented in one.
137
+ if (typeof value !== 'string')
138
+ return String(value);
139
+ const points = [...value];
140
+ return points.length > limit ? `${points.slice(0, limit).join('')}…` : value;
141
+ }
142
+ function fallbackLead(principal, reason) {
143
+ const echoed = boundIssuerText(reason, MAX_ISSUER_REASON);
144
+ return (`Proof (proof.holdings) cannot confirm ${principal}'s authorization to run this: it answered ` +
145
+ `with a reason this version of the check does not recognise (${echoed}).`);
146
+ }
147
+ /**
148
+ * Reads a lead WITHOUT inheriting one from `Object.prototype`.
149
+ *
150
+ * `reason` is network-derived and unvalidated — `poll.ts` passes the issuer's own string through
151
+ * for every value except `status_unavailable` — so a plain `map[reason]` answered EVERY
152
+ * `Object.prototype` member truthy. No count is written here: `refusal.test.ts` derives the set
153
+ * from `Object.getOwnPropertyNames(Object.prototype)` and covers all of it, so a number typed in
154
+ * this sentence could only ever disagree with the runtime. (It did: it read "eight".)
155
+ * `constructor` rendered the principal and nothing else, `toString` produced
156
+ * "[object Undefined] Details: …", and `valueOf` / `__proto__` threw a TypeError OUT of the gated
157
+ * handler, inside someone else's production process, from the one code path whose entire job is to
158
+ * say no gracefully. Measured against the built module, not reasoned about.
159
+ *
160
+ * `Object.hasOwn` is the same fix `web/src/app/(dashboard)/lookalike-watch/eligibility.ts` already
161
+ * carries for the same reason ("a code of `constructor` would otherwise resolve to a function").
162
+ */
163
+ function ownLead(map, reason) {
164
+ return Object.hasOwn(map, reason) ? map[reason] : undefined;
165
+ }
166
+ export function refusalMessage(principal, reason) {
167
+ if (isUnreachableRefusal(reason)) {
168
+ // Read through a local, exactly like the decision branch below. `noUncheckedIndexedAccess` is
169
+ // off in this package, so a reason added to UNREACHABLE_REASONS without a lead would compile
170
+ // and then throw a TypeError out of a gated handler inside someone else's production process.
171
+ // `refusal.test.ts` pins the pairing (added after a reviewer measured that it did not — a
172
+ // third reason with no lead ran green), and this local is the runtime half of the same rule:
173
+ // a behavioural pin is not a reason to let a gated handler throw in someone else's process.
174
+ // Consequence worth knowing: this inline sentence is UNREACHABLE today, so it is the one piece
175
+ // of refusal copy no test reads and no review round judged. On the day it fires it ships
176
+ // unreviewed.
177
+ const lead = ownLead(UNREACHABLE_LEADS, reason);
178
+ return (`${lead
179
+ ? lead(principal)
180
+ : `Proof (proof.holdings) could not be reached to confirm ${principal}'s authorization ` +
181
+ `to run this — a connectivity failure here, not a decision by ${principal}.`} Details: ${REFUSAL_DETAILS_URL}. ` +
182
+ 'Check outbound network access to proof.holdings from wherever this server runs.');
183
+ }
184
+ const lead = ownLead(PUBLISHER_DECISION_LEADS, reason);
185
+ return (`${lead ? lead(principal) : fallbackLead(principal, reason)} ` +
186
+ `Details: ${REFUSAL_DETAILS_URL}. If this is unexpected, contact ${principal}.`);
187
+ }
188
+ //# sourceMappingURL=refusal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal.js","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import { type ResolvedOptions } from './verdict.js';
2
+ export type ToolRegistrar = (...args: unknown[]) => unknown;
3
+ /**
4
+ * Records that this package has already run on a server, and WHICH WAY.
5
+ *
6
+ * A registry-wide `Symbol.for` so a duplicated copy of this package in one process still sees it.
7
+ * Both entry points set it and both check it, because the dangerous combination is silent: after
8
+ * `guardDelegation` runs, nothing is registered and `server.tool` is already the gating wrapper, so
9
+ * `installProofLayer`'s precondition passes and it registers the showcase THROUGH the gate — and a
10
+ * revoked delegation then kills `proof_check_this_server`, the exact failure this package's second
11
+ * entry point exists to prevent. Measured in code review; it manifests only at revocation time.
12
+ *
13
+ * The KIND matters because the two states are not the same fact. `gated` means a gate is installed
14
+ * and a registrar was handed out. `optout` means only the second half: `guardDelegation` with no
15
+ * token returned the server's RAW registrar and gated nothing. Recording them identically made a
16
+ * benign repeat of the opt-out crash the publisher's startup with a message asserting a layer that
17
+ * is not there — introduced by the fix for the real hole, caught in the next review round.
18
+ */
19
+ export type InstallKind = 'gated' | 'optout';
20
+ export declare const INSTALLED_MARKER: unique symbol;
21
+ export declare function readInstallKind(server: McpServerLike): InstallKind | undefined;
22
+ export declare function markInstalled(server: McpServerLike, kind: InstallKind): void;
23
+ /**
24
+ * Refuses a second call that cannot cover the registrar a first call already handed out.
25
+ *
26
+ * The two states fail for OPPOSITE reasons, which is why one summary cannot serve both. After
27
+ * `gated`, a second call would install over a working gate. After `optout`, the raw registrar stays
28
+ * perfectly VALID and simply bypasses whatever is installed next — nothing invalidated it. The
29
+ * message below is picked from the kind for that reason: claiming an installed layer where only a
30
+ * raw registrar was handed out sends the reader looking for something that does not exist.
31
+ */
32
+ export declare function assertNotAlreadyInstalled(server: McpServerLike, entryPoint: string): void;
33
+ /**
34
+ * The subset of `McpServer` this package touches, expressed structurally so no runtime import of
35
+ * `@modelcontextprotocol/sdk` is needed (it stays a dev-only dependency for typing tests).
36
+ * `_registeredTools` is undocumented SDK internal state — the only field that exposes "how many
37
+ * tools are registered" (mcp/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js:18-22).
38
+ * `registerTool` is optional because older SDK versions may not expose it, but when present it is
39
+ * a SECOND, independent public entry point that also populates `_registeredTools`
40
+ * (mcp.js:660,701) — both must be gated, or a publisher who calls it instead of `.tool()` gets a
41
+ * completely unguarded handler.
42
+ *
43
+ * `server` mirrors `McpServer.server` — a public field holding the underlying `Server` instance,
44
+ * whose private `_instructions` (`server/index.js:53`) is what `install.ts`'s showcase-recognition
45
+ * write targets. Optional for the same reason as `_registeredTools`: a stand-in in a test, or an
46
+ * SDK shape this package has never seen, may not carry it at all.
47
+ */
48
+ export interface McpServerLike {
49
+ tool: ToolRegistrar;
50
+ registerTool?: ToolRegistrar;
51
+ _registeredTools?: Record<string, unknown>;
52
+ server?: {
53
+ _instructions?: unknown;
54
+ };
55
+ }
56
+ /**
57
+ * The shared precondition both entry points check: the SDK must expose `_registeredTools`, and
58
+ * nothing may be registered yet. Placed late, this package does nothing — `.bind()` snapshots the
59
+ * function value — so refusing to run is the only honest answer to "installed too late".
60
+ */
61
+ export declare function assertInstallableBeforeRegistration(server: McpServerLike, entryPoint: string): void;
62
+ /**
63
+ * Wraps a registrar (`server.tool` or `server.registerTool` — both take the handler as their
64
+ * final positional argument) so every call through it is gated on `currentVerdict`.
65
+ *
66
+ * The dispatch check is a deliberate ALLOW-LIST (`kind === 'valid'` runs the handler; anything
67
+ * else, including a shape `currentVerdict`'s return type does not admit but that could reach here
68
+ * through some future bug, refuses) rather than a deny-list keyed on `'refused'` — a security gate
69
+ * must default to denying what it does not positively recognize as authorized.
70
+ *
71
+ * The handler's own arguments are passed through OPAQUELY: never read, indexed, logged or
72
+ * serialized. This package runs inside someone else's production process, where those arguments
73
+ * are that publisher's user data — `src/__tests__/drift/mcp-showcase.test.ts` pins the property
74
+ * behaviourally rather than trusting this comment.
75
+ */
76
+ export declare function wrapRegistrar(original: ToolRegistrar, opts: ResolvedOptions): ToolRegistrar;
77
+ //# sourceMappingURL=registrar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registrar.d.ts","sourceRoot":"","sources":["../src/registrar.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;AAE5D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE7C,eAAO,MAAM,gBAAgB,eAAiE,CAAC;AAE/F,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,GAAG,SAAS,CAgB9E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAE5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuBzF;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,aAAa,CAAC;IACpB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAoBnG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,eAAe,GAAG,aAAa,CAiB3F"}