UNPKG

manifest

Version:

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

226 lines 9.53 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 __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var AgentKeyAuthGuard_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentKeyAuthGuard = void 0; const common_1 = require("@nestjs/common"); const config_1 = require("@nestjs/config"); const typeorm_1 = require("@nestjs/typeorm"); const typeorm_2 = require("typeorm"); const crypto_1 = require("crypto"); const agent_api_key_entity_1 = require("../../entities/agent-api-key.entity"); const hash_util_1 = require("../../common/utils/hash.util"); const api_key_constants_1 = require("../../common/constants/api-key.constants"); const local_mode_constants_1 = require("../../common/constants/local-mode.constants"); const local_ip_1 = require("../../common/utils/local-ip"); const MIN_TOKEN_LENGTH = 12; function cacheKey(token) { return (0, crypto_1.createHash)('sha256').update(token).digest('hex'); } let AgentKeyAuthGuard = AgentKeyAuthGuard_1 = class AgentKeyAuthGuard { keyRepo; configService; logger = new common_1.Logger(AgentKeyAuthGuard_1.name); cache = new Map(); devContext = null; CACHE_TTL_MS = 5 * 60 * 1000; MAX_CACHE_SIZE = 10_000; cleanupTimer; constructor(keyRepo, configService) { this.keyRepo = keyRepo; this.configService = configService; this.cleanupTimer = setInterval(() => this.evictExpired(), 60_000); if (typeof this.cleanupTimer === 'object' && 'unref' in this.cleanupTimer) { this.cleanupTimer.unref(); } } onModuleDestroy() { clearInterval(this.cleanupTimer); } async canActivate(context) { const request = context.switchToHttp().getRequest(); const authHeader = request.headers['authorization']; const ip = request.ip ?? ''; const isLocal = this.configService.get('app.manifestMode') === 'local' && (0, local_ip_1.isAllowedLocalIp)(ip); const isDevLoopback = this.configService.get('app.nodeEnv') === 'development' && (0, local_ip_1.isLoopbackIp)(ip); if (!authHeader) { if (this.handleLocalBypass(request, isLocal)) return true; if (await this.handleDevLoopback(request, isDevLoopback)) return true; this.logger.warn(`Request without auth from ${request.ip}`); throw new common_1.UnauthorizedException('Authorization header required'); } const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader; if (!token) { throw new common_1.UnauthorizedException('Empty token'); } if (!token.startsWith(api_key_constants_1.API_KEY_PREFIX)) { if (this.handleLocalBypass(request, isLocal)) return true; if (await this.handleDevLoopback(request, isDevLoopback)) return true; throw new common_1.UnauthorizedException('Invalid API key format'); } if (token.length < MIN_TOKEN_LENGTH) { throw new common_1.UnauthorizedException('Invalid API key format'); } return this.validateMnfstToken(request, token, isLocal); } invalidateCache(key) { this.cache.delete(cacheKey(key)); } clearCache() { this.cache.clear(); } setContext(request, ctx) { request.ingestionContext = ctx; } handleLocalBypass(request, isLocal) { if (!isLocal) return false; this.setContext(request, { tenantId: local_mode_constants_1.LOCAL_TENANT_ID, agentId: local_mode_constants_1.LOCAL_AGENT_ID, agentName: local_mode_constants_1.LOCAL_AGENT_NAME, userId: local_mode_constants_1.LOCAL_USER_ID, }); return true; } async handleDevLoopback(request, isDevLoopback) { if (!isDevLoopback) return false; const devCtx = await this.resolveDevContext(); if (!devCtx) return false; this.setContext(request, devCtx); return true; } async validateMnfstToken(request, token, isLocal) { const cached = this.cache.get(cacheKey(token)); if (cached && cached.expiresAt > Date.now()) { this.setContext(request, { tenantId: cached.tenantId, agentId: cached.agentId, agentName: cached.agentName, userId: cached.userId, }); return true; } const prefix = (0, hash_util_1.keyPrefix)(token); const candidates = await this.keyRepo .createQueryBuilder('k') .leftJoinAndSelect('k.agent', 'a') .leftJoinAndSelect('k.tenant', 't') .where('k.key_prefix = :prefix', { prefix }) .andWhere('k.is_active = true') .getMany(); const keyRecord = candidates.find((c) => (0, hash_util_1.verifyKey)(token, c.key_hash)); if (!keyRecord) { return this.handleUnknownKey(request, token, isLocal, candidates); } if (keyRecord.expires_at && new Date(keyRecord.expires_at) < new Date()) { throw new common_1.UnauthorizedException('API key expired'); } this.keyRepo .createQueryBuilder() .update(agent_api_key_entity_1.AgentApiKey) .set({ last_used_at: () => 'CURRENT_TIMESTAMP' }) .where('id = :id', { id: keyRecord.id }) .execute() .catch((err) => this.logger.warn(`Failed to update last_used_at: ${err.message}`)); this.evictExpired(); if (this.cache.size >= this.MAX_CACHE_SIZE) { const firstKey = this.cache.keys().next().value; if (firstKey) this.cache.delete(firstKey); } this.cache.set(cacheKey(token), { tenantId: keyRecord.tenant_id, agentId: keyRecord.agent_id, agentName: keyRecord.agent.name, userId: keyRecord.tenant.name, expiresAt: Date.now() + this.CACHE_TTL_MS, }); this.setContext(request, { tenantId: keyRecord.tenant_id, agentId: keyRecord.agent_id, agentName: keyRecord.agent.name, userId: keyRecord.tenant.name, }); return true; } async handleUnknownKey(request, token, isLocal, candidates) { if (!isLocal) { this.logger.warn(`Rejected unknown agent key: ${token.substring(0, 8)}...`); throw new common_1.UnauthorizedException('Invalid API key'); } if (candidates.length > 0) { const best = candidates[0]; this.setContext(request, { tenantId: best.tenant_id, agentId: best.agent_id, agentName: best.agent.name, userId: best.tenant.name, }); return true; } const fallback = await this.resolveDevContext(); if (fallback) { this.setContext(request, fallback); return true; } this.setContext(request, { tenantId: local_mode_constants_1.LOCAL_TENANT_ID, agentId: local_mode_constants_1.LOCAL_AGENT_ID, agentName: local_mode_constants_1.LOCAL_AGENT_NAME, userId: local_mode_constants_1.LOCAL_USER_ID, }); return true; } async resolveDevContext() { if (this.devContext && this.devContext.expiresAt > Date.now()) { return this.devContext.context; } const keyRecord = await this.keyRepo.findOne({ where: { is_active: true }, relations: ['agent', 'tenant'], }); if (!keyRecord) return null; const ctx = { tenantId: keyRecord.tenant_id, agentId: keyRecord.agent_id, agentName: keyRecord.agent.name, userId: keyRecord.tenant.name, }; this.devContext = { context: ctx, expiresAt: Date.now() + this.CACHE_TTL_MS }; return ctx; } evictExpired() { const now = Date.now(); for (const [key, entry] of this.cache) { if (entry.expiresAt <= now) this.cache.delete(key); } } }; exports.AgentKeyAuthGuard = AgentKeyAuthGuard; exports.AgentKeyAuthGuard = AgentKeyAuthGuard = AgentKeyAuthGuard_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, typeorm_1.InjectRepository)(agent_api_key_entity_1.AgentApiKey)), __metadata("design:paramtypes", [typeorm_2.Repository, config_1.ConfigService]) ], AgentKeyAuthGuard); //# sourceMappingURL=agent-key-auth.guard.js.map