-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathauth.js
More file actions
109 lines (99 loc) · 3.59 KB
/
auth.js
File metadata and controls
109 lines (99 loc) · 3.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import { env } from '../_lib/env.js';
import { authenticateBearer, extractBearer } from '../_lib/auth.js';
import {
paymentRequirements,
verifyPayment,
send402,
resolveResourceUrl,
} from '../_lib/x402-spec.js';
import { sendX402Error } from './payments.js';
function quoteString(s) {
return `"${String(s).replace(/[\\"]/g, '\\$&')}"`;
}
export function send401(res, msg) {
const resource = env.MCP_RESOURCE;
res.statusCode = 401;
res.setHeader(
'www-authenticate',
`Bearer resource_metadata=${quoteString(`${env.APP_ORIGIN}/.well-known/oauth-protected-resource`)}, resource=${quoteString(resource)}`,
);
res.setHeader('content-type', 'application/json; charset=utf-8');
res.end(JSON.stringify({ error: 'unauthorized', error_description: msg }));
}
export function sendJsonRpcError(res, id, code, message, data) {
res.statusCode = 200;
res.setHeader('content-type', 'application/json; charset=utf-8');
res.end(JSON.stringify({ jsonrpc: '2.0', id: id ?? null, error: { code, message, data } }));
}
// Returns { auth, x402Ctx } on success, or null if a response was already sent.
export async function authenticateRequest(req, res) {
const bearer = extractBearer(req);
const paymentHeader = req.headers['x-payment'];
if (bearer) {
const auth = await authenticateBearer(bearer, { audience: env.MCP_RESOURCE });
if (!auth) {
send401(res, 'missing or invalid access token');
return null;
}
return { auth, x402Ctx: null };
}
const resourceUrl = resolveResourceUrl(req, '/api/mcp');
const requirements = paymentRequirements(resourceUrl);
if (paymentHeader) {
try {
const verified = await verifyPayment({ paymentHeader, requirements });
const x402Ctx = {
resourceUrl,
requirements,
requirement: verified.requirement,
paymentPayload: verified.paymentPayload,
payer: verified.payer,
};
// Anonymous paid caller — synthesize an auth principal scoped to public-read tools.
// userId is null because usage_events.user_id is a UUID FK; the payer wallet is
// kept on the auth object so handlers and rate limits can key off of it.
return {
auth: {
userId: null,
rateKey: `x402:${x402Ctx.payer || 'anon'}`,
// Pay-per-call callers have no user account, so they cannot read or
// write account-scoped data. They get only the no-scope public tools
// (search_public_avatars, validate/inspect/optimize_model, solana_*).
scope: '',
source: 'x402',
payer: x402Ctx.payer,
},
x402Ctx,
};
} catch (err) {
sendX402Error(res, { resourceUrl, accepts: requirements }, err);
return null;
}
}
send402(res, { resourceUrl, accepts: requirements });
return null;
}
export async function handleSse(req, res) {
// We don't hold long-lived server→client subscriptions yet; respond politely.
const bearer = extractBearer(req);
// Unauthenticated callers without an X-PAYMENT header get an x402 challenge
// so x402scan / x402 clients can discover the price. Invalid bearers still
// get 401 with WWW-Authenticate so OAuth clients can re-auth correctly.
if (!bearer && !req.headers['x-payment']) {
const sseResourceUrl = resolveResourceUrl(req, '/api/mcp');
return send402(res, {
resourceUrl: sseResourceUrl,
accepts: paymentRequirements(sseResourceUrl),
});
}
const auth = await authenticateBearer(bearer, { audience: env.MCP_RESOURCE });
if (!auth) return send401(res, 'missing or invalid access token');
res.statusCode = 405;
res.setHeader('allow', 'POST, DELETE');
res.end();
}
export function handleTerminate(_req, res) {
// Stateless per-request server — nothing to tear down.
res.statusCode = 204;
res.end();
}