UNPKG

manifest

Version:

Self-hosted Manifest LLM router with embedded server, SQLite database, and dashboard

273 lines 11.6 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var OpenaiOauthService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenaiOauthService = exports.oauthDoneHtml = void 0; const common_1 = require("@nestjs/common"); const config_1 = require("@nestjs/config"); const crypto_1 = require("crypto"); const http_1 = require("http"); const provider_service_1 = require("../routing-core/provider.service"); const model_discovery_service_1 = require("../../model-discovery/model-discovery.service"); const openai_oauth_types_1 = require("./openai-oauth.types"); Object.defineProperty(exports, "oauthDoneHtml", { enumerable: true, get: function () { return openai_oauth_types_1.oauthDoneHtml; } }); const DEFAULT_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'; const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize'; const TOKEN_URL = 'https://auth.openai.com/oauth/token'; const REVOKE_URL = 'https://auth.openai.com/oauth/revoke'; const SCOPE = 'openid profile email offline_access'; const CALLBACK_PORT = 1455; const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/auth/callback`; const STATE_TTL_MS = 10 * 60 * 1000; let OpenaiOauthService = OpenaiOauthService_1 = class OpenaiOauthService { providerService; configService; discoveryService; logger = new common_1.Logger(OpenaiOauthService_1.name); pending = new Map(); callbackServer = null; serverReady = null; clientId; useCallbackServer; constructor(providerService, configService, discoveryService) { this.providerService = providerService; this.configService = configService; this.discoveryService = discoveryService; this.clientId = this.configService.get('OPENAI_OAUTH_CLIENT_ID') ?? DEFAULT_CLIENT_ID; this.useCallbackServer = this.configService.get('MANIFEST_MODE') !== 'cloud'; } async generateAuthorizationUrl(agentId, userId, backendUrl) { this.cleanupExpired(); const state = (0, crypto_1.randomBytes)(32).toString('hex'); const verifier = (0, crypto_1.randomBytes)(32).toString('base64url'); const challenge = (0, crypto_1.createHash)('sha256').update(verifier).digest('base64url'); this.pending.set(state, { verifier, agentId, userId, backendUrl: backendUrl ?? '', expiresAt: Date.now() + STATE_TTL_MS, }); if (this.useCallbackServer) { await this.ensureCallbackServer(); } const params = new URLSearchParams({ client_id: this.clientId, redirect_uri: REDIRECT_URI, response_type: 'code', scope: SCOPE, state, code_challenge: challenge, code_challenge_method: 'S256', }); return `${AUTHORIZE_URL}?${params.toString()}`; } async exchangeCode(state, code) { const pending = this.pending.get(state); if (!pending) throw new Error('Invalid or expired OAuth state'); if (pending.expiresAt < Date.now()) { this.pending.delete(state); throw new Error('OAuth state expired'); } this.pending.delete(state); const response = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI, client_id: this.clientId, code_verifier: pending.verifier, }), }); if (!response.ok) { const text = await response.text(); this.logger.error(`OpenAI token exchange failed: ${text}`); throw new Error('Token exchange failed'); } const data = (await response.json()); const blob = { t: data.access_token, r: data.refresh_token, e: Date.now() + data.expires_in * 1000, }; const { provider: savedProvider } = await this.providerService.upsertProvider(pending.agentId, pending.userId, 'openai', JSON.stringify(blob), 'subscription'); try { await this.discoveryService.discoverModels(savedProvider); await this.providerService.recalculateTiers(pending.agentId); } catch (err) { this.logger.warn(`Model discovery after OAuth failed: ${err}`); } this.logger.log(`OpenAI OAuth token stored for agent=${pending.agentId}`); this.shutdownCallbackServerIfIdle(); } async refreshAccessToken(refreshToken) { const response = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: this.clientId, }), }); if (!response.ok) { const text = await response.text(); this.logger.error(`OpenAI token refresh failed: ${text}`); throw new Error('Token refresh failed'); } const data = (await response.json()); return { t: data.access_token, r: data.refresh_token || refreshToken, e: Date.now() + data.expires_in * 1000, }; } async unwrapToken(rawValue, agentId, userId) { let blob; try { blob = JSON.parse(rawValue); } catch { return null; } if (!blob.t || !blob.r || !blob.e) return null; if (Date.now() < blob.e - 60_000) return blob.t; try { const refreshed = await this.refreshAccessToken(blob.r); await this.providerService.upsertProvider(agentId, userId, 'openai', JSON.stringify(refreshed), 'subscription'); this.logger.log(`OpenAI OAuth token refreshed for agent=${agentId}`); return refreshed.t; } catch (err) { this.logger.error(`Failed to refresh OpenAI token: ${err}`); return blob.t; } } async revokeToken(token) { try { const response = await fetch(REVOKE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ token, client_id: this.clientId }), }); if (!response.ok) { const text = await response.text(); this.logger.warn(`OpenAI token revocation failed: ${text}`); } else { this.logger.log('OpenAI OAuth token revoked'); } } catch (err) { this.logger.warn(`OpenAI token revocation error: ${err}`); } } getPendingCount() { return this.pending.size; } clearPendingState(state) { this.pending.delete(state); } ensureCallbackServer() { if (this.callbackServer) return Promise.resolve(); if (this.serverReady) return this.serverReady; this.serverReady = new Promise((resolve, reject) => { const server = (0, http_1.createServer)((req, res) => this.handleCallbackRequest(req, res)); server.on('error', (err) => { this.callbackServer = null; this.serverReady = null; if (err.code === 'EADDRINUSE') { this.logger.warn(`Port ${CALLBACK_PORT} in use — callback server not started`); reject(new Error(`Port ${CALLBACK_PORT} is already in use. Run 'lsof -i :${CALLBACK_PORT}' to find the process.`)); } else { this.logger.error(`Callback server error: ${err.message}`); reject(new Error(`Callback server failed: ${err.message}`)); } }); server.listen(CALLBACK_PORT, '127.0.0.1', () => { this.logger.log(`OAuth callback server listening on port ${CALLBACK_PORT}`); this.callbackServer = server; resolve(); }); server.unref(); }); return this.serverReady; } handleCallbackRequest(req, res) { const url = new URL(req.url ?? '/', `http://localhost:${CALLBACK_PORT}`); if (url.pathname !== '/auth/callback') { res.writeHead(404); res.end('Not found'); return; } const code = url.searchParams.get('code') ?? ''; const state = url.searchParams.get('state') ?? ''; const error = url.searchParams.get('error'); const appUrl = this.pending.get(state)?.backendUrl || ''; if (error) { const desc = url.searchParams.get('error_description') ?? error; this.logger.error(`OAuth callback error from provider: ${desc}`); this.pending.delete(state); this.shutdownCallbackServerIfIdle(); this.sendDoneResponse(res, false, appUrl); return; } this.exchangeCode(state, code) .then(() => this.sendDoneResponse(res, true, appUrl)) .catch((err) => { this.logger.error(`OAuth callback failed: ${err}`); this.sendDoneResponse(res, false, appUrl); }); } sendDoneResponse(res, success, appUrl) { if (appUrl) { const ok = success ? '1' : '0'; res.writeHead(302, { Location: `${appUrl}/api/v1/oauth/openai/done?ok=${ok}` }); res.end(); } else { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end((0, openai_oauth_types_1.oauthDoneHtml)(success)); } } shutdownCallbackServerIfIdle() { if (this.pending.size === 0 && this.callbackServer) { this.callbackServer.close(); this.callbackServer = null; this.serverReady = null; this.logger.log('OAuth callback server shut down (no pending flows)'); } } cleanupExpired() { const now = Date.now(); for (const [key, val] of this.pending) { if (val.expiresAt < now) this.pending.delete(key); } } }; exports.OpenaiOauthService = OpenaiOauthService; exports.OpenaiOauthService = OpenaiOauthService = OpenaiOauthService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [provider_service_1.ProviderService, config_1.ConfigService, model_discovery_service_1.ModelDiscoveryService]) ], OpenaiOauthService); //# sourceMappingURL=openai-oauth.service.js.map