import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import bcrypt from 'bcryptjs'; import { randomBytes } from 'crypto'; import { GuildNode } from '../entities/guild-node.entity.js'; import { AuditService } from '../audit/audit.service.js'; @Injectable() export class NodeAdminService { constructor( @InjectRepository(GuildNode) private readonly nodeRepo: Repository, private readonly audit: AuditService, ) {} async registerNode(input: { nodeId: string; name: string; endpoint: string }) { const existedByNodeId = await this.nodeRepo.findOne({ where: { nodeId: input.nodeId } }); if (existedByNodeId) { throw new ConflictException('nodeId already exists'); } const existedByEndpoint = await this.nodeRepo.findOne({ where: { endpoint: input.endpoint } }); if (existedByEndpoint) { throw new ConflictException('endpoint already exists'); } const node = this.nodeRepo.create({ nodeId: input.nodeId, name: input.name, endpoint: input.endpoint, status: 'active', apiKeyHash: null, }); const rawApiKey = `gk_${randomBytes(24).toString('hex')}`; node.apiKeyHash = await bcrypt.hash(rawApiKey, 10); const saved = await this.nodeRepo.save(node); await this.audit.write({ action: 'node.register', targetType: 'node', targetId: saved.nodeId, detail: JSON.stringify({ endpoint: saved.endpoint, via: 'cli' }), }); return { node: { id: saved.id, nodeId: saved.nodeId, name: saved.name, endpoint: saved.endpoint, status: saved.status, }, apiKey: rawApiKey, }; } // Admin-only via cli (never HTTP): set the free-form purpose string on // a guild node. Pass an empty string to clear it (null). async setPurpose(nodeId: string, purpose: string) { const node = await this.nodeRepo.findOne({ where: { nodeId } }); if (!node) throw new NotFoundException(`node ${nodeId} not found`); const trimmed = String(purpose ?? '').trim(); node.purpose = trimmed === '' ? null : trimmed; const saved = await this.nodeRepo.save(node); await this.audit.write({ action: 'node.set_purpose', targetType: 'node', targetId: saved.nodeId, detail: JSON.stringify({ purpose: saved.purpose, via: 'cli' }), }); return { nodeId: saved.nodeId, name: saved.name, purpose: saved.purpose, }; } }