qwen_2.5_model / src /modules /integration /plugin-instance.service.ts
Muhammad Noman
Deploy OpenWA to Hugging Face Spaces
46252cd
Raw
History Blame Contribute Delete
5.34 kB
import { randomBytes } from 'node:crypto';
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PluginInstance } from './entities/plugin-instance.entity';
import type { PluginConfigSchema } from '../../core/plugins/plugin.interfaces';
import { redactSecretConfig, restoreSecretConfig, SECRET_SENTINEL } from '../plugins/redact-config';
// A supplied ingress secret must be a real, guessing-resistant value; an empty/short one would make the
// public HMAC forgeable. Absent => auto-generate. Trimmed so pasted whitespace can't slip a weak secret in.
function normalizeSecret(supplied?: string): string {
if (supplied === undefined) return randomBytes(32).toString('hex');
const s = supplied.trim();
if (s.length < 16) {
throw new BadRequestException('instance secret must be a non-empty string of at least 16 characters');
}
return s;
}
export class InstanceExistsError extends Error {
constructor(pluginId: string, instanceId: string) {
super(`instance ${instanceId} already exists for plugin ${pluginId}`);
this.name = 'InstanceExistsError';
}
}
@Injectable()
export class PluginInstanceService {
constructor(@InjectRepository(PluginInstance, 'data') private readonly repo: Repository<PluginInstance>) {}
async mint(
pluginId: string,
instanceId: string,
opts: { sessionScope?: string; verifyToken?: string; secret?: string; config?: Record<string, unknown> },
): Promise<PluginInstance> {
const id = `${pluginId}:${instanceId}`;
const existing = await this.repo.findOne({ where: { id } });
if (existing) return existing;
const inst = this.repo.create({
id,
pluginId,
instanceId,
sessionScope: opts.sessionScope || null,
secret: normalizeSecret(opts.secret),
verifyToken: opts.verifyToken ?? null,
config: opts.config ?? null,
enabled: true,
});
return this.repo.save(inst);
}
resolve(pluginId: string, instanceId: string): Promise<PluginInstance | null> {
return this.repo.findOne({ where: { id: `${pluginId}:${instanceId}` } });
}
// Operator-facing view: never leak the raw secret, and mask any `secret:true` config field (e.g. a
// provider apiToken) per the plugin's configSchema — recursively, at any depth, and fail-closed when
// the schema is unavailable — by reusing the shared redactSecretConfig (single source of truth).
maskedView(instance: PluginInstance, schema?: PluginConfigSchema): PluginInstance {
return {
...instance,
secret: SECRET_SENTINEL,
config: instance.config == null ? instance.config : redactSecretConfig(instance.config, schema),
};
}
async create(
pluginId: string,
instanceId: string,
opts: { sessionScope?: string; verifyToken?: string; secret?: string; config?: Record<string, unknown> },
): Promise<PluginInstance> {
const id = `${pluginId}:${instanceId}`;
if (await this.repo.findOne({ where: { id } })) throw new InstanceExistsError(pluginId, instanceId);
const inst = this.repo.create({
id,
pluginId,
instanceId,
sessionScope: opts.sessionScope || null,
secret: normalizeSecret(opts.secret),
verifyToken: opts.verifyToken ?? null,
config: opts.config ?? null,
enabled: true,
});
return this.repo.save(inst);
}
list(pluginId: string): Promise<PluginInstance[]> {
return this.repo.find({ where: { pluginId } });
}
/** Every persisted instance across all plugins — used by the boot-time scope-binding reconciliation. */
listAll(): Promise<PluginInstance[]> {
return this.repo.find();
}
async regenerateSecret(pluginId: string, instanceId: string): Promise<PluginInstance> {
const inst = await this.resolve(pluginId, instanceId);
if (!inst) throw new Error(`instance ${instanceId} not found for plugin ${pluginId}`);
inst.secret = randomBytes(32).toString('hex');
return this.repo.save(inst);
}
async setEnabled(pluginId: string, instanceId: string, enabled: boolean): Promise<PluginInstance | null> {
const inst = await this.resolve(pluginId, instanceId);
if (!inst) return null;
inst.enabled = enabled;
return this.repo.save(inst);
}
async update(
pluginId: string,
instanceId: string,
patch: { sessionScope?: string; config?: Record<string, unknown> },
schema?: PluginConfigSchema,
): Promise<PluginInstance | null> {
const inst = await this.resolve(pluginId, instanceId);
if (!inst) return null;
if (patch.sessionScope !== undefined) inst.sessionScope = patch.sessionScope || null;
if (patch.config !== undefined) {
// The operator view masks secrets as the sentinel, so a round-tripped config carries '***' for
// unchanged secrets. Restore the stored values instead of persisting the mask (which would corrupt
// the credential); genuinely-new values are written as provided.
inst.config = restoreSecretConfig(patch.config, inst.config ?? undefined, schema);
}
return this.repo.save(inst);
}
async remove(pluginId: string, instanceId: string): Promise<boolean> {
const result = await this.repo.delete({ id: `${pluginId}:${instanceId}` });
return (result.affected ?? 0) > 0;
}
}