SuperAPI AI Directory

adding-audit-events

Security & auditing

View repository

Updated 10-16 01:33

About this skill

Adding Audit Events

AuditEventTable in packages/types/lib/audit-trail/event.ts is the source of truth: it maps each resource to its actions and the metadata each may carry. The event union, the metadata lookup, the per-resource action lists and the webapp filters all derive from it. Add the action there first and let the compiler tell you what else to touch.

The trail is control-plane only — configuration, state and authentication. Runtime traffic (records, proxy, sync execution) is data plane and stays out; audit it and you get millions of rows a month.

Workflow

  1. Vocabulary — in AuditEventTable, under the resource: <action>: <MetadataShape>; or <action>: never; when the action carries no metadata.

  2. Metadata shape, if any — packages/types/lib/audit-trail/metadata.ts. Name it for what it holds, never for one action: integration.created and integration.deleted share IntegrationProviderMetadata because both record only the provider.

  1. Endpoint type — every customer-facing endpoint declares one:

    • Audit: AuditPolicy<'<resource>', '<action>', 'account' | 'environment'>
    • Audit: { kind: 'no-audit'; reason: '<why>' } — use 'data-plane operation' for runtime traffic.
  2. Spec — packages/server/lib/middleware/audit/<resource>.middleware.ts:

    export const auditThingDone = auditable<PostThing>({
        policy: Audit.auditable({ resource: 'thing', action: 'done', scope: 'environment' }),
        target: (req) => makeTarget('thing', nonEmptyString(req.body.id)),
        metadata: (req) => omitUndefined<ThingDoneMetadata>({ … })
    });
    

    Mounted middleware first, in the vocabulary's action order, private spec then public for the same action; multi-action emitters last; helpers below all of them.

  3. Barrel — add the export to audit/index.ts. Hand-written on purpose: it declares what has scope beyond the folder, so never export *.

  4. Mount — routes.public.ts / routes.private.ts, before withScope: .post(apiAuth, auditThingDone, withScope('…'), handler). After the scope check, denials are lost.

  5. Test — <resource>.middleware.unit.test.ts, scaffolding from ./testing.js.

  6. Webapp — add the action to actionsByResource in packages/webapp/src/pages/Audit/constants.ts. A type check pins it to the vocabulary, so omitting it is a build error. Labels are derived.

  7. New kind of target — add it to AuditTargetType.

Gotchas

  • Resolvers run before zod. req.body / params / query are raw at that point, whatever the endpoint type says. Use the guards in input.ts: nonEmptyString, positiveInt, param, query, bodyField.
  • target and metadata resolve before next(), so a value the handler generates isn't available yet. Use targetFromResponse / metadataFromResponse.
  • A 403 means no handler ran, so anything only the handler knows is missing from exactly the rows an auditor cares about most. Prefer reading from the request.
  • scope: 'account' nulls the event's environment, whatever res.locals holds.
  • On the MCP path only (defineManagementMcpTool), a stray metadata key alongside a valid one is accepted — the audit type is a union over the vocabulary. A stray key alone, a wrong type, and metadata on a never action all fail.
  • Grep input.ts and lookups.ts before adding a helper. The target and metadata builders usually exist, and near-identical …Target functions get flagged in review.

Review Checklist

  • npm run ts-build clean — it catches a missing webapp entry, metadata that doesn't fit the action, and metadata on an action declared never
  • Unit test asserts the common fields (accountId, environment, actor, outcome), not only the one it is named for
  • Test break-checked: remove the target or a metadata key and confirm it goes red
  • Audit middleware sits before withScope on every new mount
  • Denial and failure paths still identify the event — check what a 403 records, not just the 200