@mudraid/sidecar 1.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.
@@ -0,0 +1,898 @@
1
+ // src/config.ts
2
+ var DEFAULT_MAX_BODY_BYTES = 1048576;
3
+
4
+ // src/facts.ts
5
+ function classifyJsonShape(body) {
6
+ if (body.length === 0) {
7
+ return { shape: "not_json", value: void 0 };
8
+ }
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(body.toString("utf-8"));
12
+ } catch {
13
+ return { shape: "not_json", value: void 0 };
14
+ }
15
+ if (Array.isArray(parsed)) {
16
+ return { shape: "array", value: parsed };
17
+ }
18
+ if (parsed !== null && typeof parsed === "object") {
19
+ return { shape: "object", value: parsed };
20
+ }
21
+ return { shape: "scalar", value: parsed };
22
+ }
23
+ function readString(obj, key) {
24
+ const v = obj[key];
25
+ return typeof v === "string" ? v : null;
26
+ }
27
+ function buildFacts(inbound, config) {
28
+ const presentedHeaderNames = Object.keys(inbound.headers);
29
+ const { shape, value } = classifyJsonShape(inbound.body);
30
+ let jsonrpc = null;
31
+ let rpcMethod = null;
32
+ let toolName = null;
33
+ let actionMapped = false;
34
+ let action = null;
35
+ if (shape === "object") {
36
+ const obj = value;
37
+ jsonrpc = readString(obj, "jsonrpc");
38
+ rpcMethod = readString(obj, "method");
39
+ if (rpcMethod === "tools/call") {
40
+ const params = obj["params"];
41
+ if (params !== null && typeof params === "object") {
42
+ toolName = readString(params, "name");
43
+ }
44
+ if (toolName !== null && Object.prototype.hasOwnProperty.call(config.actionMap, toolName)) {
45
+ actionMapped = true;
46
+ action = config.actionMap[toolName] ?? toolName;
47
+ }
48
+ }
49
+ }
50
+ return {
51
+ protected: config.protectedSurface,
52
+ bundleActive: config.bundleActive,
53
+ method: inbound.method,
54
+ reservedHeadersPresented: presentedHeaderNames,
55
+ bodyReadable: inbound.bodyReadable,
56
+ bodyTooLarge: inbound.bodyTooLarge,
57
+ jsonShape: shape,
58
+ jsonrpc,
59
+ rpcMethod,
60
+ toolName,
61
+ actionMapped,
62
+ action
63
+ };
64
+ }
65
+
66
+ // ../mudraid-adapter-node/src/decideClient.ts
67
+ function staticDecideClient(result) {
68
+ return async () => result;
69
+ }
70
+ function throwingDecideClient(error) {
71
+ return async () => {
72
+ throw error ?? new Error("decide transport failure");
73
+ };
74
+ }
75
+
76
+ // ../mudraid-adapter-node/src/httpAuthority.ts
77
+ import { randomUUID } from "node:crypto";
78
+
79
+ // ../mudraid-adapter-node/src/signedBundle.ts
80
+ import { createHash, createPublicKey, verify } from "node:crypto";
81
+ function object(value) {
82
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid object");
83
+ return value;
84
+ }
85
+ function canonicalJson(value) {
86
+ if (value === null || typeof value === "boolean") return JSON.stringify(value);
87
+ if (typeof value === "number") {
88
+ if (!Number.isSafeInteger(value)) throw new Error("Unsupported canonical number");
89
+ return JSON.stringify(value);
90
+ }
91
+ if (typeof value === "string") {
92
+ return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
93
+ }
94
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
95
+ const record = object(value);
96
+ const compare = (a, b) => {
97
+ const aa = Array.from(a, (c) => c.codePointAt(0));
98
+ const bb = Array.from(b, (c) => c.codePointAt(0));
99
+ for (let i = 0; i < Math.min(aa.length, bb.length); i++) {
100
+ if (aa[i] !== bb[i]) return aa[i] - bb[i];
101
+ }
102
+ return aa.length - bb.length;
103
+ };
104
+ return `{${Object.keys(record).sort(compare).map((k) => `${canonicalJson(k)}:${canonicalJson(record[k])}`).join(",")}}`;
105
+ }
106
+ function instant(value) {
107
+ if (typeof value !== "string" || !/(Z|[+-]\d{2}:\d{2})$/.test(value)) throw new Error("Invalid timestamp");
108
+ const time = Date.parse(value);
109
+ if (!Number.isFinite(time)) throw new Error("Invalid timestamp");
110
+ return time;
111
+ }
112
+ function verifyClaims(claims, encoded, pem) {
113
+ if (typeof encoded !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded) || !encoded) throw new Error("Invalid signature");
114
+ if (typeof pem !== "string") throw new Error("Unknown signing key");
115
+ const key = createPublicKey(pem);
116
+ if (key.asymmetricKeyType !== "rsa" || (key.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) throw new Error("Invalid signing key");
117
+ if (!verify("RSA-SHA256", Buffer.from(canonicalJson(claims)), key, Buffer.from(encoded, "base64"))) throw new Error("Invalid signature");
118
+ }
119
+ function equal(actual, expected) {
120
+ if (actual !== expected) throw new Error("Bundle binding mismatch");
121
+ }
122
+ function freeze(value) {
123
+ if (value !== null && typeof value === "object") {
124
+ for (const child of Object.values(value)) freeze(child);
125
+ Object.freeze(value);
126
+ }
127
+ return value;
128
+ }
129
+ function verifyBundle(input, keys, binding, active, now = Date.now()) {
130
+ const envelope = object(input);
131
+ const payload = object(envelope["payload"]);
132
+ const claims = object(envelope["signature_claims"]);
133
+ const version = envelope["bundle_version"];
134
+ if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) throw new Error("Invalid bundle version");
135
+ equal(envelope["schema_version"], "1.0");
136
+ equal(payload["schema_version"], "1.0");
137
+ equal(payload["bundle_version"], version);
138
+ const digest = createHash("sha256").update(canonicalJson(payload)).digest("hex");
139
+ equal(envelope["payload_digest"], digest);
140
+ const keyId = envelope["signature_key_id"];
141
+ if (typeof keyId !== "string" || !Object.hasOwn(keys, keyId)) throw new Error("Unknown signing key");
142
+ const profile = "mudraid.bundle.signature/1";
143
+ equal(envelope["signature_profile"], profile);
144
+ equal(envelope["signature_algorithm"], "RS256");
145
+ equal(claims["profile"], profile);
146
+ equal(claims["algorithm"], "RS256");
147
+ equal(claims["key_id"], keyId);
148
+ verifyClaims(claims, envelope["signature_value"], keys[keyId]);
149
+ equal(claims["payload_digest"], digest);
150
+ equal(claims["bundle_version"], version);
151
+ const content = object(payload["content"]);
152
+ const surface = object(content["surface"]);
153
+ for (const [field, expected] of [
154
+ ["platform_id", binding.platformId],
155
+ ["environment", binding.environment],
156
+ ["canonical_resource_uri", binding.resource]
157
+ ]) {
158
+ if (!expected.trim()) throw new Error("Unbound surface");
159
+ equal(surface[field], expected);
160
+ equal(claims[field], expected);
161
+ }
162
+ const starts = instant(claims["not_before"]);
163
+ const expires = instant(claims["expires_at"]);
164
+ if (starts > now || expires <= now || expires <= starts) throw new Error("Bundle outside validity window");
165
+ const evaluation = object(content["evaluation"]);
166
+ for (const [field, expected] of Object.entries({ mode: "live", on_timeout: "deny", on_error: "deny", on_unmapped_action: "deny", on_stale_bundle: "deny", forward: "once", decide_required: true, retry_forwarded_request: false })) equal(evaluation[field], expected);
167
+ const matcher = object(content["matcher"]);
168
+ equal(matcher["kind"], "mcp_tool_exact");
169
+ const entries = matcher["actions"];
170
+ if (!Array.isArray(entries) || entries.length === 0 || entries.length > 1e4) throw new Error("Invalid action map");
171
+ const actions = /* @__PURE__ */ Object.create(null);
172
+ for (const entry of entries) {
173
+ const action = object(entry);
174
+ const tool = action["tool_name"];
175
+ const actionKey = action["action_key"];
176
+ if (typeof tool !== "string" || !tool || Buffer.byteLength(tool) > 512 || Object.hasOwn(actions, tool)) throw new Error("Invalid or ambiguous tool");
177
+ if (typeof actionKey !== "string" || !actionKey || Buffer.byteLength(actionKey) > 512) throw new Error("Invalid action");
178
+ actions[tool] = JSON.parse(JSON.stringify(action));
179
+ }
180
+ if (active && (version < active.version || version === active.version && digest !== active.digest)) throw new Error("Bundle rollback or conflict");
181
+ return freeze({ version, digest, expiresAt: expires, surface: JSON.parse(JSON.stringify(surface)), actions });
182
+ }
183
+
184
+ // ../mudraid-adapter-node/src/executionBinding.ts
185
+ import { createHash as createHash2 } from "node:crypto";
186
+ var sha256 = (bytes) => createHash2("sha256").update(bytes).digest("hex");
187
+ function bindExecution(snapshot, action, context) {
188
+ if (!(context.body instanceof Uint8Array) || context.body.byteLength > 8 * 1024 * 1024) throw new Error("Invalid request body");
189
+ if (!context.contentType || context.contentType.length > 256 || /[\r\n]/.test(context.contentType)) throw new Error("Invalid content type");
190
+ let token = context.presentedAuthorization.trim();
191
+ if (token.toLowerCase().startsWith("bearer ")) token = token.slice(7).trim();
192
+ if (!token) throw new Error("Missing caller");
193
+ const scopes = action["required_scopes"];
194
+ if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string" || !/^[\x21\x23-\x5b\x5d-\x7e]+$/.test(scope))) throw new Error("Invalid action scopes");
195
+ const material = {
196
+ profile: "mudraid.execution.request/1",
197
+ body_sha256: sha256(context.body),
198
+ content_type: context.contentType,
199
+ http_method: context.httpMethod,
200
+ path: context.path,
201
+ caller_token_sha256: sha256(token),
202
+ platform_id: snapshot.surface["platform_id"],
203
+ environment: snapshot.surface["environment"],
204
+ resource: snapshot.surface["canonical_resource_uri"],
205
+ action_key: action["action_key"],
206
+ action_version: action["action_version"],
207
+ mapping_id: action["mapping_id"],
208
+ mapping_version: action["mapping_revision"],
209
+ bundle_version: snapshot.version,
210
+ bundle_payload_digest: snapshot.digest,
211
+ required_scopes: [...new Set(scopes)].sort()
212
+ };
213
+ if (Object.values(material).some((value) => value === void 0 || value === null || value === "")) throw new Error("Incomplete execution binding");
214
+ return { digest: sha256(canonicalJson(material)), execution: {
215
+ profile: material["profile"],
216
+ body_sha256: material["body_sha256"],
217
+ content_type: material["content_type"],
218
+ ...action["argument_profile"] == null ? {} : { body_base64: boundedArgumentBody(context.body) }
219
+ } };
220
+ }
221
+ function boundedArgumentBody(body) {
222
+ if (body.byteLength === 0 || body.byteLength > 65536) throw new Error("Argument body exceeds bounds");
223
+ return Buffer.from(body).toString("base64");
224
+ }
225
+
226
+ // ../mudraid-adapter-node/src/httpAuthority.ts
227
+ function keyMap(entries) {
228
+ if (!Array.isArray(entries)) throw new Error("Invalid verification key set");
229
+ const keys = /* @__PURE__ */ Object.create(null);
230
+ for (const entry of entries) {
231
+ const row = object(entry);
232
+ if (typeof row["key_id"] !== "string" || typeof row["public_key_pem"] !== "string" || Object.hasOwn(keys, row["key_id"])) throw new Error("Invalid verification key");
233
+ keys[row["key_id"]] = row["public_key_pem"];
234
+ }
235
+ return keys;
236
+ }
237
+ var HttpAuthority = class {
238
+ base;
239
+ token;
240
+ binding;
241
+ timeout;
242
+ adapterType;
243
+ fetcher;
244
+ current;
245
+ lastAccepted;
246
+ decisionKeys = /* @__PURE__ */ Object.create(null);
247
+ refreshPending;
248
+ observed;
249
+ constructor(options) {
250
+ this.base = new URL(options.apiBase);
251
+ if (this.base.protocol !== "https:" || this.base.username || this.base.password || this.base.search || this.base.hash || this.base.pathname !== "/") throw new Error("Authority must be an HTTPS origin");
252
+ if (!options.adapterToken || options.adapterToken.length > 256 || /\s/.test(options.adapterToken)) throw new Error("Invalid adapter credential");
253
+ this.token = options.adapterToken;
254
+ this.adapterType = options.adapterType ?? "node_server_adapter";
255
+ this.binding = Object.freeze({ ...options.binding });
256
+ this.timeout = options.timeoutMs ?? 5e3;
257
+ if (!Number.isSafeInteger(this.timeout) || this.timeout < 1 || this.timeout > 3e4) throw new Error("Invalid authority timeout");
258
+ this.fetcher = options.fetch ?? globalThis.fetch;
259
+ }
260
+ get bundle() {
261
+ return this.current && this.current.expiresAt > Date.now() ? this.current : void 0;
262
+ }
263
+ async request(path, method, body, authenticated = true) {
264
+ const controller = new AbortController();
265
+ const timer = setTimeout(() => controller.abort(), this.timeout);
266
+ try {
267
+ const response = await this.fetcher(new URL(`/api/v1/adapter/enforcement/${path}`, this.base), {
268
+ method,
269
+ redirect: "error",
270
+ signal: controller.signal,
271
+ headers: { Accept: "application/json", ...authenticated ? { Authorization: `Bearer ${this.token}` } : {}, ...body === void 0 ? {} : { "Content-Type": "application/json" } },
272
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
273
+ });
274
+ if (!response.ok || !response.body) throw new Error("Authority unavailable");
275
+ const reader = response.body.getReader();
276
+ const chunks = [];
277
+ let size = 0;
278
+ try {
279
+ while (true) {
280
+ const item = await reader.read();
281
+ if (item.done) break;
282
+ size += item.value.byteLength;
283
+ if (size > 2 * 1024 * 1024) throw new Error("Authority response too large");
284
+ chunks.push(item.value);
285
+ }
286
+ } finally {
287
+ await reader.cancel();
288
+ }
289
+ return object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
290
+ } finally {
291
+ clearTimeout(timer);
292
+ }
293
+ }
294
+ /** Single-flight refresh. A failed refresh never activates unverified data. */
295
+ refresh() {
296
+ if (!this.refreshPending) this.refreshPending = this.refreshOnce().finally(() => {
297
+ this.refreshPending = void 0;
298
+ });
299
+ return this.refreshPending;
300
+ }
301
+ async refreshOnce() {
302
+ try {
303
+ const heartbeat = await this.request("heartbeat", "POST");
304
+ if (heartbeat["platform_id"] !== this.binding.platformId) {
305
+ this.current = void 0;
306
+ return false;
307
+ }
308
+ const keyResponse = await this.request("keys", "GET", void 0, false);
309
+ const bundleKeys = keyMap(keyResponse["keys"]);
310
+ this.decisionKeys = /* @__PURE__ */ Object.create(null);
311
+ if (Array.isArray(keyResponse["key_sets"])) {
312
+ for (const item of keyResponse["key_sets"]) {
313
+ const set = object(item);
314
+ if (set["purpose"] === "enforcement_decision_signing") this.decisionKeys = keyMap(set["keys"]);
315
+ }
316
+ }
317
+ const served = await this.request("bundle", "GET");
318
+ const verified = verifyBundle(served, bundleKeys, this.binding, this.lastAccepted);
319
+ if (heartbeat["desired_bundle_version"] !== verified.version || heartbeat["desired_payload_digest"] !== verified.digest) throw new Error("Desired bundle mismatch");
320
+ this.current = verified;
321
+ this.lastAccepted = verified;
322
+ const now = (/* @__PURE__ */ new Date()).toISOString();
323
+ const observedAt = this.observed?.version === verified.version && this.observed.digest === verified.digest ? this.observed.at : void 0;
324
+ await this.request("acknowledgements", "POST", {
325
+ report_id: randomUUID(),
326
+ received_version: verified.version,
327
+ received_at: now,
328
+ validated_version: verified.version,
329
+ validated_at: now,
330
+ active_version: verified.version,
331
+ active_at: now,
332
+ bundle_digest: verified.digest,
333
+ ...observedAt === void 0 ? {} : { first_observed_decision_at: observedAt }
334
+ });
335
+ return true;
336
+ } catch {
337
+ this.current = void 0;
338
+ return false;
339
+ }
340
+ }
341
+ async decide(toolName, context, snapshot = this.bundle) {
342
+ if (!snapshot || snapshot !== this.bundle) return { status: "unconfigured" };
343
+ const mapped = snapshot.actions[toolName];
344
+ if (!mapped) return { status: "deny", reason: "action_unmapped" };
345
+ if (!context.presentedAuthorization || context.presentedAuthorization.length > 8192) return { status: "deny", reason: "credential_missing" };
346
+ const decisionId = randomUUID();
347
+ const action = mapped["action_key"];
348
+ try {
349
+ const bound = bindExecution(snapshot, mapped, context);
350
+ const response = await this.request("decide", "POST", {
351
+ schema_version: "mudraid.enforce.decide-request/1",
352
+ decision_id: decisionId,
353
+ adapter: { type: this.adapterType, version: "1.1.0" },
354
+ bundle: { version: snapshot.version, payload_digest: snapshot.digest },
355
+ surface: snapshot.surface,
356
+ action: mapped,
357
+ request: { transport: "mcp_streamable_http", http_method: context.httpMethod, path: context.path },
358
+ presented_authorization: context.presentedAuthorization,
359
+ execution: bound.execution
360
+ });
361
+ if (response["schema_version"] !== "2.0" || response["decision_id"] !== decisionId) throw new Error("Unbound decision");
362
+ const now = Date.now();
363
+ const decidedAt = instant(response["decided_at"]);
364
+ if (decidedAt > now + 3e4 || now - decidedAt > 6e4) throw new Error("Stale decision");
365
+ if (instant(response["deadline_at"]) <= now) throw new Error("Expired decision");
366
+ const signature = object(response["signature"]);
367
+ const claims = object(signature["claims"]);
368
+ const keyId = signature["key_id"];
369
+ const profile = "mudraid.decision.signature/1";
370
+ if (typeof keyId !== "string" || !Object.hasOwn(this.decisionKeys, keyId) || signature["profile"] !== profile || signature["algorithm"] !== "RS256" || claims["profile"] !== profile || claims["algorithm"] !== "RS256" || claims["key_id"] !== keyId) throw new Error("Invalid decision signature");
371
+ verifyClaims(claims, signature["signature"], this.decisionKeys[keyId]);
372
+ if (claims["execution_request_digest"] !== bound.digest) throw new Error("Execution binding mismatch");
373
+ for (const field of ["decision_id", "decision", "outcome", "decided_at", "deadline_at"]) {
374
+ if ((claims[field] ?? null) !== (response[field] ?? null)) throw new Error("Altered decision");
375
+ }
376
+ const reason = object(response["reason"]);
377
+ if (claims["reason_primary"] !== reason["primary"]) throw new Error("Altered reason");
378
+ for (const [field, expected] of Object.entries({ platform_id: this.binding.platformId, environment: this.binding.environment, resource: this.binding.resource, action_key: action, bundle_version: snapshot.version })) {
379
+ if (claims[field] !== expected) throw new Error("Decision binding mismatch");
380
+ }
381
+ if (instant(claims["not_before"]) > now + 3e4 || instant(claims["expires_at"]) <= now) throw new Error("Invalid decision window");
382
+ if (snapshot !== this.bundle) throw new Error("Bundle changed during decision");
383
+ if (response["decision"] !== "allow" && response["decision"] !== "deny") throw new Error("Invalid decision outcome");
384
+ if (this.observed?.version !== snapshot.version || this.observed.digest !== snapshot.digest) {
385
+ this.observed = { version: snapshot.version, digest: snapshot.digest, at: new Date(now).toISOString() };
386
+ }
387
+ return { status: response["decision"], decisionId, ...typeof reason["primary"] === "string" ? { reason: reason["primary"] } : {} };
388
+ } catch {
389
+ return { status: "error" };
390
+ }
391
+ }
392
+ };
393
+
394
+ // ../mudraid-adapter-node/src/types.ts
395
+ var RESERVED_HEADER_PREFIX = "x-mudraid-";
396
+ var MAX_TOOL_NAME_LEN = 512;
397
+
398
+ // ../mudraid-adapter-node/src/controlLoop.ts
399
+ import { randomUUID as randomUUID2 } from "node:crypto";
400
+ var CONTROL_VERBS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "DELETE"]);
401
+ var DEFAULT_PUBLIC_METHODS = /* @__PURE__ */ new Set([
402
+ "initialize",
403
+ "ping",
404
+ "tools/list"
405
+ ]);
406
+ var DECIDE_UNAVAILABLE_REASONS = {
407
+ timeout: "deadline_exceeded",
408
+ error: "authority_source_unavailable",
409
+ unreachable: "authority_source_unavailable",
410
+ unconfigured: "adapter_config_stale",
411
+ credential_unconfigured: "adapter_config_stale"
412
+ };
413
+ var NON_DENY_REASONS = /* @__PURE__ */ new Set([
414
+ "authorized",
415
+ // allow
416
+ "adapter_config_stale",
417
+ // not_safely_decided
418
+ "deadline_exceeded",
419
+ // not_safely_decided
420
+ "authority_source_unavailable"
421
+ // not_safely_decided
422
+ ]);
423
+ var DECIDE_DENY_DEFAULT_REASON = "policy_rule_denied";
424
+ var NO_TRUSTED_CONTEXT = Object.freeze([]);
425
+ function normalizeStrippedHeaders(reservedHeadersPresented, { protectedSurface }) {
426
+ if (!protectedSurface) {
427
+ return [];
428
+ }
429
+ return reservedHeadersPresented.filter(
430
+ (h) => h.toLowerCase().startsWith(RESERVED_HEADER_PREFIX)
431
+ );
432
+ }
433
+ function validToolName(name) {
434
+ if (typeof name !== "string") {
435
+ return false;
436
+ }
437
+ const byteLen = Buffer.byteLength(name, "utf-8");
438
+ return byteLen > 0 && byteLen <= MAX_TOOL_NAME_LEN;
439
+ }
440
+ function newDecisionId() {
441
+ return randomUUID2();
442
+ }
443
+ function deny(reasonCode, tier, httpStatus, adapterCode, message, stripped, outcome = "deny") {
444
+ return {
445
+ outcome,
446
+ reasonCode,
447
+ reasonTier: tier,
448
+ httpStatus,
449
+ adapterCode,
450
+ message,
451
+ strippedReservedHeaders: stripped,
452
+ trustedContext: NO_TRUSTED_CONTEXT
453
+ };
454
+ }
455
+ function passThrough(reasonCode, stripped) {
456
+ return {
457
+ outcome: "allow",
458
+ reasonCode,
459
+ reasonTier: "transport",
460
+ httpStatus: 200,
461
+ adapterCode: null,
462
+ message: "",
463
+ strippedReservedHeaders: stripped,
464
+ trustedContext: NO_TRUSTED_CONTEXT
465
+ };
466
+ }
467
+ function shouldForward(decision) {
468
+ return decision.outcome === "allow";
469
+ }
470
+ async function evaluateV2(facts, decide) {
471
+ const stripped = normalizeStrippedHeaders(facts.reservedHeadersPresented ?? [], {
472
+ protectedSurface: facts.protected
473
+ });
474
+ if (!facts.protected) {
475
+ return passThrough("surface_not_protected", stripped);
476
+ }
477
+ if (!facts.bundleActive) {
478
+ return deny(
479
+ "adapter_config_stale",
480
+ "authorization",
481
+ 503,
482
+ "ENFORCE_NO_VALID_BUNDLE",
483
+ "no verified signed bundle is active; request cannot be safely decided",
484
+ stripped,
485
+ "not_safely_decided"
486
+ );
487
+ }
488
+ const method = (facts.method ?? "").toUpperCase();
489
+ if (CONTROL_VERBS.has(method)) {
490
+ return passThrough("control_plane_passthrough", stripped);
491
+ }
492
+ if (method !== "POST") {
493
+ return deny(
494
+ "method_not_allowed",
495
+ "transport",
496
+ 405,
497
+ "ENFORCE_METHOD_NOT_ALLOWED",
498
+ "method not allowed on a protected MCP surface",
499
+ stripped
500
+ );
501
+ }
502
+ if (facts.bodyTooLarge === true) {
503
+ return deny(
504
+ "body_too_large",
505
+ "transport",
506
+ 413,
507
+ "ENFORCE_BODY_TOO_LARGE",
508
+ "request body exceeds the bounded framing limit",
509
+ stripped
510
+ );
511
+ }
512
+ if (facts.bodyReadable === false) {
513
+ return deny(
514
+ "body_unreadable",
515
+ "transport",
516
+ 400,
517
+ "ENFORCE_BODY_UNREADABLE",
518
+ "request body could not be read",
519
+ stripped
520
+ );
521
+ }
522
+ const jsonShape = facts.jsonShape ?? "object";
523
+ if (jsonShape === "array") {
524
+ return deny(
525
+ "batch_unsupported",
526
+ "transport",
527
+ 400,
528
+ "ENFORCE_BATCH_UNSUPPORTED",
529
+ "JSON-RPC batch requests are not supported",
530
+ stripped
531
+ );
532
+ }
533
+ if (jsonShape !== "object") {
534
+ return deny(
535
+ "malformed_request",
536
+ "transport",
537
+ 400,
538
+ "ENFORCE_MALFORMED_REQUEST",
539
+ "request body is not a single JSON-RPC 2.0 object",
540
+ stripped
541
+ );
542
+ }
543
+ const rpcMethod = facts.rpcMethod;
544
+ if (facts.jsonrpc !== "2.0" || typeof rpcMethod !== "string" || rpcMethod === "") {
545
+ return deny(
546
+ "malformed_request",
547
+ "transport",
548
+ 400,
549
+ "ENFORCE_MALFORMED_REQUEST",
550
+ "request body is not a single JSON-RPC 2.0 object",
551
+ stripped
552
+ );
553
+ }
554
+ if (rpcMethod !== "tools/call") {
555
+ if (rpcMethod.startsWith("notifications/")) {
556
+ return passThrough("notification_passthrough", stripped);
557
+ }
558
+ if (DEFAULT_PUBLIC_METHODS.has(rpcMethod)) {
559
+ return passThrough("control_plane_passthrough", stripped);
560
+ }
561
+ return deny(
562
+ "message_not_allowed",
563
+ "transport",
564
+ 403,
565
+ "ENFORCE_MESSAGE_NOT_ALLOWED",
566
+ "JSON-RPC method is not permitted on a protected surface",
567
+ stripped
568
+ );
569
+ }
570
+ if (!validToolName(facts.toolName)) {
571
+ return deny(
572
+ "malformed_request",
573
+ "transport",
574
+ 400,
575
+ "ENFORCE_MALFORMED_REQUEST",
576
+ "tools/call params.name is missing or exceeds the action-name bound",
577
+ stripped
578
+ );
579
+ }
580
+ if (facts.actionMapped !== true) {
581
+ return deny(
582
+ "action_unmapped",
583
+ "authorization",
584
+ 403,
585
+ "ENFORCE_ACTION_UNMAPPED",
586
+ "no exact canonical action is mapped for this tool",
587
+ stripped
588
+ );
589
+ }
590
+ const action = facts.action ?? facts.toolName;
591
+ let result;
592
+ try {
593
+ result = await decide(action);
594
+ } catch {
595
+ return deny(
596
+ "authority_source_unavailable",
597
+ "authorization",
598
+ 503,
599
+ "ENFORCE_DECIDE_UNAVAILABLE",
600
+ "the authority could not be reached; request cannot be safely decided",
601
+ stripped,
602
+ "not_safely_decided"
603
+ );
604
+ }
605
+ if (result.status === "allow") {
606
+ const trusted = [
607
+ ["x-mudraid-action-key", action],
608
+ ["x-mudraid-decision-id", result.decisionId ?? newDecisionId()]
609
+ ];
610
+ return {
611
+ outcome: "allow",
612
+ reasonCode: "authorized",
613
+ reasonTier: "authorization",
614
+ httpStatus: 200,
615
+ adapterCode: null,
616
+ message: "",
617
+ strippedReservedHeaders: stripped,
618
+ trustedContext: trusted
619
+ };
620
+ }
621
+ if (result.status === "deny") {
622
+ let reason2 = result.reason ?? DECIDE_DENY_DEFAULT_REASON;
623
+ if (NON_DENY_REASONS.has(reason2)) {
624
+ reason2 = DECIDE_DENY_DEFAULT_REASON;
625
+ }
626
+ return deny(
627
+ reason2,
628
+ "authorization",
629
+ 403,
630
+ "ENFORCE_DECISION_DENY",
631
+ "the authority denied this action",
632
+ stripped
633
+ );
634
+ }
635
+ const reason = DECIDE_UNAVAILABLE_REASONS[result.status] ?? "authority_source_unavailable";
636
+ return deny(
637
+ reason,
638
+ "authorization",
639
+ 503,
640
+ "ENFORCE_DECIDE_UNAVAILABLE",
641
+ "the authority could not be reached; request cannot be safely decided",
642
+ stripped,
643
+ "not_safely_decided"
644
+ );
645
+ }
646
+
647
+ // src/proxy.ts
648
+ function outgoingHeaders(inbound, decision) {
649
+ const stripped = new Set(decision.strippedReservedHeaders.map((h) => h.toLowerCase()));
650
+ const headers = {};
651
+ for (const [name, value] of Object.entries(inbound.headers)) {
652
+ if (value === void 0) {
653
+ continue;
654
+ }
655
+ if (stripped.has(name.toLowerCase())) {
656
+ continue;
657
+ }
658
+ headers[name] = value;
659
+ }
660
+ for (const [name, value] of decision.trustedContext) {
661
+ headers[name] = value;
662
+ }
663
+ return headers;
664
+ }
665
+ function denyBody(decision) {
666
+ const payload = {
667
+ error: "mudraid_enforced",
668
+ code: decision.adapterCode,
669
+ reason: decision.reasonCode,
670
+ message: decision.message
671
+ };
672
+ return Buffer.from(JSON.stringify(payload), "utf-8");
673
+ }
674
+ async function enforce(inbound, deps) {
675
+ inbound = { ...inbound, headers: Object.freeze({ ...inbound.headers }), body: Buffer.from(inbound.body) };
676
+ const snapshot = deps.authority?.bundle;
677
+ const config = deps.authority ? {
678
+ ...deps.config,
679
+ protectedSurface: true,
680
+ bundleActive: snapshot !== void 0,
681
+ actionMap: Object.fromEntries(Object.entries(snapshot?.actions ?? {}).map(([tool, action]) => [tool, String(action["action_key"])]))
682
+ } : deps.config;
683
+ const facts = buildFacts(inbound, config);
684
+ const decide = deps.authority ? () => deps.authority.decide(facts.toolName ?? "", {
685
+ presentedAuthorization: Object.entries(inbound.headers).find(([name]) => name.toLowerCase() === "authorization")?.[1] ?? "",
686
+ httpMethod: inbound.method,
687
+ path: inbound.path,
688
+ body: inbound.body,
689
+ contentType: Object.entries(inbound.headers).find(([name]) => name.toLowerCase() === "content-type")?.[1] ?? ""
690
+ }, snapshot) : deps.decide;
691
+ const decision = await evaluateV2(facts, decide);
692
+ if (!shouldForward(decision)) {
693
+ return {
694
+ status: decision.httpStatus,
695
+ headers: { "content-type": "application/json" },
696
+ body: denyBody(decision),
697
+ forwarded: false,
698
+ decision
699
+ };
700
+ }
701
+ const upstream = await deps.forwardUpstream({
702
+ method: inbound.method,
703
+ path: inbound.path,
704
+ headers: outgoingHeaders(inbound, decision),
705
+ body: inbound.body
706
+ });
707
+ return {
708
+ status: upstream.status,
709
+ headers: upstream.headers,
710
+ body: upstream.body,
711
+ forwarded: true,
712
+ decision
713
+ };
714
+ }
715
+
716
+ // src/upstream.ts
717
+ function httpUpstreamForwarder(baseUrl) {
718
+ const base = new URL(baseUrl);
719
+ if (!["http:", "https:"].includes(base.protocol) || base.username || base.password || base.search || base.hash || base.pathname !== "/") {
720
+ throw new Error("Upstream must be an HTTP origin without embedded credentials");
721
+ }
722
+ const hopHeaders = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host", "content-length"]);
723
+ const excludedHeaders = (entries) => {
724
+ const excluded = new Set(hopHeaders);
725
+ for (const [name, value] of entries) {
726
+ if (name.toLowerCase() === "connection") {
727
+ for (const token of value.split(",")) excluded.add(token.trim().toLowerCase());
728
+ }
729
+ }
730
+ return excluded;
731
+ };
732
+ return async (req) => {
733
+ if (!req.path.startsWith("/") || req.path.startsWith("//") || req.path.includes("\\")) throw new Error("Invalid upstream path");
734
+ const url = new URL(req.path, base);
735
+ if (url.origin !== base.origin) throw new Error("Upstream origin changed");
736
+ const controller = new AbortController();
737
+ const timer = setTimeout(() => controller.abort(), 15e3);
738
+ try {
739
+ const requestExcluded = excludedHeaders(Object.entries(req.headers));
740
+ const requestHeaders = Object.fromEntries(Object.entries(req.headers).filter(([name]) => !requestExcluded.has(name.toLowerCase())));
741
+ const init = { method: req.method, headers: requestHeaders, redirect: "error", signal: controller.signal };
742
+ if (req.method !== "GET" && req.method !== "HEAD" && req.body.length > 0) init.body = new Uint8Array(req.body);
743
+ const resp = await fetch(url, init);
744
+ const chunks = [];
745
+ let total = 0;
746
+ if (resp.body) {
747
+ const reader = resp.body.getReader();
748
+ try {
749
+ while (true) {
750
+ const chunk = await reader.read();
751
+ if (chunk.done) break;
752
+ total += chunk.value.byteLength;
753
+ if (total > 8 * 1024 * 1024) throw new Error("Upstream response exceeds limit");
754
+ chunks.push(chunk.value);
755
+ }
756
+ } finally {
757
+ await reader.cancel();
758
+ }
759
+ }
760
+ const headers = {};
761
+ const responseExcluded = excludedHeaders(resp.headers.entries());
762
+ resp.headers.forEach((value, key) => {
763
+ if (!responseExcluded.has(key.toLowerCase()) && key.toLowerCase() !== "content-encoding") headers[key] = value;
764
+ });
765
+ return { status: resp.status, headers, body: Buffer.concat(chunks) };
766
+ } finally {
767
+ clearTimeout(timer);
768
+ }
769
+ };
770
+ }
771
+
772
+ // src/server.ts
773
+ import { createServer } from "node:http";
774
+ async function readBody(req, maxBytes) {
775
+ return new Promise((resolve) => {
776
+ const chunks = [];
777
+ let total = 0;
778
+ let tooLarge = false;
779
+ req.on("data", (chunk) => {
780
+ total += chunk.length;
781
+ if (total > maxBytes) {
782
+ tooLarge = true;
783
+ return;
784
+ }
785
+ chunks.push(chunk);
786
+ });
787
+ req.on("end", () => {
788
+ resolve({ body: Buffer.concat(chunks), bodyTooLarge: tooLarge, bodyReadable: true });
789
+ });
790
+ req.on("error", () => {
791
+ resolve({ body: Buffer.alloc(0), bodyTooLarge: tooLarge, bodyReadable: false });
792
+ });
793
+ });
794
+ }
795
+ function singleValuedHeaders(req) {
796
+ const out = {};
797
+ for (const [name, value] of Object.entries(req.headers)) {
798
+ out[name] = Array.isArray(value) ? value.join(", ") : value;
799
+ }
800
+ return out;
801
+ }
802
+ function createSidecarServer(deps, maxBodyBytes) {
803
+ return createServer((req, res) => {
804
+ void (async () => {
805
+ const { body, bodyTooLarge, bodyReadable } = await readBody(req, maxBodyBytes);
806
+ const inbound = {
807
+ method: req.method ?? "GET",
808
+ path: req.url ?? "/",
809
+ headers: singleValuedHeaders(req),
810
+ body,
811
+ bodyTooLarge,
812
+ bodyReadable
813
+ };
814
+ const result = await enforce(inbound, deps);
815
+ res.writeHead(result.status, { ...result.headers });
816
+ res.end(result.body);
817
+ })().catch(() => {
818
+ if (!res.headersSent) {
819
+ res.writeHead(503, { "content-type": "application/json" });
820
+ }
821
+ res.end(JSON.stringify({ error: "mudraid_enforced", code: "ENFORCE_DECIDE_UNAVAILABLE" }));
822
+ });
823
+ });
824
+ }
825
+ function positiveInteger(raw, name, maximum = Number.MAX_SAFE_INTEGER) {
826
+ const value = Number(raw);
827
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
828
+ throw new Error(
829
+ `${name} must be a positive integer within its supported range`
830
+ );
831
+ }
832
+ return value;
833
+ }
834
+ function configFromEnv(env = process.env) {
835
+ const maxBodyBytes = positiveInteger(
836
+ env["MUDRAID_MAX_BODY_BYTES"] ?? String(DEFAULT_MAX_BODY_BYTES),
837
+ "MUDRAID_MAX_BODY_BYTES",
838
+ 8 * 1024 * 1024
839
+ );
840
+ const port = positiveInteger(env["PORT"] ?? "8000", "PORT", 65535);
841
+ const config = {
842
+ upstreamBaseUrl: env["MUDRAID_UPSTREAM_URL"] ?? "http://127.0.0.1:8080",
843
+ protectedSurface: env["MUDRAID_PROTECTED_SURFACE"] !== "false",
844
+ // Only authority verification can activate configuration; environment
845
+ // flags cannot substitute for a verified signed bundle.
846
+ bundleActive: false,
847
+ actionMap: {},
848
+ maxBodyBytes
849
+ };
850
+ return { config, port, maxBodyBytes };
851
+ }
852
+ function authorityFromEnv(env = process.env) {
853
+ const fields = ["MUDRAID_API_URL", "MUDRAID_ADAPTER_TOKEN", "MUDRAID_PLATFORM_ID", "MUDRAID_ENVIRONMENT", "MUDRAID_RESOURCE_URI"];
854
+ if (!fields.some((field) => env[field])) return void 0;
855
+ if (fields.some((field) => !env[field]?.trim())) throw new Error("Incomplete sidecar authority configuration");
856
+ return new HttpAuthority({ adapterType: "node_sidecar", apiBase: env["MUDRAID_API_URL"], adapterToken: env["MUDRAID_ADAPTER_TOKEN"], binding: {
857
+ platformId: env["MUDRAID_PLATFORM_ID"],
858
+ environment: env["MUDRAID_ENVIRONMENT"],
859
+ resource: env["MUDRAID_RESOURCE_URI"]
860
+ } });
861
+ }
862
+ function main() {
863
+ const { config, port, maxBodyBytes } = configFromEnv();
864
+ const authority = authorityFromEnv();
865
+ const decide = staticDecideClient({ status: "unconfigured" });
866
+ const deps = {
867
+ config,
868
+ decide,
869
+ ...authority ? { authority } : {},
870
+ forwardUpstream: httpUpstreamForwarder(config.upstreamBaseUrl)
871
+ };
872
+ const server = createSidecarServer(deps, maxBodyBytes);
873
+ server.requestTimeout = 3e4;
874
+ server.headersTimeout = 1e4;
875
+ const refresh = authority ? setInterval(() => {
876
+ void authority.refresh();
877
+ }, 3e4) : void 0;
878
+ refresh?.unref();
879
+ server.on("close", () => {
880
+ if (refresh) clearInterval(refresh);
881
+ });
882
+ if (authority) void authority.refresh();
883
+ server.listen(port, () => {
884
+ console.log(`mudraid-sidecar listening on :${port}`);
885
+ });
886
+ }
887
+
888
+ export {
889
+ DEFAULT_MAX_BODY_BYTES,
890
+ buildFacts,
891
+ staticDecideClient,
892
+ throwingDecideClient,
893
+ HttpAuthority,
894
+ enforce,
895
+ httpUpstreamForwarder,
896
+ createSidecarServer,
897
+ main
898
+ };