@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.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/cache.d.ts +10 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +58 -0
- package/dist/cache.js.map +1 -0
- package/dist/guard.d.ts +19 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +57 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +8 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +123 -0
- package/dist/install.js.map +1 -0
- package/dist/poll.d.ts +10 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.js +60 -0
- package/dist/poll.js.map +1 -0
- package/dist/refusal.d.ts +92 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +188 -0
- package/dist/refusal.js.map +1 -0
- package/dist/registrar.d.ts +77 -0
- package/dist/registrar.d.ts.map +1 -0
- package/dist/registrar.js +106 -0
- package/dist/registrar.js.map +1 -0
- package/dist/result.d.ts +16 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +12 -0
- package/dist/result.js.map +1 -0
- package/dist/schedule.d.ts +14 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +20 -0
- package/dist/schedule.js.map +1 -0
- package/dist/showcase/breaker.d.ts +91 -0
- package/dist/showcase/breaker.d.ts.map +1 -0
- package/dist/showcase/breaker.js +132 -0
- package/dist/showcase/breaker.js.map +1 -0
- package/dist/showcase/connect.d.ts +39 -0
- package/dist/showcase/connect.d.ts.map +1 -0
- package/dist/showcase/connect.js +98 -0
- package/dist/showcase/connect.js.map +1 -0
- package/dist/showcase/descriptions.d.ts +46 -0
- package/dist/showcase/descriptions.d.ts.map +1 -0
- package/dist/showcase/descriptions.js +86 -0
- package/dist/showcase/descriptions.js.map +1 -0
- package/dist/showcase/instructions.d.ts +13 -0
- package/dist/showcase/instructions.d.ts.map +1 -0
- package/dist/showcase/instructions.js +20 -0
- package/dist/showcase/instructions.js.map +1 -0
- package/dist/showcase/marked-fetch.d.ts +37 -0
- package/dist/showcase/marked-fetch.d.ts.map +1 -0
- package/dist/showcase/marked-fetch.js +48 -0
- package/dist/showcase/marked-fetch.js.map +1 -0
- package/dist/showcase/tools.d.ts +49 -0
- package/dist/showcase/tools.d.ts.map +1 -0
- package/dist/showcase/tools.js +198 -0
- package/dist/showcase/tools.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +47 -0
- package/dist/verdict.d.ts.map +1 -0
- package/dist/verdict.js +89 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +69 -0
package/dist/install.js
ADDED
|
@@ -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
|
package/dist/poll.js.map
ADDED
|
@@ -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"}
|
package/dist/refusal.js
ADDED
|
@@ -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"}
|