@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/README.md +146 -0
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +32 -0
- package/dist/audit.js.map +1 -0
- package/dist/authority.d.ts +85 -0
- package/dist/authority.js +266 -0
- package/dist/authority.js.map +1 -0
- package/dist/egress.d.ts +74 -0
- package/dist/egress.js +204 -0
- package/dist/egress.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity.d.ts +42 -0
- package/dist/integrity.js +103 -0
- package/dist/integrity.js.map +1 -0
- package/dist/meta.d.ts +20 -0
- package/dist/meta.js +47 -0
- package/dist/meta.js.map +1 -0
- package/dist/policy.d.ts +14 -0
- package/dist/policy.js +139 -0
- package/dist/policy.js.map +1 -0
- package/dist/pop.d.ts +21 -0
- package/dist/pop.js +76 -0
- package/dist/pop.js.map +1 -0
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.js +337 -0
- package/dist/proxy.js.map +1 -0
- package/dist/scopes.d.ts +43 -0
- package/dist/scopes.js +126 -0
- package/dist/scopes.js.map +1 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/examples/auth51.policy.json +41 -0
- package/examples/demo-drive.mjs +88 -0
- package/examples/mock-mcp-server.mjs +95 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @auth51/mcp-proxy
|
|
2
|
+
|
|
3
|
+
**Least-privilege enforcement + a real audit trail at the MCP tool boundary — for AI coding agents.**
|
|
4
|
+
|
|
5
|
+
Claude Code, Cursor, and Codex run with your full permissions, call MCP tools that touch repos / databases / infra / prod, and leave **no record** that your existing security tooling can see. A staging token with broad rights let one agent [delete a production database *and all backups* in 9 seconds](https://thehackernews.com/2026/03/how-ceros-gives-security-teams.html). Six disclosed exploits broke coding agents by stealing their *credentials*, not their models.
|
|
6
|
+
|
|
7
|
+
`@auth51/mcp-proxy` drops in front of **any** MCP server. It inspects every `tools/call`, **blocks destructive or out-of-scope actions before they reach the tool**, and writes an append-only audit log of everything the agent tried. No account required to start.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
coding agent ──► @auth51/mcp-proxy ──► your MCP server
|
|
11
|
+
(Claude/Cursor) ▲ allow / DENY + audit
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Quickstart (≈ 5 minutes)
|
|
17
|
+
|
|
18
|
+
### 1. See it block a destructive call (no install of your own needed)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone <this repo> && cd auth51-mcp-proxy
|
|
22
|
+
npm install && npm run build
|
|
23
|
+
node examples/demo-drive.mjs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
You'll see a safe `SELECT` pass through, and `DROP TABLE` / `DELETE`-without-`WHERE` **blocked at the boundary** — with the agent told exactly why, and an audit trail at `.auth51/demo-audit.jsonl`.
|
|
27
|
+
|
|
28
|
+
### 2. Guard your own MCP server
|
|
29
|
+
|
|
30
|
+
Wrap any existing MCP server entry in your client config. **Before:**
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
// .cursor/mcp.json (or Claude Code / Codex MCP config)
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"postgres": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@modelcontextprotocol/server-postgres", "postgres://..."]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**After** — insert the proxy and move the real command after `--`:
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"postgres": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": [
|
|
52
|
+
"-y", "@auth51/mcp-proxy",
|
|
53
|
+
"--policy", "./auth51.policy.json",
|
|
54
|
+
"--label", "postgres",
|
|
55
|
+
"--",
|
|
56
|
+
"npx", "-y", "@modelcontextprotocol/server-postgres", "postgres://..."
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Restart your agent. Destructive calls are now blocked, everything is audited. That's it.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Policy
|
|
68
|
+
|
|
69
|
+
A policy is JSON: `default` decision plus `rules`. Each rule matches a tool by name
|
|
70
|
+
(`tool` glob or `toolMatch` regex) and optionally by argument content
|
|
71
|
+
(`denyArgsMatch` — regexes against the call arguments; supports a leading `(?i)`
|
|
72
|
+
for case-insensitivity). First matching rule wins.
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"version": "1",
|
|
77
|
+
"default": "allow",
|
|
78
|
+
"rules": [
|
|
79
|
+
{
|
|
80
|
+
"id": "no-drop-or-truncate",
|
|
81
|
+
"tool": "*",
|
|
82
|
+
"denyArgsMatch": ["(?i)\\bdrop\\s+(table|database)\\b", "(?i)\\btruncate\\s+table\\b"],
|
|
83
|
+
"reason": "Irreversible data destruction requires human approval."
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
With **no `--policy`**, a conservative built-in default blocks destructive SQL,
|
|
90
|
+
recursive `rm -rf`, destructive tool names, and reads of `.aws/credentials` /
|
|
91
|
+
`.ssh` keys / `.env`. See [`examples/auth51.policy.json`](examples/auth51.policy.json).
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Connect to an auth51 authority (teams & enterprise)
|
|
96
|
+
|
|
97
|
+
Local mode enforces policy and audits to disk — enough for one developer. Point it
|
|
98
|
+
at an **auth51 authority** and every *allowed* call is backed by a **scoped,
|
|
99
|
+
short-lived intent token** (carrying the agent's identity + Proof-of-Possession),
|
|
100
|
+
and decisions are centralized for your org/tenant.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export AUTH51_AUTHORITY_URL=https://authority.auth51.com
|
|
104
|
+
export AUTH51_CLIENT_ID=... # your org/app API key
|
|
105
|
+
export AUTH51_CLIENT_SECRET=...
|
|
106
|
+
# add --authority to the proxy args (or set AUTH51_AUTHORITY_URL)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The authority path is **fail-open** in v0: local policy is always the binding
|
|
110
|
+
enforcement point, so an authority outage can never wedge your agent. A local
|
|
111
|
+
*deny* always wins.
|
|
112
|
+
|
|
113
|
+
- **Individual developers:** local mode, or the hosted authority (SaaS) — authority server only.
|
|
114
|
+
- **Enterprise:** the same authority self-hosted (on-prem container) or as a dedicated SaaS tenant.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## CLI
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
auth51-mcp-proxy [options] -- <mcp-server-command> [args...]
|
|
122
|
+
|
|
123
|
+
--policy <path> Policy JSON (default: built-in safe defaults)
|
|
124
|
+
--audit <path> Audit log JSONL (default: ./.auth51/audit.jsonl)
|
|
125
|
+
--label <name> Friendly name for this server in logs/audit
|
|
126
|
+
--agent <id> Agent identity to attribute decisions to
|
|
127
|
+
--authority <url> auth51 authority base URL (or AUTH51_AUTHORITY_URL)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
- Transparent stdio MCP proxy: spawns your server, relays JSON-RPC line-for-line.
|
|
133
|
+
- Only `tools/call` is intercepted; everything else passes through untouched.
|
|
134
|
+
- On `tools/call`: evaluate policy → **allow** (forward, zero added latency) or
|
|
135
|
+
**deny** (return an MCP `isError` result explaining the block; never forward).
|
|
136
|
+
- Every decision is appended to the audit log; with an authority, allowed calls
|
|
137
|
+
also mint a scoped intent token and record the decision centrally.
|
|
138
|
+
|
|
139
|
+
## Status
|
|
140
|
+
|
|
141
|
+
v0. Stdio transport (the dominant local coding-agent case). HTTP/SSE transport,
|
|
142
|
+
per-tool scope grants, and the agent-runtime identity surface are next.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
Apache-2.0
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuditEntry } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Append-only local audit log (JSONL). Every tool-call decision is recorded here
|
|
4
|
+
* regardless of whether an authority is connected, so an individual developer can
|
|
5
|
+
* see exactly what their coding agent tried to do — the trail that today's
|
|
6
|
+
* locally-running agents leave nowhere.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Auditor {
|
|
9
|
+
private path;
|
|
10
|
+
constructor(path: string);
|
|
11
|
+
record(entry: AuditEntry): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function nowIso(): string;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Append-only local audit log (JSONL). Every tool-call decision is recorded here
|
|
5
|
+
* regardless of whether an authority is connected, so an individual developer can
|
|
6
|
+
* see exactly what their coding agent tried to do — the trail that today's
|
|
7
|
+
* locally-running agents leave nowhere.
|
|
8
|
+
*/
|
|
9
|
+
export class Auditor {
|
|
10
|
+
path;
|
|
11
|
+
constructor(path) {
|
|
12
|
+
this.path = path;
|
|
13
|
+
try {
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* best-effort */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
record(entry) {
|
|
21
|
+
try {
|
|
22
|
+
appendFileSync(this.path, JSON.stringify(entry) + "\n", "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* never let auditing crash the proxy */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function nowIso() {
|
|
30
|
+
return new Date().toISOString();
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC;;;;;GAKG;AACH,MAAM,OAAO,OAAO;IACV,IAAI,CAAS;IAErB,YAAY,IAAY;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC;YACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,KAAiB;QACtB,IAAI,CAAC;YACH,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;QAClE,CAAC;QAAC,MAAM,CAAC;YACP,wCAAwC;QAC1C,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AuthorityConfig, GrantInfo, RSEndpoint } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Optional integration with the auth51 authority server.
|
|
4
|
+
*
|
|
5
|
+
* Without it, the proxy enforces policy locally and writes a local audit trail —
|
|
6
|
+
* enough for an individual developer to try in minutes. With it, allowed calls are
|
|
7
|
+
* backed by a *scoped, short-lived intent token* minted by the authority (carrying
|
|
8
|
+
* the agent identity + Proof-of-Possession), and decisions are centralized for an
|
|
9
|
+
* org / enterprise tenant.
|
|
10
|
+
*
|
|
11
|
+
* The authority path is best-effort and FAIL-OPEN in v0: local policy remains the
|
|
12
|
+
* binding enforcement point, so an authority outage cannot wedge a developer's
|
|
13
|
+
* agent. Local-deny always wins regardless of the authority.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AuthorityClient {
|
|
16
|
+
private cfg;
|
|
17
|
+
private accessTokens;
|
|
18
|
+
private warned;
|
|
19
|
+
constructor(cfg: AuthorityConfig);
|
|
20
|
+
private base;
|
|
21
|
+
private warnOnce;
|
|
22
|
+
/**
|
|
23
|
+
* Get a client_credentials token carrying `generate:intent-token` (permission
|
|
24
|
+
* to mint) PLUS the scopes being delegated. The authority enforces that an
|
|
25
|
+
* intent token's `requested_scopes` are a subset of the caller's OAuth-token
|
|
26
|
+
* scopes — you can't delegate more than you hold — so the OAuth token must
|
|
27
|
+
* carry both. Cached per distinct scope-set.
|
|
28
|
+
*/
|
|
29
|
+
private accessTokenFor;
|
|
30
|
+
/**
|
|
31
|
+
* Fetch this agent's current grant envelope from the authority (Slice 2d):
|
|
32
|
+
* GET /v1/grants/{appId}/{agentId}. The proxy uses it to filter tools/list to
|
|
33
|
+
* the granted surface and to deny out-of-grant calls when mode==="enforce".
|
|
34
|
+
*
|
|
35
|
+
* Fail-open: returns null on any error / 404 (no grant registered) / missing
|
|
36
|
+
* config, so a developer without an authority-side grant keeps working on
|
|
37
|
+
* local policy alone. Local policy + the authority's own mint-time enforcement
|
|
38
|
+
* remain the binding controls regardless.
|
|
39
|
+
*/
|
|
40
|
+
fetchGrant(): Promise<GrantInfo | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Mint a scoped intent token for an allowed call (authority TokenRequest shape:
|
|
43
|
+
* grant_type="agent_checksum" + agent_id + computed_checksum + requested_scopes).
|
|
44
|
+
* The minted intent token is returned in the response's `access_token`.
|
|
45
|
+
* Returns the token + its jti for the audit chain, or null if the authority is
|
|
46
|
+
* unreachable / not provisioned (fail-open).
|
|
47
|
+
*/
|
|
48
|
+
mintIntent(opts: {
|
|
49
|
+
scope: string;
|
|
50
|
+
workflowId?: string;
|
|
51
|
+
workflowStep?: Record<string, unknown>;
|
|
52
|
+
delegationContext?: Record<string, unknown>;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
jti: string;
|
|
55
|
+
token: string;
|
|
56
|
+
} | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch a resource server's registered endpoint catalog (O6):
|
|
59
|
+
* GET /v1/resource-servers/{rsId}/endpoints → `[{method, path_template, scope,
|
|
60
|
+
* risk_tier}]`. The egress uses it to map a CONCRETE request (`GET /repos/42`)
|
|
61
|
+
* to its route TEMPLATE (`/repos/{id}`) and thus the exact scope the verifier
|
|
62
|
+
* will require. Returns [] on any error / missing catalog (the egress then
|
|
63
|
+
* falls back to deriving the scope from the concrete path).
|
|
64
|
+
*/
|
|
65
|
+
fetchEndpoints(rsId: string): Promise<RSEndpoint[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Hop B (RFC 8693): exchange a Hop-A intent token (subject) for a downstream
|
|
68
|
+
* token scoped + audienced for the resource server the MCP server is about to
|
|
69
|
+
* call. Used at the server→RS egress: the proxy derives the endpoint's
|
|
70
|
+
* `a51:rs:*` scope from the concrete method+URL (see scopes.ts) and requests
|
|
71
|
+
* exactly it here. The proxy authenticates as ITSELF (its OAuth client) — that
|
|
72
|
+
* is the verifiable identity on this hop; the agent identity rode in the
|
|
73
|
+
* subject token. `cnfJkt` binds the result to the proxy's DPoP key (RFC 9449),
|
|
74
|
+
* so the downstream RS token is sender-constrained to this proxy.
|
|
75
|
+
*
|
|
76
|
+
* Fail-open like the rest: returns null on any error, so an authority hiccup
|
|
77
|
+
* degrades to forwarding without the RS token rather than wedging the call.
|
|
78
|
+
*/
|
|
79
|
+
exchangeToken(opts: {
|
|
80
|
+
subjectToken: string;
|
|
81
|
+
audience: string;
|
|
82
|
+
scope?: string;
|
|
83
|
+
cnfJkt?: string;
|
|
84
|
+
}): Promise<string | null>;
|
|
85
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional integration with the auth51 authority server.
|
|
3
|
+
*
|
|
4
|
+
* Without it, the proxy enforces policy locally and writes a local audit trail —
|
|
5
|
+
* enough for an individual developer to try in minutes. With it, allowed calls are
|
|
6
|
+
* backed by a *scoped, short-lived intent token* minted by the authority (carrying
|
|
7
|
+
* the agent identity + Proof-of-Possession), and decisions are centralized for an
|
|
8
|
+
* org / enterprise tenant.
|
|
9
|
+
*
|
|
10
|
+
* The authority path is best-effort and FAIL-OPEN in v0: local policy remains the
|
|
11
|
+
* binding enforcement point, so an authority outage cannot wedge a developer's
|
|
12
|
+
* agent. Local-deny always wins regardless of the authority.
|
|
13
|
+
*/
|
|
14
|
+
export class AuthorityClient {
|
|
15
|
+
cfg;
|
|
16
|
+
accessTokens = new Map();
|
|
17
|
+
warned = false;
|
|
18
|
+
constructor(cfg) {
|
|
19
|
+
this.cfg = cfg;
|
|
20
|
+
}
|
|
21
|
+
base(path) {
|
|
22
|
+
return this.cfg.baseUrl.replace(/\/$/, "") + path;
|
|
23
|
+
}
|
|
24
|
+
warnOnce(msg) {
|
|
25
|
+
if (this.warned)
|
|
26
|
+
return;
|
|
27
|
+
this.warned = true;
|
|
28
|
+
process.stderr.write(`[auth51] authority unavailable, continuing on local policy: ${msg}\n`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get a client_credentials token carrying `generate:intent-token` (permission
|
|
32
|
+
* to mint) PLUS the scopes being delegated. The authority enforces that an
|
|
33
|
+
* intent token's `requested_scopes` are a subset of the caller's OAuth-token
|
|
34
|
+
* scopes — you can't delegate more than you hold — so the OAuth token must
|
|
35
|
+
* carry both. Cached per distinct scope-set.
|
|
36
|
+
*/
|
|
37
|
+
async accessTokenFor(scope) {
|
|
38
|
+
const full = ["generate:intent-token", ...scope.split(/\s+/).filter(Boolean)].join(" ");
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const cached = this.accessTokens.get(full);
|
|
41
|
+
if (cached && now < cached.exp - 5000)
|
|
42
|
+
return cached.token;
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(this.base("/v1/oauth/token"), {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
47
|
+
body: new URLSearchParams({
|
|
48
|
+
grant_type: "client_credentials",
|
|
49
|
+
client_id: this.cfg.clientId,
|
|
50
|
+
client_secret: this.cfg.clientSecret,
|
|
51
|
+
scope: full,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
this.warnOnce(`token endpoint returned ${res.status}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const data = (await res.json());
|
|
59
|
+
if (!data.access_token) {
|
|
60
|
+
this.warnOnce("token endpoint returned no access_token");
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
this.accessTokens.set(full, { token: data.access_token, exp: now + (data.expires_in ?? 300) * 1000 });
|
|
64
|
+
return data.access_token;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
this.warnOnce(err.message);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetch this agent's current grant envelope from the authority (Slice 2d):
|
|
73
|
+
* GET /v1/grants/{appId}/{agentId}. The proxy uses it to filter tools/list to
|
|
74
|
+
* the granted surface and to deny out-of-grant calls when mode==="enforce".
|
|
75
|
+
*
|
|
76
|
+
* Fail-open: returns null on any error / 404 (no grant registered) / missing
|
|
77
|
+
* config, so a developer without an authority-side grant keeps working on
|
|
78
|
+
* local policy alone. Local policy + the authority's own mint-time enforcement
|
|
79
|
+
* remain the binding controls regardless.
|
|
80
|
+
*/
|
|
81
|
+
async fetchGrant() {
|
|
82
|
+
if (!this.cfg.appId || !this.cfg.agentId) {
|
|
83
|
+
this.warnOnce("no appId/agentId configured; cannot fetch agent grant");
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const token = await this.accessTokenFor("read:agents");
|
|
87
|
+
if (!token)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
const path = `/v1/grants/${encodeURIComponent(this.cfg.appId)}/${encodeURIComponent(this.cfg.agentId)}`;
|
|
91
|
+
const res = await fetch(this.base(path), {
|
|
92
|
+
headers: { authorization: `Bearer ${token}` },
|
|
93
|
+
});
|
|
94
|
+
if (res.status === 404)
|
|
95
|
+
return null; // no grant registered → fail-open
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
this.warnOnce(`grant fetch returned ${res.status}`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const d = (await res.json());
|
|
101
|
+
return {
|
|
102
|
+
allowedScopes: d.allowed_scopes ?? [],
|
|
103
|
+
stepUpScopes: d.step_up_scopes ?? [],
|
|
104
|
+
mode: d.mode === "observe" ? "observe" : "enforce",
|
|
105
|
+
version: d.version ?? 0,
|
|
106
|
+
source: d.source ?? "unknown",
|
|
107
|
+
expiresAt: d.expires_at ?? null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
this.warnOnce(err.message);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Mint a scoped intent token for an allowed call (authority TokenRequest shape:
|
|
117
|
+
* grant_type="agent_checksum" + agent_id + computed_checksum + requested_scopes).
|
|
118
|
+
* The minted intent token is returned in the response's `access_token`.
|
|
119
|
+
* Returns the token + its jti for the audit chain, or null if the authority is
|
|
120
|
+
* unreachable / not provisioned (fail-open).
|
|
121
|
+
*/
|
|
122
|
+
async mintIntent(opts) {
|
|
123
|
+
const token = await this.accessTokenFor(opts.scope);
|
|
124
|
+
if (!token)
|
|
125
|
+
return null;
|
|
126
|
+
if (!this.cfg.agentId || !this.cfg.agentChecksum) {
|
|
127
|
+
this.warnOnce("no agentId/agentChecksum configured; cannot mint intent token");
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(this.base("/v1/intent/token"), {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
"content-type": "application/json",
|
|
135
|
+
authorization: `Bearer ${token}`,
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
grant_type: "agent_checksum",
|
|
139
|
+
agent_id: this.cfg.agentId,
|
|
140
|
+
computed_checksum: this.cfg.agentChecksum,
|
|
141
|
+
requested_scopes: opts.scope ? opts.scope.split(/\s+/).filter(Boolean) : [],
|
|
142
|
+
audience: this.cfg.audience ?? "",
|
|
143
|
+
workflow_id: opts.workflowId,
|
|
144
|
+
workflow_step: opts.workflowStep,
|
|
145
|
+
delegation_context: opts.delegationContext,
|
|
146
|
+
// The MCP proxy does NOT use the authority's pre-registered workflow-DAG
|
|
147
|
+
// authorization (that 403s on any unregistered workflow_id). Our workflow
|
|
148
|
+
// provenance is the prev_jti hash-chain (§4.3); workflow_id/step ride along
|
|
149
|
+
// for the audit + token claims, informational only. Flip to true later if
|
|
150
|
+
// we register workflows in the authority (calibration model, Slice 2+).
|
|
151
|
+
workflow_enabled: false,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
this.warnOnce(`intent mint returned ${res.status}`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const data = (await res.json());
|
|
159
|
+
const intentToken = data.access_token;
|
|
160
|
+
if (!intentToken)
|
|
161
|
+
return null;
|
|
162
|
+
const jti = parseJti(intentToken) ?? "unknown";
|
|
163
|
+
return { jti, token: intentToken };
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
this.warnOnce(err.message);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Fetch a resource server's registered endpoint catalog (O6):
|
|
172
|
+
* GET /v1/resource-servers/{rsId}/endpoints → `[{method, path_template, scope,
|
|
173
|
+
* risk_tier}]`. The egress uses it to map a CONCRETE request (`GET /repos/42`)
|
|
174
|
+
* to its route TEMPLATE (`/repos/{id}`) and thus the exact scope the verifier
|
|
175
|
+
* will require. Returns [] on any error / missing catalog (the egress then
|
|
176
|
+
* falls back to deriving the scope from the concrete path).
|
|
177
|
+
*/
|
|
178
|
+
async fetchEndpoints(rsId) {
|
|
179
|
+
const token = await this.accessTokenFor("read:agents");
|
|
180
|
+
if (!token)
|
|
181
|
+
return [];
|
|
182
|
+
try {
|
|
183
|
+
const path = `/v1/resource-servers/${encodeURIComponent(rsId)}/endpoints`;
|
|
184
|
+
const res = await fetch(this.base(path), {
|
|
185
|
+
headers: { authorization: `Bearer ${token}` },
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok)
|
|
188
|
+
return [];
|
|
189
|
+
const rows = (await res.json());
|
|
190
|
+
return rows.map((r) => ({
|
|
191
|
+
method: (r.method ?? "GET").toUpperCase(),
|
|
192
|
+
pathTemplate: r.path_template ?? "/",
|
|
193
|
+
scope: r.scope ?? "",
|
|
194
|
+
riskTier: r.risk_tier ?? "low",
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
this.warnOnce(err.message);
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Hop B (RFC 8693): exchange a Hop-A intent token (subject) for a downstream
|
|
204
|
+
* token scoped + audienced for the resource server the MCP server is about to
|
|
205
|
+
* call. Used at the server→RS egress: the proxy derives the endpoint's
|
|
206
|
+
* `a51:rs:*` scope from the concrete method+URL (see scopes.ts) and requests
|
|
207
|
+
* exactly it here. The proxy authenticates as ITSELF (its OAuth client) — that
|
|
208
|
+
* is the verifiable identity on this hop; the agent identity rode in the
|
|
209
|
+
* subject token. `cnfJkt` binds the result to the proxy's DPoP key (RFC 9449),
|
|
210
|
+
* so the downstream RS token is sender-constrained to this proxy.
|
|
211
|
+
*
|
|
212
|
+
* Fail-open like the rest: returns null on any error, so an authority hiccup
|
|
213
|
+
* degrades to forwarding without the RS token rather than wedging the call.
|
|
214
|
+
*/
|
|
215
|
+
async exchangeToken(opts) {
|
|
216
|
+
try {
|
|
217
|
+
const body = {
|
|
218
|
+
grant_type: TOKEN_EXCHANGE_GRANT,
|
|
219
|
+
subject_token: opts.subjectToken,
|
|
220
|
+
subject_token_type: JWT_TOKEN_TYPE,
|
|
221
|
+
audience: opts.audience,
|
|
222
|
+
};
|
|
223
|
+
if (opts.scope)
|
|
224
|
+
body.scope = opts.scope;
|
|
225
|
+
// Authenticate the exchanging party (the proxy) when credentials exist —
|
|
226
|
+
// this is the proxy's own verifiable identity on the Hop-B exchange.
|
|
227
|
+
if (this.cfg.clientId)
|
|
228
|
+
body.client_id = this.cfg.clientId;
|
|
229
|
+
if (this.cfg.clientSecret)
|
|
230
|
+
body.client_secret = this.cfg.clientSecret;
|
|
231
|
+
if (opts.cnfJkt)
|
|
232
|
+
body.cnf_jkt = opts.cnfJkt;
|
|
233
|
+
const res = await fetch(this.base("/v1/oauth/token"), {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
236
|
+
body: new URLSearchParams(body),
|
|
237
|
+
});
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
this.warnOnce(`token exchange returned ${res.status}`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const data = (await res.json());
|
|
243
|
+
return data.access_token ?? null;
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
this.warnOnce(err.message);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const TOKEN_EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
252
|
+
const JWT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt";
|
|
253
|
+
/** Extract the jti claim from a JWT without verifying it (audit display only). */
|
|
254
|
+
function parseJti(jwt) {
|
|
255
|
+
const parts = jwt.split(".");
|
|
256
|
+
if (parts.length < 2)
|
|
257
|
+
return null;
|
|
258
|
+
try {
|
|
259
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
260
|
+
return typeof payload.jti === "string" ? payload.jti : null;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=authority.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authority.js","sourceRoot":"","sources":["../src/authority.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,eAAe;IAClB,GAAG,CAAkB;IACrB,YAAY,GAAG,IAAI,GAAG,EAA0C,CAAC;IACjE,MAAM,GAAG,KAAK,CAAC;IAEvB,YAAY,GAAoB;QAC9B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAEO,IAAI,CAAC,IAAY;QACvB,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC;IACpD,CAAC;IAEO,QAAQ,CAAC,GAAW;QAC1B,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+DAA+D,GAAG,IAAI,CAAC,CAAC;IAC/F,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,cAAc,CAAC,KAAa;QACxC,MAAM,IAAI,GAAG,CAAC,uBAAuB,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,GAAG,GAAG,IAAI;YAAE,OAAO,MAAM,CAAC,KAAK,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE;gBACpD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;gBAChE,IAAI,EAAE,IAAI,eAAe,CAAC;oBACxB,UAAU,EAAE,oBAAoB;oBAChC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ;oBAC5B,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY;oBACpC,KAAK,EAAE,IAAI;iBACZ,CAAC;aACH,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,IAAI,CAAC,QAAQ,CAAC,2BAA2B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;gBACvD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmD,CAAC;YAClF,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;gBACvB,IAAI,CAAC,QAAQ,CAAC,yCAAyC,CAAC,CAAC;gBACzD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;YACtG,OAAO,IAAI,CAAC,YAAY,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;YACtC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YACzC,IAAI,CAAC,QAAQ,CAAC,uDAAuD,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,cAAc,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACxG,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;gBACvC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;aAC9C,CAAC,CAAC;YACH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAAE,OAAO,IAAI,CAAC,CAAC,kCAAkC;YACvE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,IAAI,CAAC,QAAQ,CAAC,wBAAwB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;gBACpD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAO1B,CAAC;YACF,OAAO;gBACL,aAAa,EAAE,CAAC,CAAC,cAAc,IAAI,EAAE;gBACrC,YAAY,EAAE,CAAC,CAAC,cAAc,IAAI,EAAE;gBACpC,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;gBAClD,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,CAAC;gBACvB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;gBAC7B,SAAS,EAAE,CAAC,CAAC,UAAU,IAAI,IAAI;aAChC,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;YACtC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,UAAU,CAAC,IAKhB;QACC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpD,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;YACjD,IAAI,CAAC,QAAQ,CAAC,+DAA+D,CAAC,CAAC;YAC/E,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,EAAE;gBACrD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,aAAa,EAAE,UAAU,KAAK,EAAE;iBACjC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,UAAU,EAAE,gBAAgB;oBAC5B,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO;oBAC1B,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa;oBACzC,gBAAgB,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE;oBAC3E,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE;oBACjC,WAAW,EAAE,IAAI,CAAC,UAAU;oBAC5B,aAAa,EAAE,IAAI,CAAC,YAAY;oBAChC,kBAAkB,EAAE,IAAI,CAAC,iBAAiB;oBAC1C,yEAAyE;oBACzE,0EAA0E;oBAC1E,4EAA4E;oBAC5E,0EAA0E;oBAC1E,wEAAwE;oBACxE,gBAAgB,EAAE,KAAK;iBACxB,CAAC;aACH,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,IAAI,CAAC,QAAQ,CAAC,wBAAwB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;gBACpD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA8B,CAAC;YAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC;YACtC,IAAI,CAAC,WAAW;gBAAE,OAAO,IAAI,CAAC;YAC9B,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC;YAC/C,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QACrC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;YACtC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,cAAc,CAAC,IAAY;QAC/B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,wBAAwB,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC;YAC1E,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;gBACvC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;aAC9C,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,OAAO,EAAE,CAAC;YACvB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAK5B,CAAC;YACH,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACtB,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE;gBACzC,YAAY,EAAE,CAAC,CAAC,aAAa,IAAI,GAAG;gBACpC,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;gBACpB,QAAQ,EAAE,CAAC,CAAC,SAAS,IAAI,KAAK;aAC/B,CAAC,CAAC,CAAC;QACN,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;YACtC,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,aAAa,CAAC,IAKnB;QACC,IAAI,CAAC;YACH,MAAM,IAAI,GAA2B;gBACnC,UAAU,EAAE,oBAAoB;gBAChC,aAAa,EAAE,IAAI,CAAC,YAAY;gBAChC,kBAAkB,EAAE,cAAc;gBAClC,QAAQ,EAAE,IAAI,CAAC,QAAQ;aACxB,CAAC;YACF,IAAI,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YACxC,yEAAyE;YACzE,qEAAqE;YACrE,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ;gBAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC1D,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY;gBAAE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC;YACtE,IAAI,IAAI,CAAC,MAAM;gBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC;YAC5C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE;gBACpD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;gBAChE,IAAI,EAAE,IAAI,eAAe,CAAC,IAAI,CAAC;aAChC,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,IAAI,CAAC,QAAQ,CAAC,2BAA2B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;gBACvD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA8B,CAAC;YAC7D,OAAO,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;YACtC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAED,MAAM,oBAAoB,GAAG,iDAAiD,CAAC;AAC/E,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAE9D,kFAAkF;AAClF,SAAS,QAAQ,CAAC,GAAW;IAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAChF,OAAO,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/dist/egress.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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 type { AuthorityClient } from "./authority.js";
|
|
17
|
+
import type { RSEndpoint } from "./types.js";
|
|
18
|
+
export interface EgressConfig {
|
|
19
|
+
/** The governed RS's id — namespaces its scopes (usually === its audience). */
|
|
20
|
+
rsId: string;
|
|
21
|
+
/** Token audience for the exchange (often === rsId). */
|
|
22
|
+
audience: string;
|
|
23
|
+
/** Where to forward the (now-governed) request — the real RS base URL. */
|
|
24
|
+
targetBaseUrl: string;
|
|
25
|
+
/** DPoP-bind the downstream token to the proxy key. Default true. */
|
|
26
|
+
popEnabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function setInFlightIntent(token: string | null): void;
|
|
29
|
+
export declare function getInFlightIntent(): string | null;
|
|
30
|
+
/** Compile a route template (`/repos/{id}`) to an anchored matcher. */
|
|
31
|
+
export declare function templateToRegex(template: string): RegExp;
|
|
32
|
+
/** Strip query + trailing slash, collapse `//` — the concrete request path. */
|
|
33
|
+
export declare function cleanPath(p: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* The registered endpoint whose (method, template) matches this concrete
|
|
36
|
+
* request, preferring the most specific (fewest params) on a tie — so a static
|
|
37
|
+
* `/files/latest` wins over `/files/{id}`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function matchEndpoint(method: string, path: string, endpoints: RSEndpoint[]): RSEndpoint | null;
|
|
40
|
+
export interface ScopeSelection {
|
|
41
|
+
scope: string;
|
|
42
|
+
matched: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The `a51:rs` scope to request for this request. Prefer the registered
|
|
46
|
+
* endpoint's stored scope (templated routes need the catalog to be right). If
|
|
47
|
+
* no catalog/match, derive from the concrete path — correct for parameterless
|
|
48
|
+
* routes, best-effort otherwise (a `{id}` route can't be recovered from one
|
|
49
|
+
* concrete path). Returns null only for an underivable request (bad method).
|
|
50
|
+
*/
|
|
51
|
+
export declare function selectScope(method: string, path: string, endpoints: RSEndpoint[], rsId: string): ScopeSelection | null;
|
|
52
|
+
export interface ExchangeResult {
|
|
53
|
+
token: string;
|
|
54
|
+
scope: string;
|
|
55
|
+
matched: boolean;
|
|
56
|
+
cnfJkt?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Obtain the downstream RS token for a request: select the scope, get a subject
|
|
60
|
+
* intent token (in-flight from `_meta`, Case E; else mint it, Case T), and
|
|
61
|
+
* exchange it (RFC 8693) for a DPoP-bound RS-scoped token. Null ⇒ passthrough.
|
|
62
|
+
*/
|
|
63
|
+
export declare function governedExchange(authority: AuthorityClient, cfg: EgressConfig, method: string, path: string, endpoints: RSEndpoint[]): Promise<ExchangeResult | null>;
|
|
64
|
+
export interface EgressServer {
|
|
65
|
+
server: http.Server;
|
|
66
|
+
port: () => number;
|
|
67
|
+
close: () => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Start the egress reverse-proxy. Point the MCP server's governed RS base URL at
|
|
71
|
+
* `http://127.0.0.1:<port>`; the proxy governs each call and forwards to
|
|
72
|
+
* `cfg.targetBaseUrl`. `onAudit` (optional) receives one record per request.
|
|
73
|
+
*/
|
|
74
|
+
export declare function createEgressServer(authority: AuthorityClient, cfg: EgressConfig, onAudit?: (rec: Record<string, unknown>) => void): EgressServer;
|