1 authoritative audit event schema and projections
forgejo-actions edited this page 2026-07-15 12:43:13 +00:00

Authoritative Audit Event Schema And Projections

Contract

AuthoritativeAuditEvent is the shared in-memory and serializable shape used to describe command, admin, and authentication authority decisions. It normalizes actor/subject identity and outcomes while allowing each lane to retain typed source records and lane-specific persistence.

Field Contract
lane command, admin, or auth.
request_id Optional correlation id. Command and current admin adapters set it; current auth events do not.
action_kind Stable action label within the lane.
actor / subject Principals identifying who initiated/owned the event and what it affected.
outcome Normalized accepted/rejected or success/failure result.
mutation_ids Typed gameplay mutation boundary ids; omitted from JSON when empty.
message Human-readable audit summary, not a replacement for structured fields.
metadata Sorted string-to-string lane metadata; omitted when empty.
replay Command family and replay flag for events that can project into gameplay replay.
idempotency Shared source, request, mutation, scope, and retry/replay envelope for idempotent commands.

The event currently has no explicit schema-version field. Treat field names, enum spellings, principal conventions, and serialized optional/default behavior as compatibility-sensitive whenever stored JSON or downstream exports consume it.

Lanes And Outcomes

AuthoritativeAuditLane::Command covers gameplay actions and trigger resolution. Admin covers operator actions such as session moderation or encounter administration. Auth covers login/session access outcomes emitted by the login server.

Command and admin decisions normally use Accepted or Rejected. Auth uses Success or Failure. is_positive() deliberately treats Accepted and Success alike; generic Postgres projection writes this result to its accepted boolean, and replay projection uses it as the normalized accepted flag. Do not collapse the original outcome string when consumers need to distinguish command acceptance from operation success.

Principals

AuthoritativeAuditPrincipal contains a free-form kind, stable string id, and optional display name. Current conventions include account, character, operator, admin_target, service, and auth subjects such as account or username.

Use an immutable id for correlation and reserve display_name for human-facing context. with_display_name trims input and discards an empty value. Principal kinds are strings rather than a closed enum, so adapters and queries must keep their spelling stable; the shared type does not validate kind or reject an empty id.

Current lane mappings are:

  • Command: account actor and character subject; character display name is captured at command time.
  • Admin: operator account actor with username display name, and an admin_target subject.
  • Auth: service:login-server actor and a route-selected subject kind/id.

Idempotency Envelope

AuthoritativeCommandIdempotencyEnvelope contains source, source-local request_id, mutation ids, persistence_scope, and replay metadata. Sources are realtime_gameplay, trigger_authority, economy_transaction, persistence_retry, cross_instance_transfer, and system. Scopes are none, session, character, realm, and global.

AuthoritativeAuditEvent::command infers realtime_gameplay from gameplay_action, trigger_authority from trigger_resolution, and system from any other command type. It currently assigns character persistence scope. Callers with a different ownership or durability boundary must construct the appropriate envelope explicitly instead of relying on that default.

with_mutation_ids sorts by each mutation's boundary string and removes duplicates. The stable key is:

<source>:<persistence_scope>:<request_id>:<mutation-id|mutation-id|...>

An empty mutation list uses none. Replay metadata (replayed, attempt, session token, retry job id) is intentionally excluded from the stable key, so retries and replays retain the logical command identity.

Normalization has two important limits. The command constructor keeps the caller's original mutation-id vector on event.mutation_ids, while the embedded envelope stores the sorted/deduplicated vector. Replay projection normalizes again before producing its record. Direct struct construction can also bypass constructor defaults entirely. Use the command constructor or AuthoritativeCommandAuditRecord builder path when idempotency and replay are required, and compare the envelope key rather than raw event mutation ordering when diagnosing duplicate handling.

Projections

trace_projection() returns target authoritative_audit and a sorted string field map containing lane, action, outcome, actor/subject, message, optional request id, idempotency key/source/scope, and metadata.<key> entries. Lane emitters log additional native fields: the command emitter logs mutation ids, while admin and auth emitters include their metadata and the projection map.

