Skip to main content

Shared package API

Part of the Universal Audit Log Specification. The TypeScript surface of @constellation-platform/audit — auditCritical(), 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.