P15a — MCP grant store + connection-bound JWT (pinning-webui)
Server-side channel that lets a user grant a paired AI (MCP) scoped access to their REAL files, delivered to the stateless MCP via a fetchable store (NOT bundle re-injection).
Background
A grant = a ShareToken (JSON; path_scope+permissions+expiry+id are PLAINTEXT, only the DEK is HPKE-sealed to the MCP X25519 pubkey) PLUS a matching Capability{scope,permissions} the MCP adds to its bundle. FxFiles mints one token PER FILE for a folder/tag, sealed to the MCP pubkey, and publishes them here. The MCP later GETs its grants and merges them. Storing the sealed token server-side is safe (only the MCP secret unwraps).
Scope (additive, backward-compatible — extends P11)
- Connection-bound JWT.
mintMcpToken + POST /api/mcp/tokens accept an OPTIONAL mcp_pub_b64 (base64 of a 32-byte X25519 pubkey); when present it is embedded as a top-level cnf claim (cnf: { mcp_pub_b64 }, RFC-7800 key-binding) SIBLING of mcp. Absent ⇒ no cnf (older flow unchanged). verifyMcpToken exposes cnf.mcp_pub_b64 (validated 32-byte b64). Additive: the P12 gateway ignores unknown claims.
mcp_grants table + helpers (postgres.ts, mirrors mcp_revoked_tokens): insertMcpGrants (bulk), listActiveGrantsForConnection(userId, mcpPubB64), revokeMcpGrant, gcExpiredGrants.
- Endpoints (
app.ts):
POST /api/mcp/grants — USER session/bearer; bulk insert (cap ≤1000).
GET /api/mcp/grants — MCP scoped JWT auth; returns ONLY listActiveGrantsForConnection(sub, cnf.mcp_pub_b64). No cnf ⇒ empty. THIS FILTER IS THE SECURITY BOUNDARY (closes cross-agent metadata leak).
POST /api/mcp/grants/revoke — USER session/bearer.
- Audit-log issue/revoke counts (no token_json, no secrets).
Tests
vitest: always-green unit (cnf mint/verify, missing-cnf handling, payload-shape validation, ≤1000 cap) + pg-gated integration (insert; A-vs-B isolation; revoke; unauth → 401).
PR INTO feat/fula-mcp (NOT main).
P15a — MCP grant store + connection-bound JWT (pinning-webui)
Server-side channel that lets a user grant a paired AI (MCP) scoped access to their REAL files, delivered to the stateless MCP via a fetchable store (NOT bundle re-injection).
Background
A grant = a
ShareToken(JSON;path_scope+permissions+expiry+idare PLAINTEXT, only the DEK is HPKE-sealed to the MCP X25519 pubkey) PLUS a matchingCapability{scope,permissions}the MCP adds to its bundle. FxFiles mints one token PER FILE for a folder/tag, sealed to the MCP pubkey, and publishes them here. The MCP later GETs its grants and merges them. Storing the sealed token server-side is safe (only the MCP secret unwraps).Scope (additive, backward-compatible — extends P11)
mintMcpToken+POST /api/mcp/tokensaccept an OPTIONALmcp_pub_b64(base64 of a 32-byte X25519 pubkey); when present it is embedded as a top-levelcnfclaim (cnf: { mcp_pub_b64 }, RFC-7800 key-binding) SIBLING ofmcp. Absent ⇒ nocnf(older flow unchanged).verifyMcpTokenexposescnf.mcp_pub_b64(validated 32-byte b64). Additive: the P12 gateway ignores unknown claims.mcp_grantstable + helpers (postgres.ts, mirrorsmcp_revoked_tokens):insertMcpGrants(bulk),listActiveGrantsForConnection(userId, mcpPubB64),revokeMcpGrant,gcExpiredGrants.app.ts):POST /api/mcp/grants— USER session/bearer; bulk insert (cap ≤1000).GET /api/mcp/grants— MCP scoped JWT auth; returns ONLYlistActiveGrantsForConnection(sub, cnf.mcp_pub_b64). Nocnf⇒ empty. THIS FILTER IS THE SECURITY BOUNDARY (closes cross-agent metadata leak).POST /api/mcp/grants/revoke— USER session/bearer.Tests
vitest: always-green unit (cnf mint/verify, missing-cnf handling, payload-shape validation, ≤1000 cap) + pg-gated integration (insert; A-vs-B isolation; revoke; unauth → 401).
PR INTO
feat/fula-mcp(NOT main).