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:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user