feat(p9): thread purpose + persist consent receipt through OutboundService

Task S2.2: OutboundService.send gains an optional purpose, passes it to the broker,
and persists the returned consentReceiptRef on IiosOutboundCommand (new nullable
column, migration outbound-consent). RouteService.execute passes 'route_forward';
calendar reminders pass 'meeting_reminder'. CMP-DENY → command BLOCKED, no send.
2 tests: receipt recorded on allow, purpose-DENY blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 21:26:01 +05:30
parent 2223d7a891
commit cdc59be481
6 changed files with 35 additions and 6 deletions
@@ -27,6 +27,7 @@ export class OutboundService {
payload: Record<string, unknown>,
idempotencyKey: string = randomUUID(),
scopeId?: string,
purpose?: string,
) {
const existing = await this.prisma.iiosOutboundCommand.findUnique({ where: { idempotencyKey } });
if (existing) return existing;
@@ -45,7 +46,7 @@ export class OutboundService {
let result;
try {
result = await this.broker.invoke({ capability: 'channel.send', channelType, target, payload, idempotencyKey, scopeId });
result = await this.broker.invoke({ capability: 'channel.send', channelType, target, payload, idempotencyKey, scopeId, purpose });
} catch (err) {
// Fail-closed (e.g. PolicyDeniedError): mark the command FAILED, record the attempt, propagate.
await this.prisma.$transaction([
@@ -56,7 +57,10 @@ export class OutboundService {
}
const [updated] = await this.prisma.$transaction([
this.prisma.iiosOutboundCommand.update({ where: { id: cmd.id }, data: { status: result.outcome, providerRef: result.providerRef } }),
this.prisma.iiosOutboundCommand.update({
where: { id: cmd.id },
data: { status: result.outcome, providerRef: result.providerRef, consentReceiptRef: result.consentReceiptRef },
}),
this.prisma.iiosDeliveryAttempt.create({
data: { commandId: cmd.id, attemptNo: 1, status: result.outcome, providerRef: result.providerRef, latencyMs: result.latencyMs, errorCode: result.errorCode },
}),