@auth51/mcp-proxy 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/dist/egress.js ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Server→RS egress: the proxy's Hop-B (non-embed parity, DESIGN-proxy-egress-hop-b).
3
+ *
4
+ * The MCP server's outbound RS calls are routed through this reverse-proxy (v1
5
+ * on-ramp: governed RS base URLs point here). For each request it:
6
+ * 1. maps the CONCRETE (method, path) to the registered endpoint TEMPLATE →
7
+ * the exact `a51:rs:*` scope the verifier will require (O6);
8
+ * 2. obtains a Hop-A intent token — the in-flight one the embed put in `_meta`
9
+ * (Case E), or one the proxy mints itself (Case T) — then EXCHANGES it
10
+ * (RFC 8693) for a downstream, DPoP-bound, RS-scoped token;
11
+ * 3. attaches `Authorization: Bearer <rs-token>` (+ DPoP) and forwards.
12
+ * An ungoverned path (no scope match, no catalog) is a transparent passthrough.
13
+ * Fail-open throughout: any hiccup forwards the request unchanged.
14
+ */
15
+ import * as http from "node:http";
16
+ import { deriveScope, InvalidScope } from "./scopes.js";
17
+ import * as pop from "./pop.js";
18
+ // ── Ingress→egress bridge (single-flight) ───────────────────────────────────
19
+ // The stdio ingress records the current tool call's intent token here (Case E,
20
+ // from `_meta`); the egress reads it for the exchange. One in-flight tool call
21
+ // at a time for a stdio server, so a single slot is correct (concurrent HTTP
22
+ // servers need a real correlator — see the design §4).
23
+ let _inFlightIntent = null;
24
+ export function setInFlightIntent(token) {
25
+ _inFlightIntent = token;
26
+ }
27
+ export function getInFlightIntent() {
28
+ return _inFlightIntent;
29
+ }
30
+ // ── Route matching: concrete path → registered template → scope ─────────────
31
+ function escapeRe(s) {
32
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
+ }
34
+ /** Compile a route template (`/repos/{id}`) to an anchored matcher. */
35
+ export function templateToRegex(template) {
36
+ let re = "";
37
+ let i = 0;
38
+ while (i < template.length) {
39
+ if (template[i] === "{") {
40
+ const end = template.indexOf("}", i);
41
+ if (end === -1) {
42
+ re += escapeRe(template.slice(i));
43
+ break;
44
+ }
45
+ re += "[^/]+"; // a single path segment
46
+ i = end + 1;
47
+ }
48
+ else {
49
+ const next = template.indexOf("{", i);
50
+ const lit = next === -1 ? template.slice(i) : template.slice(i, next);
51
+ re += escapeRe(lit);
52
+ i = next === -1 ? template.length : next;
53
+ }
54
+ }
55
+ return new RegExp("^" + re + "$");
56
+ }
57
+ /** Strip query + trailing slash, collapse `//` — the concrete request path. */
58
+ export function cleanPath(p) {
59
+ const q = p.indexOf("?");
60
+ if (q >= 0)
61
+ p = p.slice(0, q);
62
+ p = "/" + p.replace(/^\/+/, "").replace(/\/+$/, "");
63
+ return p.replace(/\/{2,}/g, "/") || "/";
64
+ }
65
+ function paramCount(template) {
66
+ return (template.match(/\{[^}]+\}/g) || []).length;
67
+ }
68
+ /**
69
+ * The registered endpoint whose (method, template) matches this concrete
70
+ * request, preferring the most specific (fewest params) on a tie — so a static
71
+ * `/files/latest` wins over `/files/{id}`.
72
+ */
73
+ export function matchEndpoint(method, path, endpoints) {
74
+ const m = method.toUpperCase();
75
+ const clean = cleanPath(path);
76
+ const hits = endpoints
77
+ .filter((e) => e.method.toUpperCase() === m && templateToRegex(e.pathTemplate).test(clean))
78
+ .sort((a, b) => paramCount(a.pathTemplate) - paramCount(b.pathTemplate));
79
+ return hits[0] ?? null;
80
+ }
81
+ /**
82
+ * The `a51:rs` scope to request for this request. Prefer the registered
83
+ * endpoint's stored scope (templated routes need the catalog to be right). If
84
+ * no catalog/match, derive from the concrete path — correct for parameterless
85
+ * routes, best-effort otherwise (a `{id}` route can't be recovered from one
86
+ * concrete path). Returns null only for an underivable request (bad method).
87
+ */
88
+ export function selectScope(method, path, endpoints, rsId) {
89
+ const ep = matchEndpoint(method, path, endpoints);
90
+ if (ep && ep.scope)
91
+ return { scope: ep.scope, matched: true };
92
+ try {
93
+ return { scope: deriveScope(rsId, method, cleanPath(path)), matched: false };
94
+ }
95
+ catch (e) {
96
+ if (e instanceof InvalidScope)
97
+ return null;
98
+ throw e;
99
+ }
100
+ }
101
+ /**
102
+ * Obtain the downstream RS token for a request: select the scope, get a subject
103
+ * intent token (in-flight from `_meta`, Case E; else mint it, Case T), and
104
+ * exchange it (RFC 8693) for a DPoP-bound RS-scoped token. Null ⇒ passthrough.
105
+ */
106
+ export async function governedExchange(authority, cfg, method, path, endpoints) {
107
+ const sel = selectScope(method, path, endpoints, cfg.rsId);
108
+ if (!sel)
109
+ return null;
110
+ // Case E: the embed's Hop-A token from _meta. Case T: mint one now (weaker,
111
+ // proxy-asserted identity) — same downstream path either way.
112
+ let subject = getInFlightIntent();
113
+ if (!subject) {
114
+ const minted = await authority.mintIntent({ scope: sel.scope });
115
+ subject = minted?.token ?? null;
116
+ }
117
+ if (!subject)
118
+ return null; // fail-open (no token → forward unchanged)
119
+ const cnfJkt = cfg.popEnabled === false ? undefined : pop.jkt();
120
+ const token = await authority.exchangeToken({
121
+ subjectToken: subject, audience: cfg.audience, scope: sel.scope, cnfJkt,
122
+ });
123
+ if (!token)
124
+ return null;
125
+ return { token, scope: sel.scope, matched: sel.matched, cnfJkt };
126
+ }
127
+ // ── Reverse-proxy server (v1 on-ramp) ───────────────────────────────────────
128
+ async function readBody(req) {
129
+ const chunks = [];
130
+ for await (const c of req)
131
+ chunks.push(c);
132
+ return Buffer.concat(chunks);
133
+ }
134
+ /**
135
+ * Start the egress reverse-proxy. Point the MCP server's governed RS base URL at
136
+ * `http://127.0.0.1:<port>`; the proxy governs each call and forwards to
137
+ * `cfg.targetBaseUrl`. `onAudit` (optional) receives one record per request.
138
+ */
139
+ export function createEgressServer(authority, cfg, onAudit) {
140
+ let endpoints = null;
141
+ const getEndpoints = async () => {
142
+ if (endpoints === null)
143
+ endpoints = await authority.fetchEndpoints(cfg.rsId);
144
+ return endpoints;
145
+ };
146
+ const server = http.createServer((req, res) => {
147
+ void (async () => {
148
+ const method = req.method || "GET";
149
+ const path = req.url || "/";
150
+ const body = await readBody(req);
151
+ const targetUrl = cfg.targetBaseUrl.replace(/\/$/, "") + path;
152
+ let result = null;
153
+ try {
154
+ result = await governedExchange(authority, cfg, method, path, await getEndpoints());
155
+ }
156
+ catch {
157
+ result = null; // fail-open
158
+ }
159
+ const headers = {};
160
+ for (const [k, v] of Object.entries(req.headers)) {
161
+ if (typeof v === "string" && k.toLowerCase() !== "host")
162
+ headers[k] = v;
163
+ }
164
+ if (result) {
165
+ headers["authorization"] = `Bearer ${result.token}`;
166
+ if (result.cnfJkt) {
167
+ try {
168
+ headers["dpop"] = pop.proof(method, targetUrl, { token: result.token });
169
+ }
170
+ catch { /* proof best-effort */ }
171
+ }
172
+ }
173
+ try {
174
+ const upstream = await fetch(targetUrl, {
175
+ method,
176
+ headers,
177
+ body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : body,
178
+ });
179
+ const buf = Buffer.from(await upstream.arrayBuffer());
180
+ res.statusCode = upstream.status;
181
+ upstream.headers.forEach((val, key) => {
182
+ if (key.toLowerCase() !== "content-encoding")
183
+ res.setHeader(key, val);
184
+ });
185
+ res.end(buf);
186
+ onAudit?.({
187
+ method, path, target: targetUrl, status: upstream.status,
188
+ governed: !!result, scope: result?.scope ?? null,
189
+ matched: result?.matched ?? null,
190
+ });
191
+ }
192
+ catch (err) {
193
+ res.statusCode = 502;
194
+ res.end(JSON.stringify({ error: `egress forward failed: ${err.message}` }));
195
+ }
196
+ })();
197
+ });
198
+ return {
199
+ server,
200
+ port: () => server.address().port,
201
+ close: () => new Promise((resolve) => server.close(() => resolve())),
202
+ };
203
+ }
204
+ //# sourceMappingURL=egress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"egress.js","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import { basename } from "node:path";
3
+ import { startProxy } from "./proxy.js";
4
+ const USAGE = `auth51-mcp-proxy — least-privilege enforcement + audit at the MCP tool boundary.
5
+
6
+ Usage:
7
+ auth51-mcp-proxy [options] -- <mcp-server-command> [args...]
8
+
9
+ Wrap any MCP server. Put this in front of an entry in your Claude Code / Cursor
10
+ MCP config, e.g.:
11
+
12
+ "filesystem": {
13
+ "command": "npx",
14
+ "args": ["-y", "@auth51/mcp-proxy", "--",
15
+ "npx", "-y", "@modelcontextprotocol/server-filesystem", "/work"]
16
+ }
17
+
18
+ Options:
19
+ --policy <path> Policy JSON file (default: built-in safe defaults)
20
+ --audit <path> Append-only JSONL audit log (default: ./.auth51/audit.jsonl)
21
+ --label <name> Friendly name for this server in logs/audit
22
+ --grant <a,b,c> Static capability grant: comma-separated allowed tool names
23
+ (also AUTH51_GRANT). tools/list is filtered to this set;
24
+ calls outside it are denied. Omit = allow all. When an
25
+ authority + --app-id is configured, the agent's server-side
26
+ grant supersedes this and honours its observe/enforce mode.
27
+ --app-id <name> App id (e.g. "Patchet"), also AUTH51_APP_ID. With an
28
+ authority, the proxy fetches this agent's grant and filters
29
+ the tool surface to it (Slice 2d).
30
+ --agent <id> Agent identity label or checksum to attribute decisions to
31
+ --authority <url> auth51 authority base URL (also via AUTH51_AUTHORITY_URL).
32
+ Requires AUTH51_CLIENT_ID + AUTH51_CLIENT_SECRET.
33
+ -h, --help Show this help
34
+
35
+ Authority env:
36
+ AUTH51_AUTHORITY_URL, AUTH51_CLIENT_ID, AUTH51_CLIENT_SECRET, AUTH51_AUDIENCE,
37
+ AUTH51_AGENT_ID, AUTH51_AGENT_CHECKSUM, AUTH51_APP_ID (the registered agent to mint intent tokens / fetch the grant for)
38
+
39
+ Everything works with NO account: local policy enforces, decisions are logged
40
+ locally. Connect an authority to back allowed calls with scoped intent tokens
41
+ and centralize the audit trail for a team / enterprise tenant.
42
+ `;
43
+ function parseArgs(argv) {
44
+ const sep = argv.indexOf("--");
45
+ if (sep === -1) {
46
+ process.stderr.write(USAGE);
47
+ process.stderr.write(`\nerror: missing "--" followed by the MCP server command.\n`);
48
+ process.exit(2);
49
+ }
50
+ const flags = argv.slice(0, sep);
51
+ const rest = argv.slice(sep + 1);
52
+ if (flags.includes("-h") || flags.includes("--help")) {
53
+ process.stdout.write(USAGE);
54
+ process.exit(0);
55
+ }
56
+ if (rest.length === 0) {
57
+ process.stderr.write(`error: no MCP server command after "--".\n`);
58
+ process.exit(2);
59
+ }
60
+ const getFlag = (name) => {
61
+ const i = flags.indexOf(name);
62
+ return i >= 0 && i + 1 < flags.length ? flags[i + 1] : undefined;
63
+ };
64
+ const command = rest[0];
65
+ const commandArgs = rest.slice(1);
66
+ const serverLabel = getFlag("--label") ?? basename(command);
67
+ const grantRaw = getFlag("--grant") ?? process.env.AUTH51_GRANT;
68
+ const grant = grantRaw
69
+ ? grantRaw.split(",").map((s) => s.trim()).filter(Boolean)
70
+ : undefined;
71
+ const authorityUrl = getFlag("--authority") ?? process.env.AUTH51_AUTHORITY_URL;
72
+ let authority;
73
+ if (authorityUrl) {
74
+ const clientId = process.env.AUTH51_CLIENT_ID;
75
+ const clientSecret = process.env.AUTH51_CLIENT_SECRET;
76
+ if (!clientId || !clientSecret) {
77
+ process.stderr.write(`[auth51] --authority set but AUTH51_CLIENT_ID / AUTH51_CLIENT_SECRET missing; ` +
78
+ `running local-only.\n`);
79
+ }
80
+ else {
81
+ authority = {
82
+ baseUrl: authorityUrl,
83
+ clientId,
84
+ clientSecret,
85
+ audience: process.env.AUTH51_AUDIENCE,
86
+ agentId: process.env.AUTH51_AGENT_ID,
87
+ agentChecksum: process.env.AUTH51_AGENT_CHECKSUM,
88
+ appId: getFlag("--app-id") ?? process.env.AUTH51_APP_ID,
89
+ };
90
+ }
91
+ }
92
+ // Server→RS egress governance (Hop-B). Opt-in: only when a target RS base URL
93
+ // is given. rsId/audience default to AUTH51_AUDIENCE (the governed RS).
94
+ let egress;
95
+ const egressTarget = getFlag("--egress-target") ?? process.env.AUTH51_EGRESS_TARGET;
96
+ if (egressTarget) {
97
+ const rsId = getFlag("--rs-id") ?? process.env.AUTH51_RS_ID ?? process.env.AUTH51_AUDIENCE;
98
+ if (!rsId) {
99
+ process.stderr.write("[auth51] --egress-target set but no --rs-id / AUTH51_RS_ID / AUTH51_AUDIENCE; skipping egress.\n");
100
+ }
101
+ else {
102
+ const portStr = getFlag("--egress-port") ?? process.env.AUTH51_EGRESS_PORT;
103
+ egress = {
104
+ rsId,
105
+ audience: process.env.AUTH51_AUDIENCE ?? rsId,
106
+ targetBaseUrl: egressTarget,
107
+ port: portStr ? Number(portStr) : undefined,
108
+ popEnabled: process.env.AUTH51_POP_ENABLED !== "0",
109
+ };
110
+ }
111
+ }
112
+ return {
113
+ command,
114
+ commandArgs,
115
+ policyPath: getFlag("--policy"),
116
+ auditPath: getFlag("--audit") ?? `${process.cwd()}/.auth51/audit.jsonl`,
117
+ serverLabel,
118
+ grant,
119
+ agent: getFlag("--agent") ?? process.env.AUTH51_AGENT,
120
+ authority,
121
+ egress,
122
+ };
123
+ }
124
+ startProxy(parseArgs(process.argv.slice(2)));
125
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /** The security-relevant surface of a tool, as seen in a tools/list result. */
2
+ export interface ToolDescriptor {
3
+ name: string;
4
+ description?: string;
5
+ inputSchema?: unknown;
6
+ }
7
+ export interface DriftFinding {
8
+ tool: string;
9
+ kind: "new" | "changed" | "removed";
10
+ oldFp?: string;
11
+ newFp?: string;
12
+ }
13
+ /**
14
+ * L0 — metadata fingerprint. Hash of the tool's *declared surface*: name +
15
+ * input schema + description, scoped to the server. Catches description/schema
16
+ * rug-pulls. NOTE (see design §2.2): L0 is blind to "same metadata, changed code"
17
+ * — that needs L1 (artifact) + L3 (behavioral). This is the cheap, necessary layer.
18
+ */
19
+ export declare function toolFingerprint(serverLabel: string, t: ToolDescriptor): string;
20
+ /**
21
+ * L1 — artifact fingerprint of the launched server (command + args). Any change
22
+ * to a local server's code requires a new artifact *iff* the version is pinned;
23
+ * v0 captures command+args. Richer resolution (lockfile integrity / image digest)
24
+ * is a later enhancement (design §2.2, L2 via the artifactory).
25
+ */
26
+ export declare function artifactFingerprint(command: string, args: string[]): string;
27
+ /**
28
+ * Tracks tool/artifact fingerprints against a persisted baseline so drift across
29
+ * sessions (and mid-session via tools/list_changed) is detected. In the full
30
+ * model the baseline is the *registered* surface (E1); here it's a local file,
31
+ * which already catches rug-pulls between runs.
32
+ */
33
+ export declare class IntegrityTracker {
34
+ private path;
35
+ private baseline;
36
+ constructor(path: string);
37
+ private persist;
38
+ /** L1: record/compare the launched-artifact fingerprint. Returns drift if changed. */
39
+ checkArtifact(serverLabel: string, command: string, args: string[]): DriftFinding | null;
40
+ /** L0: record/compare tool metadata fingerprints. Returns drift findings. */
41
+ checkTools(serverLabel: string, tools: ToolDescriptor[]): DriftFinding[];
42
+ }
@@ -0,0 +1,103 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ function sha256(s) {
5
+ return createHash("sha256").update(s).digest("hex");
6
+ }
7
+ /**
8
+ * L0 — metadata fingerprint. Hash of the tool's *declared surface*: name +
9
+ * input schema + description, scoped to the server. Catches description/schema
10
+ * rug-pulls. NOTE (see design §2.2): L0 is blind to "same metadata, changed code"
11
+ * — that needs L1 (artifact) + L3 (behavioral). This is the cheap, necessary layer.
12
+ */
13
+ export function toolFingerprint(serverLabel, t) {
14
+ const canonical = JSON.stringify({
15
+ s: serverLabel,
16
+ n: t.name,
17
+ d: t.description ?? "",
18
+ i: t.inputSchema ?? null,
19
+ });
20
+ return "l0:" + sha256(canonical);
21
+ }
22
+ /**
23
+ * L1 — artifact fingerprint of the launched server (command + args). Any change
24
+ * to a local server's code requires a new artifact *iff* the version is pinned;
25
+ * v0 captures command+args. Richer resolution (lockfile integrity / image digest)
26
+ * is a later enhancement (design §2.2, L2 via the artifactory).
27
+ */
28
+ export function artifactFingerprint(command, args) {
29
+ return "l1:" + sha256(JSON.stringify({ command, args }));
30
+ }
31
+ /**
32
+ * Tracks tool/artifact fingerprints against a persisted baseline so drift across
33
+ * sessions (and mid-session via tools/list_changed) is detected. In the full
34
+ * model the baseline is the *registered* surface (E1); here it's a local file,
35
+ * which already catches rug-pulls between runs.
36
+ */
37
+ export class IntegrityTracker {
38
+ path;
39
+ baseline = {};
40
+ constructor(path) {
41
+ this.path = path;
42
+ try {
43
+ this.baseline = JSON.parse(readFileSync(path, "utf8"));
44
+ }
45
+ catch {
46
+ /* first run — empty baseline */
47
+ }
48
+ }
49
+ persist() {
50
+ try {
51
+ mkdirSync(dirname(this.path), { recursive: true });
52
+ writeFileSync(this.path, JSON.stringify(this.baseline, null, 2), "utf8");
53
+ }
54
+ catch {
55
+ /* best-effort */
56
+ }
57
+ }
58
+ /** L1: record/compare the launched-artifact fingerprint. Returns drift if changed. */
59
+ checkArtifact(serverLabel, command, args) {
60
+ const fp = artifactFingerprint(command, args);
61
+ const entry = (this.baseline[serverLabel] ??= { tools: {} });
62
+ let finding = null;
63
+ if (entry.artifact && entry.artifact !== fp) {
64
+ finding = { tool: "(server artifact)", kind: "changed", oldFp: entry.artifact, newFp: fp };
65
+ }
66
+ entry.artifact = fp;
67
+ this.persist();
68
+ return finding;
69
+ }
70
+ /** L0: record/compare tool metadata fingerprints. Returns drift findings. */
71
+ checkTools(serverLabel, tools) {
72
+ const entry = (this.baseline[serverLabel] ??= { tools: {} });
73
+ const hadBaseline = Object.keys(entry.tools).length > 0;
74
+ const findings = [];
75
+ const seen = new Set();
76
+ for (const t of tools) {
77
+ if (!t || typeof t.name !== "string")
78
+ continue;
79
+ const fp = toolFingerprint(serverLabel, t);
80
+ seen.add(t.name);
81
+ const prev = entry.tools[t.name];
82
+ if (prev === undefined) {
83
+ if (hadBaseline)
84
+ findings.push({ tool: t.name, kind: "new", newFp: fp });
85
+ }
86
+ else if (prev !== fp) {
87
+ findings.push({ tool: t.name, kind: "changed", oldFp: prev, newFp: fp });
88
+ }
89
+ entry.tools[t.name] = fp;
90
+ }
91
+ if (hadBaseline) {
92
+ for (const name of Object.keys(entry.tools)) {
93
+ if (!seen.has(name)) {
94
+ findings.push({ tool: name, kind: "removed", oldFp: entry.tools[name] });
95
+ delete entry.tools[name];
96
+ }
97
+ }
98
+ }
99
+ this.persist();
100
+ return findings;
101
+ }
102
+ }
103
+ //# sourceMappingURL=integrity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrity.js","sourceRoot":"","sources":["../src/integrity.ts"],"names":[],"mappings":"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"}
package/dist/meta.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * MCP intent-token propagation — the `_meta` profile (Hop A).
3
+ *
4
+ * The minted intent token rides INSIDE the JSON-RPC call under
5
+ * `params._meta["io.auth51/intent"]` (NOT an HTTP header), so it binds to this
6
+ * exact tool + args and travels with the message to the server. This mirrors
7
+ * the embed's shape (auth51.mcp) byte-for-byte — `{ v, token, chain? }` — so a
8
+ * server can't tell whether the embed or this proxy produced it.
9
+ */
10
+ export declare const INTENT_META_KEY = "io.auth51/intent";
11
+ export declare const META_VERSION = 1;
12
+ /**
13
+ * Return `line` (a JSON-RPC message) with the intent token injected into
14
+ * `params._meta["io.auth51/intent"]`, preserving any existing `_meta` keys and
15
+ * params. On any parse/shape problem it returns the original line unchanged —
16
+ * injection must never break the call.
17
+ */
18
+ export declare function injectIntentMeta(line: string, token: string, chain?: Record<string, unknown>): string;
19
+ /** MCP-server side: pull the intent token out of an incoming call's `_meta`. */
20
+ export declare function extractIntent(meta: unknown): string | null;
package/dist/meta.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * MCP intent-token propagation — the `_meta` profile (Hop A).
3
+ *
4
+ * The minted intent token rides INSIDE the JSON-RPC call under
5
+ * `params._meta["io.auth51/intent"]` (NOT an HTTP header), so it binds to this
6
+ * exact tool + args and travels with the message to the server. This mirrors
7
+ * the embed's shape (auth51.mcp) byte-for-byte — `{ v, token, chain? }` — so a
8
+ * server can't tell whether the embed or this proxy produced it.
9
+ */
10
+ export const INTENT_META_KEY = "io.auth51/intent";
11
+ export const META_VERSION = 1;
12
+ /**
13
+ * Return `line` (a JSON-RPC message) with the intent token injected into
14
+ * `params._meta["io.auth51/intent"]`, preserving any existing `_meta` keys and
15
+ * params. On any parse/shape problem it returns the original line unchanged —
16
+ * injection must never break the call.
17
+ */
18
+ export function injectIntentMeta(line, token, chain) {
19
+ try {
20
+ const msg = JSON.parse(line);
21
+ if (!msg || typeof msg !== "object")
22
+ return line;
23
+ const params = msg.params && typeof msg.params === "object" ? msg.params : {};
24
+ const meta = params._meta && typeof params._meta === "object" ? params._meta : {};
25
+ const entry = { v: META_VERSION, token };
26
+ if (chain)
27
+ entry.chain = chain;
28
+ meta[INTENT_META_KEY] = entry;
29
+ params._meta = meta;
30
+ msg.params = params;
31
+ return JSON.stringify(msg);
32
+ }
33
+ catch {
34
+ return line;
35
+ }
36
+ }
37
+ /** MCP-server side: pull the intent token out of an incoming call's `_meta`. */
38
+ export function extractIntent(meta) {
39
+ if (!meta || typeof meta !== "object")
40
+ return null;
41
+ const entry = meta[INTENT_META_KEY];
42
+ if (entry && typeof entry === "object" && typeof entry.token === "string") {
43
+ return entry.token;
44
+ }
45
+ return null;
46
+ }
47
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../src/meta.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAClD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,KAAa,EACb,KAA+B;IAE/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAwB,CAAC;QACpD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACjD,MAAM,MAAM,GACV,GAAG,CAAC,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GACR,MAAM,CAAC,KAAK,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,MAAM,KAAK,GAA4B,EAAE,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;QAClE,IAAI,KAAK;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QAC/B,IAAI,CAAC,eAAe,CAAC,GAAG,KAAK,CAAC;QAC9B,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,KAAK,GAAI,IAA4B,CAAC,eAAe,CAAC,CAAC;IAC7D,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC1E,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Policy, ToolCall, Decision } from "./types.js";
2
+ /**
3
+ * The default policy if none is supplied. It is deliberately conservative for the
4
+ * coding-agent use case: allow by default, but block the classes of action that
5
+ * caused the real-world incidents (prod data destruction, recursive deletes,
6
+ * credential exfiltration, unscoped privilege escalation).
7
+ */
8
+ export declare const DEFAULT_POLICY: Policy;
9
+ export declare class PolicyEngine {
10
+ private policy;
11
+ constructor(policy: Policy);
12
+ static load(path?: string): PolicyEngine;
13
+ evaluate(call: ToolCall): Decision;
14
+ }