lib/mcp-oauth.js
Rama main · réplica de lectura del repositorio original.
var crypto = require('crypto');
var http = require('http');
var assertPublicUrl = require('./mcp-network').assertPublicUrl;
function createOAuthProvider(record, persist, flow, authorize) {
record.auth = record.auth || {};
var auth = record.auth;
function guard() { if (flow && flow.cancelled) throw new Error('Sign-in expired.'); if (authorize) authorize(); }
function save() { persist(); }
return {
get redirectUrl() { return flow ? flow.redirectUrl : auth.redirectUrl; },
get clientMetadata() { return { client_name: 'Clay', redirect_uris: [flow ? flow.redirectUrl : auth.redirectUrl],
grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'none' }; },
state: function () { return flow ? flow.state : crypto.randomBytes(32).toString('hex'); },
clientInformation: function () { guard(); return auth.client; },
saveClientInformation: function (value) { guard(); auth.client = value; save(); },
tokens: function () { guard(); return auth.tokens; },
saveTokens: function (value) { guard(); auth.tokens = value; save(); },
saveCodeVerifier: function (value) { guard(); if (flow) flow.verifier = value; },
codeVerifier: function () { if (!flow || !flow.verifier) throw new Error('Sign in again.'); return flow.verifier; },
redirectToAuthorization: async function (url) {
if (!flow) { var required = new Error('Sign in to connect this server.'); required.name = 'UnauthorizedError'; throw required; }
await (flow.validateUrl || assertPublicUrl)(url.href);
guard();
if (url.searchParams.get('state') !== flow.state || url.searchParams.get('redirect_uri') !== flow.redirectUrl || !url.searchParams.get('code_challenge')) throw new Error('Invalid sign-in request.');
flow.authorizationUrl = url.href;
},
discoveryState: function () { guard(); return auth.discovery; },
saveDiscoveryState: function (value) {
guard();
// Bind client credentials to the discovered issuer.
if (auth.discovery && auth.discovery.authorizationServerUrl !== value.authorizationServerUrl) { delete auth.client; delete auth.tokens; }
guard(); auth.discovery = value; save();
},
invalidateCredentials: function (which) {
guard();
if (which === 'all' || which === 'client') delete auth.client;
if (which === 'all' || which === 'tokens') delete auth.tokens;
if (which === 'all' || which === 'discovery') delete auth.discovery;
if (flow && (which === 'all' || which === 'verifier')) flow.verifier = null;
save();
},
};
}
async function createCallback(onCode) {
var flow = { state: crypto.randomBytes(32).toString('hex'), used: false };
var server = http.createServer(function (req, res) {
var url = new URL(req.url, flow.redirectUrl);
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Referrer-Policy', 'no-referrer');
if (req.method !== 'GET' || url.pathname !== '/callback' || flow.used || url.searchParams.getAll('state').length !== 1 || url.searchParams.getAll('code').length !== 1 || url.searchParams.getAll('iss').length > 1 || url.searchParams.get('state') !== flow.state) {
res.writeHead(400); res.end('Invalid or expired sign-in.'); return;
}
var code = url.searchParams.get('code');
if (!code || code.length > 8192 || url.searchParams.has('error')) { res.writeHead(400); res.end('Sign-in was not completed. Return to Clay and retry.'); return; }
flow.used = true;
Promise.resolve().then(function () { return onCode(code, url.searchParams.get('iss')); }).then(function () {
res.end('Connected. You can return to Clay.');
}, function () { res.writeHead(400); res.end('Sign-in failed. Return to Clay and retry.'); });
});
await new Promise(function (resolve, reject) { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
flow.redirectUrl = 'http://127.0.0.1:' + server.address().port + '/callback';
flow.close = function () { server.close(); server.closeAllConnections(); flow.verifier = null; };
return flow;
}
module.exports = { createOAuthProvider: createOAuthProvider, createCallback: createCallback };