manifest
Version:
Self-hosted Manifest LLM router with embedded server, SQLite database, and dashboard
273 lines • 11.6 kB
JavaScript
"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