postgres_projection(table) is a row-like adapter, not a database write. It projects common lane, request, principal, outcome, message, metadata, and idempotency columns. It does not add an audit id, timestamp, lane-specific payload columns, or execute SQL. A persistence owner must map the projection to a real migration and transaction.

replay_projection_input() returns a value only when event.replay is present. It carries command type, request id, action, positive outcome, replay flag, mutation ids, and message. replay_projection() then sorts and deduplicates mutation ids into GameplayAuthorityReplayAuditRecord. Admin and auth adapters set replay to None, so they do not project into gameplay fixtures.

AuthoritativeCommandAuditRecord is the typed command adapter. It builds the shared event, inserts session/realm/source/target/revision metadata, places the session token in replay metadata, and exposes the normalized replay record. Prefer this adapter when starting from game-server command state.

Metadata And Persistence Caveats

Metadata values are strings. Source/target tiles are encoded there as x,y, and revision numbers are decimal strings. Typed command columns still remain the query-friendly source for those values in command_audit_log; metadata is useful for common export and trace projection, not for replacing typed database columns.

Persistence differs by lane:

  • Command events are logged and persisted to command_audit_log. The row stores typed command/session/placement fields, mutation ids, full audit_event JSONB, optional normalized replay_payload JSONB, persistence_outcome, message, and timestamp. Indexes support character history, action/outcome review, and command type plus request id.
  • Admin actions emit a shared admin event, but admin_action_audit_log persists the existing typed admin record and request/response JSON. The current SQL writer does not store the generic event or postgres_projection() map.
  • Auth success/failure emits the shared authoritative_audit trace and a legacy auth trace. The current adapter does not persist a shared audit row.

Do not assume that serializing an event or calling postgres_projection() makes it durable. Also do not assume replay_payload is present only when a row's replayed flag is true: command persistence projects any command event carrying replay metadata, and the normal command builder always supplies that projection. Use the payload's own replayed flag.

GameplayCommandAuditPersistenceOutcome is separate from event outcome. accepted can coexist with state_queued or outbox_queued; incident review must inspect both authority outcome and durability outcome.

Usage

Build command events through the typed source record when the complete server context exists:

let event = record.audit_event();
let trace = event.trace_projection();
let idempotency_key = event.idempotency.as_ref().map(|value| value.stable_key());
let replay = event.replay_projection();

Use a direct AuthoritativeAuditEvent literal for admin or auth only when the adapter deliberately has no command idempotency/replay contract. Set actor and subject semantics explicitly, use the lane's outcome family, and keep machine-queryable facts in structured metadata instead of embedding them only in message.

Run the code-backed contract tests after changing fields, normalization, adapters, logs, or migrations:

cargo test -p aether-world-domain authoritative_audit
cargo test -p aether-game-server command_audit
cargo test -p aether-game-server admin_action_audit_projects_to_shared_audit_event_and_postgres_projection
cargo test -p aether-login-server auth_audit_context_builds_shared_success_and_failure_events

Debugging

For one command, start with command_type plus request_id, then compare accepted, replayed, and persistence_outcome. Inspect audit_event.idempotency for source/scope/key, and compare normalized mutation ids with the replay payload. If event mutation order differs but the envelope and replay record agree, normalization is working as designed.

For missing traces, filter on tracing target authoritative_audit and lane. Auth also emits legacy fields audit = "auth" and action; this is a compatibility trace, not a second authority decision. Admin and command emitters have lane-specific message text, so the target and structured lane are more reliable filters than the prose log message.

For missing durable rows, follow the lane owner rather than the generic projection. Command rows use persist_command_audit, admin rows use persist_admin_action_audit, and auth currently has no shared row writer. A valid trace therefore does not by itself prove persistence.

For replay drift, compare the stored replay_payload with the normalized event projection, then run the gameplay replay fixture tests. Audit drift usually points to command labels, accepted/replayed classification, mutation ids, or message changes; state and wire drift belong to their separate fixture projections.