feat(p9): Capability Broker — governed egress + fail-closed gate + obligations
Tasks 9.1+9.2: contracts (CapabilityRequest/Result/Provider, PolicyObligation) + service CapabilityBroker that fails closed via opa (PolicyDeniedError, nothing sent), enforces obligations before any provider call (DENY_OUTBOUND/REVIEW → BLOCKED; MASK/REDACT → mask payload field), then routes to a CapabilityProviderRegistry (SandboxProvider default; env-gated HttpProvider overrides per channel; unknown channel fails closed). 5 tests: allow/deny/block/redact/unknown-channel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import type { CapabilityProvider } from '@insignia/iios-contracts';
|
||||
import { SandboxProvider } from './sandbox.provider';
|
||||
import { HttpProvider } from './http.provider';
|
||||
|
||||
const DEFAULT_CHANNELS = ['WEBHOOK', 'EMAIL', 'WHATSAPP', 'PORTAL'];
|
||||
|
||||
/**
|
||||
* Maps a channelType to the provider that executes egress for it. Sandbox by
|
||||
* default; if `IIOS_PROVIDER_URL_<CHANNELTYPE>` is set, a real HttpProvider
|
||||
* overrides the sandbox for that channel (the "flip the binding" swap). Unknown
|
||||
* channels fail closed — no silent egress path.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CapabilityProviderRegistry {
|
||||
private readonly byChannel = new Map<string, CapabilityProvider>();
|
||||
|
||||
constructor() {
|
||||
for (const ch of DEFAULT_CHANNELS) this.register(new SandboxProvider([ch]));
|
||||
for (const ch of DEFAULT_CHANNELS) {
|
||||
const url = process.env[`IIOS_PROVIDER_URL_${ch}`];
|
||||
if (url) this.register(new HttpProvider(ch, url));
|
||||
}
|
||||
}
|
||||
|
||||
register(provider: CapabilityProvider): void {
|
||||
for (const ch of provider.channelTypes) this.byChannel.set(ch, provider);
|
||||
}
|
||||
|
||||
forChannel(channelType: string): CapabilityProvider {
|
||||
const p = this.byChannel.get(channelType);
|
||||
if (!p) throw new NotFoundException(`no egress provider registered for channel: ${channelType}`);
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Ops view: which provider backs each channel. */
|
||||
bindings(): Record<string, string> {
|
||||
return Object.fromEntries([...this.byChannel.entries()].map(([ch, p]) => [ch, p.name]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user