import { Inject, Injectable } from '@nestjs/common'; import type { CapabilityRequest, CapabilityResult, IiosPlatformPorts } from '@insignia/iios-contracts'; import { PLATFORM_PORTS } from '../platform/platform-ports'; import { decideOrThrow } from '../platform/fail-closed'; import { CapabilityProviderRegistry } from './capability.registry'; /** * The single governed egress boundary (P9). Every outbound action flows through * here: it fails closed on policy (nothing sent unless explicitly allowed), * enforces the policy's obligations BEFORE a provider is ever called, then routes * to the channel's provider. It is the concrete realization of the `capability` * platform port — a real broker replacing the permissive P1 stub. */ @Injectable() export class CapabilityBroker { constructor( @Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts, private readonly registry: CapabilityProviderRegistry, ) {} async invoke(req: CapabilityRequest): Promise { // 1. Fail-closed policy gate. Deny/timeout ⇒ PolicyDeniedError, nothing sent. const decision = await decideOrThrow(this.ports, { action: `iios.capability.${req.capability}`, scopeId: req.scopeId, channelType: req.channelType, target: req.target, }); // 2. Enforce obligations BEFORE touching a provider. for (const ob of decision.obligations) { if (ob.kind === 'DENY_OUTBOUND' || ob.kind === 'REVIEW') { return { providerRef: 'blocked', outcome: 'BLOCKED', errorCode: ob.kind }; } } const payload = this.applyMasks(req.payload, decision.obligations); // 3. Route to the channel's provider (unknown channel fails closed in the registry). const provider = this.registry.forChannel(req.channelType); const result = await provider.send({ ...req, payload }); // 4. Normalize the provider's outcome. const outcome = result.outcome === 'SENT' ? 'SENT' : result.outcome === 'RATE_LIMITED' ? 'RATE_LIMITED' : 'FAILED'; return { providerRef: result.providerRef, outcome, errorCode: result.errorCode, latencyMs: result.latencyMs }; } private applyMasks( payload: Record, obligations: { kind: string; target?: string }[], ): Record { const masks = obligations.filter((o) => (o.kind === 'MASK' || o.kind === 'REDACT') && o.target); if (masks.length === 0) return payload; const out = { ...payload }; for (const m of masks) out[m.target as string] = '[REDACTED]'; return out; } }