/** * Capability-use example — Phase 06 #23. * * One handler that touches every "regular" capability so the audit- * trail page shows a non-trivial set of rows per invocation: * 1. storage.read to fetch the tenant's Slack webhook URL * 2. http.outbound to POST a notification to Slack * 3. log everywhere for structured tracing * * When run with the host audit recorder wired in, each capability call * emits one PluginCapabilityUse row with outcome=success / error and a * sha256(payloadDescriptor). Tenant admins see exactly which channel * received which event without the message body ever being persisted. */ import { defineHook } from "../../src/index.js"; export interface EntityCreatedInput { readonly tenantId: string; readonly entityName: string; readonly recordId: string; } export const handleEntityCreated = defineHook( async (input, ctx) => { ctx.log.info( { tenantId: input.tenantId, entity: input.entityName }, "capability-use.handleEntityCreated invoked" ); if (!ctx.storage) { ctx.log.warn({}, "storage capability not bound — manifest must declare storage.read"); return { posted: false }; } const webhookUrl = await ctx.storage.get(`slack:${input.tenantId}:webhook-url`); if (!webhookUrl) { ctx.log.info({ tenantId: input.tenantId }, "no slack webhook configured — skipping"); return { posted: false }; } if (!ctx.http) { ctx.log.warn({}, "http capability not bound — manifest must declare http.outbound"); return { posted: false }; } const body = JSON.stringify({ text: `New ${input.entityName} created (id ${input.recordId})`, }); const response = await ctx.http.fetch({ url: webhookUrl, method: "POST", headers: { "Content-Type": "application/json" }, body, }); if (response.status >= 200 && response.status < 300) { ctx.log.info({ status: response.status }, "slack webhook posted"); return { posted: true }; } ctx.log.error( { status: response.status, body: response.body.slice(0, 256) }, "slack webhook returned non-2xx" ); return { posted: false }; } );