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?: stringparentResourceType?: stringparentResourceId?: stringcontext?: Record<string, unknown>sessionId?: stringuserAgent?: stringoutcome?: '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)— normalisedipAddress(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: trueflag is added. - The diff populates the
changed_fieldscolumn 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.