gaunt-sloth-assistant
Version:
[](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml) [ {
this.config = config;
this.innerState = crypto.randomUUID();
if (!this.config.redirectUrl) {
throw new Error('No redirect URL provided');
}
if (!this.config.serverUrl) {
throw new Error('No server URL provided');
}
this.storagePath = getOAuthStoragePath(this.config.serverUrl);
}
loadStorageData() {
if (existsSync(this.storagePath)) {
try {
const data = readFileSync(this.storagePath, 'utf-8');
return JSON.parse(data);
}
catch (error) {
displayInfo('Failed to load OAuth storage data:' + error);
return {};
}
}
return {};
}
saveStorageData(data) {
try {
writeFileSync(this.storagePath, JSON.stringify(data, null, 2), 'utf-8');
}
catch (error) {
console.error('Failed to save OAuth storage data:', error);
}
}
state() {
return this.innerState;
}
// noinspection JSUnusedGlobalSymbols
get redirectUrl() {
return this.config.redirectUrl;
}
// noinspection JSUnusedGlobalSymbols
get clientMetadata() {
return {
redirect_uris: [this.config.redirectUrl],
client_name: 'Gaunt Sloth Assistant',
client_uri: 'https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant',
software_id: '1dd38b83-946b-4631-8855-66ee467bfd68',
scope: 'mcp:read mcp:write',
token_endpoint_auth_method: 'none',
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
};
}
// noinspection JSUnusedGlobalSymbols
saveClientInformation(clientInformation) {
const data = this.loadStorageData();
data.clientInformation = clientInformation;
this.saveStorageData(data);
return Promise.resolve();
}
// noinspection JSUnusedGlobalSymbols
async clientInformation() {
const data = this.loadStorageData();
return Promise.resolve(data.clientInformation);
}
// noinspection JSUnusedGlobalSymbols
async saveTokens(tokens) {
const data = this.loadStorageData();
data.tokens = tokens;
this.saveStorageData(data);
}
// noinspection JSUnusedGlobalSymbols
tokens() {
const data = this.loadStorageData();
return data.tokens;
}
// noinspection JSUnusedGlobalSymbols
saveCodeVerifier(codeVerifier) {
const data = this.loadStorageData();
data.codeVerifier = codeVerifier;
this.saveStorageData(data);
return Promise.resolve();
}
// noinspection JSUnusedGlobalSymbols
codeVerifier() {
const data = this.loadStorageData();
if (!data.codeVerifier) {
throw new Error('No code verifier stored');
}
return Promise.resolve(data.codeVerifier);
}
// noinspection JSUnusedGlobalSymbols
async redirectToAuthorization(authUrl) {
displayInfo('Auth url: ' + authUrl.toString());
try {
const url = authUrl.toString();
displayInfo('Trying to open browser');
// Handle different platforms
const platformName = platform();
if (platformName === 'win32') {
// Windows
execSync(`start "" "${url}"`);
}
else if (platformName === 'darwin') {
// macOS
execSync(`open "${url}"`);
}
else {
// Linux and others
execSync(`xdg-open "${url}"`);
}
}
catch (error) {
displayInfo(`Failed to open browser: ${error}`);
displayInfo(`Please open ${authUrl.toString()} in your browser`);
}
}
}
export async function createAuthProviderAndAuthenticate(mcpServer) {
const { port, server, codePromise } = await createOAuthRedirectServer('/oauth-callback');
const authProvider = new OAuthClientProviderImpl({
redirectUrl: `http://127.0.0.1:${port}/oauth-callback`,
serverUrl: mcpServer.url,
});
const outcome = await auth(authProvider, { serverUrl: mcpServer.url });
if (outcome == 'REDIRECT') {
const authorizationCode = await codePromise;
await auth(authProvider, { serverUrl: mcpServer.url, authorizationCode });
}
else if (outcome == 'AUTHORIZED') {
try {
server.close();
}
catch { }
displayInfo('Authorized');
}
else {
throw new Error(`Unexpected Auth outcome: ${outcome}`);
}
return authProvider;
}
export function createOAuthRedirectServer(path, portParam = 0) {
const redirectApp = express();
return new Promise((resolve, reject) => {
const codePromise = new Promise((resolveCode, rejectCode) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
redirectApp.get(path, (req, res) => {
const code = req.query.code;
if (!code) {
res.status(400).send('Error: No auth code received');
rejectCode('Error: No auth code received');
return;
}
res.send(`<div style="height: 80vh;text-align: center;display: flex;justify-content: center;align-items: center;">
<div>
<h1>Auth successful!</h1>
You may close this window and return to the Gaunt Sloth.
<div>
<img src="data:image/bmp;base64,Qk02AwAAAAAAADYAAAAoAAAAEAAAABAAAAABABgAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAnD98ZBtDTQ8sey9YOQobRA8nZx9OdyxahzZrMwERdCtShjZqLQUVLwgZKwUWKQYXsFCRq06KciJPnEh/n0GEmjuClDl9lzl/uFGglz56dChOm0h0SBInLgQVKwgZKgUVgzZchDRfqEeLsUmavFWjvV6joEGFizFvsU+Yw2CowWCiu1qdbCRNKgEQMAofMAcebSRIgC9bt1KdqEqNnD6BokGJqEaOvVmjqEWMu12ftFeYwGCjpUOMUxc8KQIRLAQWhjVmo0qGs1CYqEmOqEmPmjyArk2UrEyQr02TsU2WwV6pvF2eo0CKlDl+OgsiJwQSijZthS9qnDuAo0GFqESMmz2BijZtpUOLnT2CuVicyWmtsVCUjDh0p0KQbiVYJAQSkzx3kzZ7iS5rv2mn3Y3Gq0+Pgi1laCFOfyxmmTx/slCVmT55eihemzyGjTd6NwgfgSxokzl9qkuY4pLR+Lvg1H3EjTRzZB1IjTV3cCNWmjmAgC1diCxpjC1ulDd8RhAtdihbljl/vmK1v2WuuWGgtVWdjDVwgS5ldCVZcydckjl5kzh3ynGwxHOooT6DSA8sQQwkpUeTyGrBlTp8eCNZkzh1rEeSmDx9ZB9JcyNYkzh2tlek5pjT8q3bqU2PPQokJgEPhTVxy2nBkDd6kDV2qEaOwGOvvWGxuViqoUSNlDd6nj6FvmKq13zKnkiJMgUZKwUWPg4poUaOkzp+kTh7nkCExG60033Jym7C03zKsVOdiC9tjjR10W7IeThrKAAOMAUYJgUULgUZdCZdqkqYu1ersk+fvFyww2q4z3fDqUmXhy5ulTp/yWi/Txo6JgEPLgYZLQUXKQUVJwkaSxExq1GTxWW3t1SpsU+gv1mvnUOLjTJ3izh3dTRkJwEQKgQWLQYaLAgcLQUYLQYZLAERezZaiTtmXyFJgjp0ijt1iTlqOAsjNQ0pKQASJwUVKQYYLgUZLAQZLAgcLgUZMAUYZyVRfCthPQoiIQAJOgcgYyBLKAIRLAUZNwsoMQcfKAQV"
width="16" height="16">
</div>
</div>
</div>`);
resolveCode(code);
if (server) {
displayInfo('Cleaning auth redirect server...');
server.close();
}
});
});
const server = redirectApp.listen(portParam, () => {
const addressInfo = server.address();
const port = addressInfo.port;
displayInfo(`OAuth callback server listening at ${port}`);
resolve({ port, server, codePromise });
});
server.on('error', (err) => {
// If the server fails to start, reject the outer promise.
reject(err);
});
});
}
//# sourceMappingURL=OAuthClientProviderImpl.js.map