@vfarcic/dot-ai
Version:
AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
146 lines (145 loc) • 5.69 kB
JavaScript
;
/**
* Lightweight Dex OIDC client for PRD #380, Task 2.3.
*
* Three pure utility functions using only node:http/node:https.
* No external dependencies. Dex is trusted in-cluster, so ID tokens
* are decoded without signature verification.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAuthorizeUrl = buildAuthorizeUrl;
exports.exchangeDexCode = exchangeDexCode;
exports.parseIdToken = parseIdToken;
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
/**
* Build the Dex OIDC authorization URL for the browser redirect.
*
* Uses dexConfig.issuerUrl (the external Dex URL) because this URL
* is followed by the user's browser, not the MCP server.
*/
function buildAuthorizeUrl(dexConfig, params) {
const base = dexConfig.issuerUrl.replace(/\/$/, '');
const url = new URL(`${base}/auth`);
url.searchParams.set('client_id', dexConfig.clientId);
url.searchParams.set('redirect_uri', params.redirectUri);
url.searchParams.set('response_type', 'code');
url.searchParams.set('scope', params.scope ?? 'openid email profile groups');
url.searchParams.set('state', params.state);
return url.toString();
}
/**
* Exchange a Dex authorization code for tokens.
*
* Uses dexConfig.tokenEndpoint (the in-cluster URL) for server-to-server
* communication. Posts application/x-www-form-urlencoded with client credentials.
*/
async function exchangeDexCode(dexConfig, code, redirectUri) {
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: dexConfig.clientId,
client_secret: dexConfig.clientSecret,
}).toString();
const tokenUrl = new URL(dexConfig.tokenEndpoint);
const transport = tokenUrl.protocol === 'https:' ? https : http;
const response = await new Promise((resolve, reject) => {
const req = transport.request(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(body).toString(),
},
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk.toString(); });
res.on('end', () => {
resolve({ statusCode: res.statusCode ?? 0, body: data });
});
});
req.setTimeout(10_000, () => {
req.destroy(new Error('Dex token exchange timed out'));
});
req.on('error', reject);
req.write(body);
req.end();
});
if (response.statusCode !== 200) {
throw new Error(`Dex token exchange failed (HTTP ${response.statusCode}): ${response.body}`);
}
const parsed = JSON.parse(response.body);
if (!parsed.id_token) {
throw new Error('Dex token response missing id_token');
}
return {
idToken: parsed.id_token,
accessToken: parsed.access_token ?? '',
};
}
/**
* Decode a Dex ID token payload without signature verification.
*
* Dex is trusted in-cluster — the token was received directly from
* Dex's token endpoint over the internal network. No JWKS needed.
*/
function parseIdToken(idToken) {
const parts = idToken.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format — expected 3 segments');
}
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
const now = Math.floor(Date.now() / 1000);
if (typeof payload.exp !== 'number' || payload.exp <= now) {
throw new Error('ID token is expired or missing exp claim');
}
if (typeof payload.sub !== 'string' || payload.sub.length === 0) {
throw new Error('ID token missing sub claim');
}
if (payload.email !== undefined && typeof payload.email !== 'string') {
throw new Error('ID token email claim must be a string');
}
if (payload.groups !== undefined) {
if (!Array.isArray(payload.groups) || !payload.groups.every((g) => typeof g === 'string')) {
throw new Error('ID token groups claim must be an array of strings');
}
}
return {
sub: payload.sub,
email: payload.email,
groups: payload.groups,
};
}