Files
iios/packages/iios-service/src/dev/dev.controller.ts
T
maaz519 cdba0f0179 feat(iios): retention sweep dev endpoint + /metrics + smoke (P9)
POST /v1/dev/retention/sweep runs the sweep on demand; /metrics gains a global
retention section (snapshot counts by status). smoke-retention.mjs (0-day
windows) proves an aged interaction is redacted in place and surfaces as a
REDACTED snapshot on /metrics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:57:51 +05:30

118 lines
4.5 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import { BadRequestException, Body, Controller, ForbiddenException, Headers, Param, Post } from '@nestjs/common';
import jwt from 'jsonwebtoken';
import { IIOS_EVENTS } from '@insignia/iios-contracts';
import { hmacSign } from '@insignia/iios-adapter-sdk';
import { InboundService } from '../adapters/inbound.service';
import { AdapterRegistry } from '../adapters/adapter.registry';
import { PrismaService } from '../prisma/prisma.service';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { SessionVerifier } from '../platform/session.verifier';
import { RetentionService } from '../retention/retention.service';
/**
* Dev-only helpers (gated by IIOS_DEV_TOKENS=1). Never enable in production.
*/
@Controller('v1/dev')
export class DevController {
constructor(
private readonly inbound: InboundService,
private readonly registry: AdapterRegistry,
private readonly prisma: PrismaService,
private readonly actors: ActorResolver,
private readonly session: SessionVerifier,
private readonly retention: RetentionService,
) {}
@Post('token')
token(@Body() body: { appId: string; userId: string; name?: string; orgId?: string }): { token: string } {
this.assertDev();
let secrets: Record<string, string>;
try {
secrets = JSON.parse(process.env.APP_SECRETS ?? '{}');
} catch {
secrets = {};
}
const secret = secrets[body.appId];
if (!secret) throw new BadRequestException(`unknown app: ${body.appId}`);
const token = jwt.sign(
{ sub: body.userId, name: body.name ?? body.userId, appId: body.appId, orgId: body.orgId ?? `org_${body.appId}` },
secret,
{ algorithm: 'HS256', expiresIn: '2h' },
);
return { token };
}
/** Server signs a sample provider payload and feeds it to the inbound pipeline. */
@Post('webhook/:channelType')
async injectWebhook(@Param('channelType') channelType: string, @Body() body?: { from?: string; text?: string }) {
this.assertDev();
const reg = this.registry.get(channelType);
if (!reg) throw new BadRequestException(`unknown channel type: ${channelType}`);
const payload = {
eventId: `dev-${randomUUID()}`,
from: body?.from ?? 'sim@customer',
displayName: 'Sim Customer',
threadRef: `sim-${randomUUID().slice(0, 8)}`,
subject: 'Simulated inbound',
text: body?.text ?? 'Hello from a simulated provider',
};
const raw = JSON.stringify(payload);
return this.inbound.receive(channelType, raw, {
'x-iios-signature': hmacSign(raw, reg.config.secret),
'content-type': 'application/json',
});
}
/**
* Inject a poison chaos event into the outbox. The relay publishes it, the
* ChaosConsumer throws on first delivery → it is dead-lettered under the caller's
* scope, then `POST /v1/dlq/:id/replay` resolves it. Drives the DLQ smoke.
*/
@Post('chaos/poison')
async poison(@Headers('authorization') auth?: string): Promise<{ eventId: string; scopeId: string }> {
this.assertDev();
const principal = this.principal(auth);
const scope = await this.actors.resolveScope(principal); // find-or-create so GET /v1/dlq matches
const id = `evt_chaos_${randomUUID()}`;
const cloudEvent = {
specversion: '1.0',
id,
type: IIOS_EVENTS.devChaos,
source: `iios/dev/${principal.appId}`,
subject: `chaos/${id}`,
time: new Date().toISOString(),
datacontenttype: 'application/json',
insignia: { scopeSnapshotId: scope.id, idempotencyKey: id, dataClass: 'internal' },
data: { poison: true },
};
await this.prisma.iiosOutboxEvent.create({
data: {
aggregateType: 'dev-chaos',
aggregateId: id,
eventType: IIOS_EVENTS.devChaos,
cloudEvent: cloudEvent as unknown as object,
partitionKey: `${principal.orgId}:${principal.appId}:chaos`,
},
});
return { eventId: id, scopeId: scope.id };
}
/** Run the retention sweep on demand (drives the retention smoke). */
@Post('retention/sweep')
async retentionSweep() {
this.assertDev();
return this.retention.sweep();
}
private principal(authorization?: string): MessagePrincipal {
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
if (!token) throw new BadRequestException('Authorization bearer token is required');
return this.session.verify(token);
}
private assertDev(): void {
if (process.env.IIOS_DEV_TOKENS !== '1') throw new ForbiddenException('dev endpoints disabled');
}
}