feat(iios): tenant-scoped /v1/dlq REST + chaos endpoint + smoke (P9 slice 5)

GET /v1/dlq lists the caller tenant's dead-letters; POST /v1/dlq/:id/replay
fences by assertOwns (KG-02) + audits each replay. Dev-only POST
/v1/dev/chaos/poison + ChaosConsumer (fails first delivery, succeeds on
replay) drive smoke-dlq.mjs: poison → QUARANTINED → cross-tenant denied →
owner replay → RESOLVED. Adds dev.chaos.v1 contract event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 02:01:40 +05:30
parent d3d557766e
commit dbf8f814c0
6 changed files with 215 additions and 2 deletions
@@ -1,9 +1,13 @@
import { randomUUID } from 'node:crypto';
import { BadRequestException, Body, Controller, ForbiddenException, Param, Post } from '@nestjs/common';
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';
/**
* Dev-only helpers (gated by IIOS_DEV_TOKENS=1). Never enable in production.
@@ -13,6 +17,9 @@ export class DevController {
constructor(
private readonly inbound: InboundService,
private readonly registry: AdapterRegistry,
private readonly prisma: PrismaService,
private readonly actors: ActorResolver,
private readonly session: SessionVerifier,
) {}
@Post('token')
@@ -55,6 +62,46 @@ export class DevController {
});
}
/**
* 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 };
}
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');
}