manifest
Version:
Self-hosted Manifest LLM router with embedded server, SQLite database, and dashboard
226 lines • 9.53 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 __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