Skip to main content

Shared package API

Part of the Universal Audit Log Specification. The TypeScript surface of @constellation-platform/auditauditCritical(), the audit-action shape, helpers for routine (non-critical) audit, and how the package composes with the transactional outbox to keep audit + mutation atomic.

10. Shared Package API

10.1 @constellation-platform/db

@constellation-platform/db SHALL remain the canonical low-level API.

It SHALL own:

  • auditAction(tx, opts)
  • auditBatch(tx, entries[])
  • queryAuditTrail(tx, opts)
  • countAuditEntries(tx, opts)

AuditActionOptions SHALL be extended to include:

  • organisationId?: string
  • parentResourceType?: string
  • parentResourceId?: string
  • context?: Record<string, unknown>
  • sessionId?: string
  • userAgent?: string
  • outcome?: 'SUCCESS' | 'FAILURE' | 'DENIED'
  • durationMs?: number

10.2 @constellation-platform/audit

@constellation-platform/audit SHALL evolve into the shared high-level layer.

It SHALL provide:

  • auditCritical(tx, opts) — current behaviour retained; MUST redact before publishing to outbox (see Section 12.3)
  • buildAuditDiff(before, after, opts?) — computes normalised diffs (see Section 10.4)
  • createAuditor(ctx) — factory for scoped audit helpers (see Section 10.3)
  • withAuditedMutation(tx, opts, mutationFn) — higher-order ceremony wrapper (see Section 10.3)
  • extractRequestAuditMeta(request) — normalised ipAddress (truncated), userAgent, sessionId, optional request context

10.3 Developer Ergonomics

The 7-step audit ceremony (Section 6.2) requires many repeated parameters. Two helpers SHALL reduce boilerplate:

createAuditor(ctx)

Factory that closes over common context, returning a scoped helper:

const auditor = createAuditor({
tenantId: ctx.tenantId,
actorId: ctx.actorId,
organisationId: ctx.organisationId,
correlationId: ctx.correlationId,
ipAddress: ctx.ipAddress,
sessionId: ctx.sessionId,
userAgent: ctx.userAgent,
});

// Scoped helper — only action-specific params needed
await auditor.mutation(tx, {
action: 'UPDATE',
module: 'projects',
resourceType: 'projects.task',
resourceId: task.id,
parentResourceType: 'projects.project',
parentResourceId: task.projectId,
before,
after,
});

This reduces per-call parameters from ~13 to ~5.

withAuditedMutation(tx, opts, mutationFn)

Higher-order helper that wraps the full 7-step ceremony:

await withAuditedMutation(
tx,
{
auditor,
action: 'UPDATE',
resourceType: 'projects.task',
resourceId: task.id,
},
async (tx) => {
const before = await repo.findById(tx, task.id);
const result = await repo.update(tx, task.id, data);
return { before, after: result };
},
);

The helper calls mutationFn to obtain { before, after }, computes the diff via buildAuditDiff(), writes the audit entry, and returns the mutation result. If the mutation throws, the transaction rolls back normally with no audit row.

10.4 buildAuditDiff Specification

buildAuditDiff(before, after, opts?) computes a normalised field-level diff.

Output format:

{ [fieldName: string]: { before: unknown; after: unknown } }

Behaviour rules:

  • Only changed fields are included in the output.
  • Nested objects are flattened with dot notation up to configurable depth (default: 3). Example: address.city.
  • Arrays are stored as before/after of the full array, not per-element diffs.
  • Maximum diff size: 64 KB after JSON serialisation. If exceeded, the diff is truncated and a _truncated: true flag is added.
  • The diff populates the changed_fields column with the top-level keys present.

Options:

interface BuildAuditDiffOptions {
redact?: RedactionPolicy;
ignoreFields?: string[];
maxDepth?: number; // default: 3
maxSize?: number; // default: 65536 (64 KB)
}

10.5 What Stays Out of the Platform Package

The platform package SHALL NOT own:

  • module-specific display strings
  • module-specific permission rules
  • module-specific route handlers
  • module-specific field redaction logic beyond generic hooks

Those remain app/module concerns built on top of the shared audit primitives.