@axtary/mcp 0.0.1 → 0.2.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.
@@ -0,0 +1,218 @@
1
+ import { type CryptoKey, type JWK, type KeyObject } from "jose";
2
+ import { z } from "zod";
3
+ import type { ToolHandler } from "@axtary/proxy";
4
+ import { type McpToolDefinitionInput, type McpToolDefinitionRecord, type McpToolInvoker } from "./core.js";
5
+ /**
6
+ * A signed MCP tool definition gives a tool a *publisher identity* and an
7
+ * immutable, versioned lineage on top of the content hash that
8
+ * `@axtary/mcp` already computes. This is the Axtary profile of the ETDI idea
9
+ * (arXiv:2506.01333): the publisher signs the exact `definitionHash`, a
10
+ * semantic version, the required scopes, and a pointer to the prior version's
11
+ * hash. A client rejects any definition that is unsigned, signed by an
12
+ * unknown key, mis-signed, or whose on-the-wire bytes no longer hash to the
13
+ * signed `definitionHash` — and it rejects it *before* the tool is invoked.
14
+ *
15
+ * We stay on the JOSE/ES256 profile already used by the ActionPass issuer
16
+ * keyring; the signature is a compact JWT (`statement+jwt`).
17
+ */
18
+ export declare const MCP_SIGNED_DEFINITION_PROFILE = "axtary.mcp_signed_definition.v1";
19
+ export declare const MCP_SIGNED_DEFINITION_TYP = "axtary-mcp-tool-definition+jwt";
20
+ export declare const MCP_PUBLISHER_KEYRING_VERSION = "axtary.mcp_publisher_keyring.v0";
21
+ export declare const MCP_PUBLISHER_TRUST_VERSION = "axtary.mcp_publisher_trust.v0";
22
+ export declare const MCP_SIGNED_DEFINITION_REGISTRY_VERSION = "axtary.mcp_signed_definition_registry.v0";
23
+ /**
24
+ * The claims the publisher signs. `definitionHash` binds the signature to the
25
+ * exact tool definition; `priorDefinitionHash` links this version to the one
26
+ * it supersedes so a rug-pull (silent re-definition) is visible as a chain.
27
+ */
28
+ export declare const McpSignedDefinitionClaimsSchema: z.ZodObject<{
29
+ iss: z.ZodString;
30
+ sub: z.ZodString;
31
+ iat: z.ZodNumber;
32
+ axtary_mcp_profile: z.ZodLiteral<"axtary.mcp_signed_definition.v1">;
33
+ serverIdentity: z.ZodString;
34
+ toolName: z.ZodString;
35
+ schemaVersion: z.ZodString;
36
+ definitionHash: z.ZodString;
37
+ semver: z.ZodString;
38
+ requiredScopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
39
+ priorDefinitionHash: z.ZodDefault<z.ZodNullable<z.ZodString>>;
40
+ }, z.core.$strip>;
41
+ export type McpSignedDefinitionClaims = z.infer<typeof McpSignedDefinitionClaimsSchema>;
42
+ /** A public publisher key, JWKS-shaped, resolvable by `kid`. */
43
+ export declare const McpPublisherKeySchema: z.ZodObject<{
44
+ kid: z.ZodString;
45
+ kty: z.ZodString;
46
+ crv: z.ZodString;
47
+ x: z.ZodString;
48
+ y: z.ZodString;
49
+ alg: z.ZodDefault<z.ZodLiteral<"ES256">>;
50
+ use: z.ZodDefault<z.ZodLiteral<"sig">>;
51
+ }, z.core.$strip>;
52
+ export type McpPublisherKey = z.infer<typeof McpPublisherKeySchema>;
53
+ export declare const McpPublisherKeyringSchema: z.ZodObject<{
54
+ schemaVersion: z.ZodLiteral<"axtary.mcp_publisher_keyring.v0">;
55
+ publisher: z.ZodString;
56
+ activeKid: z.ZodString;
57
+ keys: z.ZodArray<z.ZodObject<{
58
+ kid: z.ZodString;
59
+ kty: z.ZodString;
60
+ crv: z.ZodString;
61
+ x: z.ZodString;
62
+ y: z.ZodString;
63
+ alg: z.ZodDefault<z.ZodLiteral<"ES256">>;
64
+ use: z.ZodDefault<z.ZodLiteral<"sig">>;
65
+ privateKeyPem: z.ZodString;
66
+ createdAt: z.ZodString;
67
+ }, z.core.$strip>>;
68
+ }, z.core.$strip>;
69
+ export type McpPublisherKeyring = z.infer<typeof McpPublisherKeyringSchema>;
70
+ /**
71
+ * The verifier-side trust anchor: a configured map of publisher identity to
72
+ * the public keys we will accept signatures from. A signature whose `kid` is
73
+ * not in this store is rejected — there is no trust-on-first-use for
74
+ * *signatures* (that would defeat publisher identity); TOFU stays at the pin
75
+ * layer, which is a deliberate, separate, human-reviewed decision.
76
+ */
77
+ export declare const McpPublisherTrustEntrySchema: z.ZodObject<{
78
+ publisher: z.ZodString;
79
+ keys: z.ZodArray<z.ZodObject<{
80
+ kid: z.ZodString;
81
+ kty: z.ZodString;
82
+ crv: z.ZodString;
83
+ x: z.ZodString;
84
+ y: z.ZodString;
85
+ alg: z.ZodDefault<z.ZodLiteral<"ES256">>;
86
+ use: z.ZodDefault<z.ZodLiteral<"sig">>;
87
+ }, z.core.$strip>>;
88
+ }, z.core.$strip>;
89
+ export type McpPublisherTrustEntry = z.infer<typeof McpPublisherTrustEntrySchema>;
90
+ export declare const McpPublisherTrustStoreSchema: z.ZodObject<{
91
+ schemaVersion: z.ZodLiteral<"axtary.mcp_publisher_trust.v0">;
92
+ publishers: z.ZodDefault<z.ZodArray<z.ZodObject<{
93
+ publisher: z.ZodString;
94
+ keys: z.ZodArray<z.ZodObject<{
95
+ kid: z.ZodString;
96
+ kty: z.ZodString;
97
+ crv: z.ZodString;
98
+ x: z.ZodString;
99
+ y: z.ZodString;
100
+ alg: z.ZodDefault<z.ZodLiteral<"ES256">>;
101
+ use: z.ZodDefault<z.ZodLiteral<"sig">>;
102
+ }, z.core.$strip>>;
103
+ }, z.core.$strip>>>;
104
+ }, z.core.$strip>;
105
+ export type McpPublisherTrustStore = z.infer<typeof McpPublisherTrustStoreSchema>;
106
+ export declare const McpSignedDefinitionRegistryEntrySchema: z.ZodObject<{
107
+ serverIdentity: z.ZodString;
108
+ toolName: z.ZodString;
109
+ publisher: z.ZodString;
110
+ statement: z.ZodString;
111
+ }, z.core.$strip>;
112
+ export type McpSignedDefinitionRegistryEntry = z.infer<typeof McpSignedDefinitionRegistryEntrySchema>;
113
+ export declare const McpSignedDefinitionRegistrySchema: z.ZodObject<{
114
+ schemaVersion: z.ZodLiteral<"axtary.mcp_signed_definition_registry.v0">;
115
+ statements: z.ZodDefault<z.ZodArray<z.ZodObject<{
116
+ serverIdentity: z.ZodString;
117
+ toolName: z.ZodString;
118
+ publisher: z.ZodString;
119
+ statement: z.ZodString;
120
+ }, z.core.$strip>>>;
121
+ }, z.core.$strip>;
122
+ export type McpSignedDefinitionRegistry = z.infer<typeof McpSignedDefinitionRegistrySchema>;
123
+ export type McpPublisherKeyResolver = (kid: string | undefined) => Promise<CryptoKey | KeyObject | Uint8Array | undefined>;
124
+ export type McpSignedDefinitionVerification = {
125
+ claims: McpSignedDefinitionClaims;
126
+ publisher: string;
127
+ kid: string;
128
+ semver: string;
129
+ requiredScopes: string[];
130
+ definitionHash: string;
131
+ priorDefinitionHash: string | null;
132
+ };
133
+ export type SignMcpToolDefinitionInput = {
134
+ definition: McpToolDefinitionInput | McpToolDefinitionRecord;
135
+ publisher: string;
136
+ semver: string;
137
+ signingKey: KeyObject | CryptoKey | JWK | Uint8Array;
138
+ keyId: string;
139
+ requiredScopes?: string[];
140
+ priorDefinitionHash?: string | null;
141
+ now?: Date;
142
+ };
143
+ /**
144
+ * Publisher-side: sign a tool definition. The signature commits to the exact
145
+ * `definitionHash`, so a later byte-level change to the definition cannot be
146
+ * presented under this signature.
147
+ */
148
+ export declare function signMcpToolDefinition(input: SignMcpToolDefinitionInput): Promise<{
149
+ statement: string;
150
+ claims: McpSignedDefinitionClaims;
151
+ }>;
152
+ /**
153
+ * Verifier-side: verify a signed definition against a known publisher key and
154
+ * the *actual* tool definition presented now. Fails closed on every distinct
155
+ * failure with a stable reason. The `definitionHash` recomputed from the live
156
+ * definition must equal the signed hash — that is the altered-definition /
157
+ * tool-poisoning rejection.
158
+ */
159
+ export declare function verifyMcpSignedDefinition(input: {
160
+ statement: string;
161
+ definition: McpToolDefinitionInput | McpToolDefinitionRecord;
162
+ verificationKeys: McpPublisherKeyResolver;
163
+ expectedPublisher?: string;
164
+ }): Promise<McpSignedDefinitionVerification>;
165
+ /**
166
+ * Wrap a tool invoker so the publisher signature is verified against the live
167
+ * definition *before* the tool runs. Any failure — unsigned (no statement
168
+ * configured is simply not this handler), mis-signed, unknown key, or an
169
+ * altered definition whose bytes no longer hash to the signed value — throws
170
+ * and the tool is never invoked. When the action carries bound provenance, it
171
+ * must also agree with the verified signature.
172
+ */
173
+ export declare function createSignedMcpToolHandler(input: {
174
+ definition: McpToolDefinitionInput | McpToolDefinitionRecord;
175
+ invoke: McpToolInvoker;
176
+ statement: string;
177
+ verificationKeys: McpPublisherKeyResolver;
178
+ expectedPublisher?: string;
179
+ }): ToolHandler;
180
+ /**
181
+ * Build a key resolver from a configured trust store. A `kid` that is not
182
+ * present (optionally scoped to `expectedPublisher`) resolves to `undefined`,
183
+ * which makes verification fail closed.
184
+ */
185
+ export declare function mcpPublisherKeyResolver(store: McpPublisherTrustStore, options?: {
186
+ publisher?: string;
187
+ }): McpPublisherKeyResolver;
188
+ export declare function emptyMcpPublisherTrustStore(): McpPublisherTrustStore;
189
+ export declare function parseMcpPublisherTrustStore(input: unknown): McpPublisherTrustStore;
190
+ export declare function loadMcpPublisherTrustStore(path: string): Promise<McpPublisherTrustStore>;
191
+ export declare function saveMcpPublisherTrustStore(path: string, store: McpPublisherTrustStore): Promise<void>;
192
+ export declare function loadMcpSignedDefinitionRegistry(path: string): Promise<McpSignedDefinitionRegistry>;
193
+ export declare function findMcpSignedDefinition(registry: McpSignedDefinitionRegistry, serverIdentity: string, toolName: string): McpSignedDefinitionRegistryEntry | undefined;
194
+ /**
195
+ * Add (or replace) a publisher's public keys in the trust store. Pure: never
196
+ * mutates the input. Used to register a publisher's JWKS for verification.
197
+ */
198
+ export declare function trustMcpPublisher(store: McpPublisherTrustStore, publisher: string, keys: McpPublisherKey[]): McpPublisherTrustStore;
199
+ /** Public JWKS for a publisher keyring, suitable for distribution. */
200
+ export declare function mcpPublisherJwks(keyring: McpPublisherKeyring): {
201
+ keys: McpPublisherKey[];
202
+ };
203
+ /** The active signing key for a publisher keyring. */
204
+ export declare function mcpPublisherSigningKey(keyring: McpPublisherKeyring): {
205
+ kid: string;
206
+ privateKey: KeyObject;
207
+ publicJwk: McpPublisherKey;
208
+ };
209
+ /**
210
+ * Load or create a publisher keyring. The private keys are written `0600` via
211
+ * an atomic rename; a missing file is first-use, a corrupt file fails loud.
212
+ */
213
+ export declare function loadOrCreateMcpPublisherKeyring(input: {
214
+ filePath: string;
215
+ publisher: string;
216
+ now?: Date;
217
+ }): Promise<McpPublisherKeyring>;
218
+ //# sourceMappingURL=signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,429 @@
1
+ import { generateKeyPairSync, createPrivateKey, randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { decodeProtectedHeader, importJWK, jwtVerify, SignJWT, } from "jose";
5
+ import { z } from "zod";
6
+ import { createMcpToolHandler, hashMcpToolDefinition, mcpToolResource, } from "./core.js";
7
+ /**
8
+ * A signed MCP tool definition gives a tool a *publisher identity* and an
9
+ * immutable, versioned lineage on top of the content hash that
10
+ * `@axtary/mcp` already computes. This is the Axtary profile of the ETDI idea
11
+ * (arXiv:2506.01333): the publisher signs the exact `definitionHash`, a
12
+ * semantic version, the required scopes, and a pointer to the prior version's
13
+ * hash. A client rejects any definition that is unsigned, signed by an
14
+ * unknown key, mis-signed, or whose on-the-wire bytes no longer hash to the
15
+ * signed `definitionHash` — and it rejects it *before* the tool is invoked.
16
+ *
17
+ * We stay on the JOSE/ES256 profile already used by the ActionPass issuer
18
+ * keyring; the signature is a compact JWT (`statement+jwt`).
19
+ */
20
+ export const MCP_SIGNED_DEFINITION_PROFILE = "axtary.mcp_signed_definition.v1";
21
+ export const MCP_SIGNED_DEFINITION_TYP = "axtary-mcp-tool-definition+jwt";
22
+ export const MCP_PUBLISHER_KEYRING_VERSION = "axtary.mcp_publisher_keyring.v0";
23
+ export const MCP_PUBLISHER_TRUST_VERSION = "axtary.mcp_publisher_trust.v0";
24
+ export const MCP_SIGNED_DEFINITION_REGISTRY_VERSION = "axtary.mcp_signed_definition_registry.v0";
25
+ const SemverSchema = z
26
+ .string()
27
+ .regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/, "mcp_semver_invalid");
28
+ /**
29
+ * The claims the publisher signs. `definitionHash` binds the signature to the
30
+ * exact tool definition; `priorDefinitionHash` links this version to the one
31
+ * it supersedes so a rug-pull (silent re-definition) is visible as a chain.
32
+ */
33
+ export const McpSignedDefinitionClaimsSchema = z.object({
34
+ iss: z.string().min(1),
35
+ sub: z.string().min(1),
36
+ iat: z.number().int().positive(),
37
+ axtary_mcp_profile: z.literal(MCP_SIGNED_DEFINITION_PROFILE),
38
+ serverIdentity: z.string().min(1),
39
+ toolName: z.string().min(1),
40
+ schemaVersion: z.string().min(1),
41
+ definitionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
42
+ semver: SemverSchema,
43
+ requiredScopes: z.array(z.string().min(1)).default([]),
44
+ priorDefinitionHash: z
45
+ .string()
46
+ .regex(/^sha256:[a-f0-9]{64}$/)
47
+ .nullable()
48
+ .default(null),
49
+ });
50
+ /** A public publisher key, JWKS-shaped, resolvable by `kid`. */
51
+ export const McpPublisherKeySchema = z.object({
52
+ kid: z.string().min(1),
53
+ kty: z.string().min(1),
54
+ crv: z.string().min(1),
55
+ x: z.string().min(1),
56
+ y: z.string().min(1),
57
+ alg: z.literal("ES256").default("ES256"),
58
+ use: z.literal("sig").default("sig"),
59
+ });
60
+ const McpPublisherKeyRecordSchema = McpPublisherKeySchema.extend({
61
+ privateKeyPem: z.string().min(1),
62
+ createdAt: z.string().min(1),
63
+ });
64
+ export const McpPublisherKeyringSchema = z.object({
65
+ schemaVersion: z.literal(MCP_PUBLISHER_KEYRING_VERSION),
66
+ publisher: z.string().min(1),
67
+ activeKid: z.string().min(1),
68
+ keys: z.array(McpPublisherKeyRecordSchema).min(1),
69
+ });
70
+ /**
71
+ * The verifier-side trust anchor: a configured map of publisher identity to
72
+ * the public keys we will accept signatures from. A signature whose `kid` is
73
+ * not in this store is rejected — there is no trust-on-first-use for
74
+ * *signatures* (that would defeat publisher identity); TOFU stays at the pin
75
+ * layer, which is a deliberate, separate, human-reviewed decision.
76
+ */
77
+ export const McpPublisherTrustEntrySchema = z.object({
78
+ publisher: z.string().min(1),
79
+ keys: z.array(McpPublisherKeySchema).min(1),
80
+ });
81
+ export const McpPublisherTrustStoreSchema = z.object({
82
+ schemaVersion: z.literal(MCP_PUBLISHER_TRUST_VERSION),
83
+ publishers: z.array(McpPublisherTrustEntrySchema).default([]),
84
+ });
85
+ export const McpSignedDefinitionRegistryEntrySchema = z.object({
86
+ serverIdentity: z.string().min(1),
87
+ toolName: z.string().min(1),
88
+ publisher: z.string().min(1),
89
+ statement: z.string().min(1),
90
+ });
91
+ export const McpSignedDefinitionRegistrySchema = z.object({
92
+ schemaVersion: z.literal(MCP_SIGNED_DEFINITION_REGISTRY_VERSION),
93
+ statements: z.array(McpSignedDefinitionRegistryEntrySchema).default([]),
94
+ });
95
+ /**
96
+ * Publisher-side: sign a tool definition. The signature commits to the exact
97
+ * `definitionHash`, so a later byte-level change to the definition cannot be
98
+ * presented under this signature.
99
+ */
100
+ export async function signMcpToolDefinition(input) {
101
+ const semver = SemverSchema.parse(input.semver);
102
+ const definitionHash = hashMcpToolDefinition(input.definition);
103
+ const serverIdentity = input.definition.serverIdentity;
104
+ const toolName = input.definition.name;
105
+ const issuedAt = Math.floor((input.now ?? new Date()).getTime() / 1000);
106
+ const claims = McpSignedDefinitionClaimsSchema.parse({
107
+ iss: input.publisher,
108
+ sub: mcpToolResource(serverIdentity, toolName),
109
+ iat: issuedAt,
110
+ axtary_mcp_profile: MCP_SIGNED_DEFINITION_PROFILE,
111
+ serverIdentity,
112
+ toolName,
113
+ schemaVersion: input.definition.schemaVersion,
114
+ definitionHash,
115
+ semver,
116
+ requiredScopes: input.requiredScopes ?? [],
117
+ priorDefinitionHash: input.priorDefinitionHash ?? null,
118
+ });
119
+ const statement = await new SignJWT({
120
+ axtary_mcp_profile: claims.axtary_mcp_profile,
121
+ serverIdentity: claims.serverIdentity,
122
+ toolName: claims.toolName,
123
+ schemaVersion: claims.schemaVersion,
124
+ definitionHash: claims.definitionHash,
125
+ semver: claims.semver,
126
+ requiredScopes: claims.requiredScopes,
127
+ priorDefinitionHash: claims.priorDefinitionHash,
128
+ })
129
+ .setProtectedHeader({
130
+ alg: "ES256",
131
+ kid: input.keyId,
132
+ typ: MCP_SIGNED_DEFINITION_TYP,
133
+ })
134
+ .setIssuer(claims.iss)
135
+ .setSubject(claims.sub)
136
+ .setIssuedAt(issuedAt)
137
+ .sign(input.signingKey);
138
+ return { statement, claims };
139
+ }
140
+ /**
141
+ * Verifier-side: verify a signed definition against a known publisher key and
142
+ * the *actual* tool definition presented now. Fails closed on every distinct
143
+ * failure with a stable reason. The `definitionHash` recomputed from the live
144
+ * definition must equal the signed hash — that is the altered-definition /
145
+ * tool-poisoning rejection.
146
+ */
147
+ export async function verifyMcpSignedDefinition(input) {
148
+ let header;
149
+ try {
150
+ header = decodeProtectedHeader(input.statement);
151
+ }
152
+ catch {
153
+ throw new Error("mcp_signature_malformed");
154
+ }
155
+ if (header.typ !== MCP_SIGNED_DEFINITION_TYP) {
156
+ throw new Error("mcp_signature_type_invalid");
157
+ }
158
+ if (!header.kid) {
159
+ throw new Error("mcp_signature_key_id_required");
160
+ }
161
+ const key = await input.verificationKeys(header.kid);
162
+ if (!key) {
163
+ throw new Error(`mcp_publisher_key_not_found:${header.kid}`);
164
+ }
165
+ const expectedSubject = mcpToolResource(input.definition.serverIdentity, input.definition.name);
166
+ let payload;
167
+ try {
168
+ ({ payload } = await jwtVerify(input.statement, key, {
169
+ algorithms: ["ES256"],
170
+ issuer: input.expectedPublisher,
171
+ subject: expectedSubject,
172
+ }));
173
+ }
174
+ catch {
175
+ throw new Error("mcp_signature_invalid");
176
+ }
177
+ const parsed = McpSignedDefinitionClaimsSchema.safeParse(payload);
178
+ if (!parsed.success) {
179
+ throw new Error("mcp_signed_claims_invalid");
180
+ }
181
+ const claims = parsed.data;
182
+ if (claims.axtary_mcp_profile !== MCP_SIGNED_DEFINITION_PROFILE) {
183
+ throw new Error("mcp_signed_profile_invalid");
184
+ }
185
+ if (claims.serverIdentity !== input.definition.serverIdentity) {
186
+ throw new Error(`mcp_signed_server_identity_mismatch:${input.definition.serverIdentity}`);
187
+ }
188
+ if (claims.toolName !== input.definition.name) {
189
+ throw new Error(`mcp_signed_tool_name_mismatch:${input.definition.name}`);
190
+ }
191
+ if (claims.schemaVersion !== input.definition.schemaVersion) {
192
+ throw new Error(`mcp_signed_schema_version_mismatch:${input.definition.schemaVersion}`);
193
+ }
194
+ const liveHash = hashMcpToolDefinition(input.definition);
195
+ if (claims.definitionHash !== liveHash) {
196
+ throw new Error(`mcp_signed_definition_hash_mismatch:${liveHash}`);
197
+ }
198
+ return {
199
+ claims,
200
+ publisher: claims.iss,
201
+ kid: header.kid,
202
+ semver: claims.semver,
203
+ requiredScopes: claims.requiredScopes,
204
+ definitionHash: claims.definitionHash,
205
+ priorDefinitionHash: claims.priorDefinitionHash,
206
+ };
207
+ }
208
+ /**
209
+ * Wrap a tool invoker so the publisher signature is verified against the live
210
+ * definition *before* the tool runs. Any failure — unsigned (no statement
211
+ * configured is simply not this handler), mis-signed, unknown key, or an
212
+ * altered definition whose bytes no longer hash to the signed value — throws
213
+ * and the tool is never invoked. When the action carries bound provenance, it
214
+ * must also agree with the verified signature.
215
+ */
216
+ export function createSignedMcpToolHandler(input) {
217
+ const baseHandler = createMcpToolHandler({
218
+ definition: input.definition,
219
+ invoke: input.invoke,
220
+ });
221
+ return async (request) => {
222
+ const verification = await verifyMcpSignedDefinition({
223
+ statement: input.statement,
224
+ definition: input.definition,
225
+ verificationKeys: input.verificationKeys,
226
+ expectedPublisher: input.expectedPublisher,
227
+ });
228
+ assertActionProvenanceMatches(request.action, verification);
229
+ return baseHandler(request);
230
+ };
231
+ }
232
+ /**
233
+ * If an action was built with bound signed provenance, it must match the
234
+ * signature we just verified. A hash-only action (no bound provenance) is
235
+ * allowed through a signed handler — the signature check above already gates
236
+ * it — but mismatched bound provenance is a fail-closed rejection.
237
+ */
238
+ function assertActionProvenanceMatches(action, verification) {
239
+ const bound = action.toolDefinition;
240
+ if (!bound)
241
+ return;
242
+ if (bound.definitionHash !== verification.definitionHash) {
243
+ throw new Error(`mcp_signed_definition_hash_mismatch:${verification.definitionHash}`);
244
+ }
245
+ if (bound.publisher && bound.publisher !== verification.publisher) {
246
+ throw new Error(`mcp_signed_publisher_mismatch:${verification.publisher}`);
247
+ }
248
+ if (bound.publisherKeyId && bound.publisherKeyId !== verification.kid) {
249
+ throw new Error(`mcp_signed_publisher_key_mismatch:${verification.kid}`);
250
+ }
251
+ if (bound.semver && bound.semver !== verification.semver) {
252
+ throw new Error(`mcp_signed_semver_mismatch:${verification.semver}`);
253
+ }
254
+ }
255
+ /**
256
+ * Build a key resolver from a configured trust store. A `kid` that is not
257
+ * present (optionally scoped to `expectedPublisher`) resolves to `undefined`,
258
+ * which makes verification fail closed.
259
+ */
260
+ export function mcpPublisherKeyResolver(store, options = {}) {
261
+ const parsed = McpPublisherTrustStoreSchema.parse(store);
262
+ return async (kid) => {
263
+ if (!kid)
264
+ return undefined;
265
+ for (const entry of parsed.publishers) {
266
+ if (options.publisher && entry.publisher !== options.publisher) {
267
+ continue;
268
+ }
269
+ const jwk = entry.keys.find((key) => key.kid === kid);
270
+ if (jwk) {
271
+ return importJWK(jwk, "ES256");
272
+ }
273
+ }
274
+ return undefined;
275
+ };
276
+ }
277
+ export function emptyMcpPublisherTrustStore() {
278
+ return { schemaVersion: MCP_PUBLISHER_TRUST_VERSION, publishers: [] };
279
+ }
280
+ export function parseMcpPublisherTrustStore(input) {
281
+ return McpPublisherTrustStoreSchema.parse(input);
282
+ }
283
+ export async function loadMcpPublisherTrustStore(path) {
284
+ try {
285
+ return parseMcpPublisherTrustStore(JSON.parse(await readFile(path, "utf8")));
286
+ }
287
+ catch (error) {
288
+ if (error.code === "ENOENT") {
289
+ return emptyMcpPublisherTrustStore();
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+ export async function saveMcpPublisherTrustStore(path, store) {
295
+ await writePrivateJson(path, McpPublisherTrustStoreSchema.parse(store));
296
+ }
297
+ export async function loadMcpSignedDefinitionRegistry(path) {
298
+ try {
299
+ return McpSignedDefinitionRegistrySchema.parse(JSON.parse(await readFile(path, "utf8")));
300
+ }
301
+ catch (error) {
302
+ if (error.code === "ENOENT") {
303
+ return {
304
+ schemaVersion: MCP_SIGNED_DEFINITION_REGISTRY_VERSION,
305
+ statements: [],
306
+ };
307
+ }
308
+ throw error;
309
+ }
310
+ }
311
+ export function findMcpSignedDefinition(registry, serverIdentity, toolName) {
312
+ const parsed = McpSignedDefinitionRegistrySchema.parse(registry);
313
+ return parsed.statements.find((entry) => entry.serverIdentity === serverIdentity && entry.toolName === toolName);
314
+ }
315
+ /**
316
+ * Add (or replace) a publisher's public keys in the trust store. Pure: never
317
+ * mutates the input. Used to register a publisher's JWKS for verification.
318
+ */
319
+ export function trustMcpPublisher(store, publisher, keys) {
320
+ const parsed = McpPublisherTrustStoreSchema.parse(store);
321
+ const entry = McpPublisherTrustEntrySchema.parse({
322
+ publisher,
323
+ keys: keys.map((key) => McpPublisherKeySchema.parse(key)),
324
+ });
325
+ const others = parsed.publishers.filter((p) => p.publisher !== publisher);
326
+ return {
327
+ schemaVersion: MCP_PUBLISHER_TRUST_VERSION,
328
+ publishers: [...others, entry].sort((left, right) => left.publisher < right.publisher ? -1 : left.publisher > right.publisher ? 1 : 0),
329
+ };
330
+ }
331
+ /** Public JWKS for a publisher keyring, suitable for distribution. */
332
+ export function mcpPublisherJwks(keyring) {
333
+ const parsed = McpPublisherKeyringSchema.parse(keyring);
334
+ return {
335
+ keys: parsed.keys.map((key) => McpPublisherKeySchema.parse(stripPrivate(key))),
336
+ };
337
+ }
338
+ /** The active signing key for a publisher keyring. */
339
+ export function mcpPublisherSigningKey(keyring) {
340
+ const parsed = McpPublisherKeyringSchema.parse(keyring);
341
+ const active = parsed.keys.find((key) => key.kid === parsed.activeKid);
342
+ if (!active) {
343
+ throw new Error("mcp_publisher_active_key_missing");
344
+ }
345
+ return {
346
+ kid: active.kid,
347
+ privateKey: createPrivateKey(active.privateKeyPem),
348
+ publicJwk: McpPublisherKeySchema.parse(stripPrivate(active)),
349
+ };
350
+ }
351
+ /**
352
+ * Load or create a publisher keyring. The private keys are written `0600` via
353
+ * an atomic rename; a missing file is first-use, a corrupt file fails loud.
354
+ */
355
+ export async function loadOrCreateMcpPublisherKeyring(input) {
356
+ try {
357
+ const data = McpPublisherKeyringSchema.parse(JSON.parse(await readFile(input.filePath, "utf8")));
358
+ if (data.publisher !== input.publisher) {
359
+ throw new Error("mcp_publisher_keyring_identity_mismatch");
360
+ }
361
+ return data;
362
+ }
363
+ catch (error) {
364
+ if (error.code !== "ENOENT") {
365
+ throw error;
366
+ }
367
+ }
368
+ const key = createMcpPublisherKeyRecord(input.now ?? new Date());
369
+ const keyring = {
370
+ schemaVersion: MCP_PUBLISHER_KEYRING_VERSION,
371
+ publisher: input.publisher,
372
+ activeKid: key.kid,
373
+ keys: [key],
374
+ };
375
+ await writePrivateJson(input.filePath, keyring);
376
+ return keyring;
377
+ }
378
+ function createMcpPublisherKeyRecord(now) {
379
+ const { privateKey, publicKey } = generateKeyPairSync("ec", {
380
+ namedCurve: "P-256",
381
+ });
382
+ const kid = `mcp-publisher-${randomUUID().slice(0, 12)}`;
383
+ const jwk = publicKey.export({ format: "jwk" });
384
+ return McpPublisherKeyRecordSchema.parse({
385
+ kid,
386
+ kty: jwk.kty,
387
+ crv: jwk.crv,
388
+ x: jwk.x,
389
+ y: jwk.y,
390
+ alg: "ES256",
391
+ use: "sig",
392
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
393
+ createdAt: now.toISOString(),
394
+ });
395
+ }
396
+ function stripPrivate(key) {
397
+ return {
398
+ kid: key.kid,
399
+ kty: key.kty,
400
+ crv: key.crv,
401
+ x: key.x,
402
+ y: key.y,
403
+ alg: key.alg,
404
+ use: key.use,
405
+ };
406
+ }
407
+ async function writePrivateJson(filePath, value) {
408
+ await mkdir(dirname(filePath), { recursive: true });
409
+ const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
410
+ let renamed = false;
411
+ try {
412
+ const handle = await open(tmpPath, "wx", 0o600);
413
+ try {
414
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
415
+ await handle.sync();
416
+ }
417
+ finally {
418
+ await handle.close();
419
+ }
420
+ await rename(tmpPath, filePath);
421
+ renamed = true;
422
+ }
423
+ finally {
424
+ if (!renamed) {
425
+ await rm(tmpPath, { force: true });
426
+ }
427
+ }
428
+ }
429
+ //# sourceMappingURL=signing.js.map