UNPKG

@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
"use strict"; /** * 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, }; }