Audit Trail System - striae-org/striae GitHub Wiki

Overview

Striae's audit trail system records user and workflow activity for forensic accountability, operational review, and compliance support.

This page focuses on the audit-domain architecture and behavior. Endpoint-level contracts and complete type definitions are maintained in dedicated guides.

Table of Contents

Architecture

High-level flow

graph TB
    A[User action] --> B[AuditService.logEvent]
    B --> C[ValidationAuditEntry]
    C --> D[Audit Worker /audit/]
    D --> E[STRIAE_AUDIT R2 bucket]

    F[Viewer request] --> G[AuditService.getAuditEntriesForUser]
    G --> H[Audit Worker /audit/]
    H --> I[Entries + total]
    I --> J[Client-side filtering]
    J --> K[UserAuditViewer]

    L[Case archive export] --> M[signAuditExport]
    M --> N[Data Worker signing endpoint]
    N --> O[Signed metadata + signature]
    O --> P[Bundled in case archive ZIP]

Storage model

  • Audit data is written by the Audit Worker to the dedicated STRIAE_AUDIT bucket.
  • Audit objects are encrypted at rest via mandatory worker-managed data-at-rest encryption. See Data-at-Rest Encryption — Audit Worker for implementation details.
  • Daily files are organized as audit-trails/{userId}/{YYYY-MM-DD}.json.
  • The primary API is append-oriented (POST and GET). No DELETE endpoint exists; audit entries are immutable once written.

Core Components

1. Audit service

Source: app/services/audit/audit.service.ts

Primary responsibilities:

  • Build normalized ValidationAuditEntry objects via dedicated builder modules.
  • Persist entries to the Audit Worker.
  • Retrieve entries for a user with optional query constraints.
  • Apply client-side filters and pagination when needed.
  • Manage workflow tracking via startWorkflow(caseNumber) / endWorkflow().

The service delegates to a modular internal architecture:

Module Path Purpose
Worker client audit-worker-client.ts fetchAuditEntriesForUser, persistAuditEntryForUser
API client audit-api-client.ts fetchAuditApi with Firebase token auth
Query helpers audit-query-helpers.ts applyAuditEntryFilters, applyAuditPagination, sortAuditEntriesNewestFirst, generateAuditSummary
Console logger audit-console-logger.ts Formatted console output for audit entries
File type utils audit-file-type.ts File type classification
Builders builders/ Specialized param builders per action category (case/file, annotation, workflow, user/security)

Operational behavior:

  • logEvent is best-effort and does not throw to caller on persistence failure.
  • An in-memory buffer is maintained and can be used as a fallback if worker fetch fails.
  • logEvent propagates known badgeId into details.userProfileDetails.badgeId when available so downstream filtering and viewer display can stay consistent.

Badge/ID propagation in audit entries:

AuditService maintains an in-memory userBadgeIdByUserId map across logEvent calls within a session. On each call:

  1. If params.userProfileDetails.badgeId is provided, it is stored in the cache for that userId.
  2. If no badge ID is provided in the current call, resolveBadgeIdForUser checks the cache first, then falls back to Firebase lookup.
  3. The resolved Badge/ID is written into details.userProfileDetails.badgeId on every persisted entry.
  4. confirmation-import entries additionally capture details.reviewerBadgeId to attribute the reviewing examiner's badge number to that specific action.

This allows the UserAuditViewer to filter all entries for a session by Badge/ID, including entries logged before the badge was explicitly provided.

2. Audit export signing

Source: app/services/audit/audit-export-signing.ts

Primary responsibilities:

  • Build signing payloads for audit trail exports bundled with case archives.
  • Delegate to signAuditExportData (in signing-operations.ts) to request server-side signatures via the Data Worker endpoint /api/forensic/sign-audit-export.

Audit trails are:

  • Bundled into encrypted case archive ZIP packages (via case-manage/archive-package-builder.ts).
  • Viewed in the UserAuditViewer component.

3. User audit viewer

Source: app/components/audit/user-audit-viewer.tsx

Primary responsibilities:

  • Render audit entries with action/result/date/case filters.
  • Support Badge/ID filtering and show Badge/ID metadata on matching entries.
  • Build optional case-specific AuditTrail summaries for display.
  • Display archived-case notices and bundled audit trail warnings when applicable.
  • Provide case-scoped Export PDF action from the audit viewer header.

Current props:

interface UserAuditViewerProps {
  isOpen: boolean;
  onClose: () => void;
  caseNumber?: string;
  title?: string;
}

The component delegates to focused subcomponents and hooks:

  • Subcomponents: AuditViewerHeader, AuditUserInfoCard, AuditActivitySummary, AuditFiltersPanel, AuditEntriesList.
  • Hooks: useAuditViewerData (loading, errors, entries, case context), useAuditViewerFilters (filter state and apply/clear logic), useOverlayDismiss (overlay close behavior).

Case audit trail PDF export workflow

Source: app/components/actions/export-audit-pdf.ts

Behavior:

  • Available from the case audit viewer only (case-scoped viewer context).
  • Enforces canAccessCase permission before export.
  • Uses full case history for export and does not follow the active viewer date/action/result filters.
  • Sends export requests to /api/pdf/* with data.reportMode = "audit-trail" and data.auditTrailReport payload.
  • Auto-splits large exports into multiple part files.
  • Includes comprehensive entry detail with raw JSON appendix in generated PDFs.

Archived imported case behavior:

  • For imported archived read-only cases with bundled audit data (bundledAuditTrail.source = archive-bundle), export intentionally includes all bundled entries.
  • This avoids clipping bundled history by import-time case metadata timestamps.

Audit Event Model

Core event shape:

interface ValidationAuditEntry {
  timestamp: string;
  userId: string;
  userEmail: string;
  action: AuditAction;
  result: AuditResult;
  details: AuditDetails;
}

AuditAction categories (full set in app/types/audit.ts):

  • Case management: case-create, case-rename, case-delete, case-archive
  • Confirmation workflow: case-export, case-import, confirmation-create, confirmation-export, confirmation-import
  • File operations: file-upload, file-delete, file-access
  • Annotation operations: annotation-create, annotation-edit, annotation-delete
  • User and session: user-login, user-logout, user-profile-update, user-password-reset, user-account-delete, user-registration, email-verification, mfa-enrollment, mfa-authentication
  • Document generation: pdf-generate
  • Security: security-violation
  • Legacy (backward compatibility): import, export, confirm, validate

AuditResult values: success, failure, warning, blocked, pending.

Action model notes:

  • Several workflow helpers currently log legacy actions plus workflowPhase context (for example export with case-export, import with confirmation).

For complete type definitions, use app/types/audit.ts and API Reference.

Logging Events

Base logging method

await auditService.logEvent({
  userId: user.uid,
  userEmail: user.email || '',
  action: 'annotation-create',
  result: 'success',
  caseNumber: 'CASE-2026-001',
  workflowPhase: 'casework',
  validationErrors: []
});

Common specialized helpers

  • Case workflows: logCaseCreation, logCaseRename, logCaseDeletion, logCaseArchive
  • Export/import workflows: logCaseExport, logCaseImport
  • Confirmation workflows: logConfirmationCreation, logConfirmationExport, logConfirmationImport
  • File workflows: logFileUpload, logFileDeletion, logFileAccess, logPDFGeneration
  • Annotation workflows: logAnnotationCreate, logAnnotationEdit, logAnnotationDelete
  • User session: logUserLogin, logUserLogout
  • User profile/auth: logUserProfileUpdate, logPasswordReset, logAccountDeletionSimple, logUserRegistration
  • MFA and email: logMfaEnrollment, logMfaAuthentication, logEmailVerification, logEmailVerificationByEmail, markEmailVerificationSuccessful
  • Security: logSecurityViolation

Query and Filter Behavior

Primary read method:

const entries = await auditService.getAuditEntriesForUser(userId, {
  requestingUser,
  startDate,
  endDate,
  caseNumber,
  action,
  result,
  workflowPhase,
  offset,
  limit
});

Behavior split:

  • Worker-side query support: userId (required), startDate, endDate.
  • Client-side filtering in audit.service.ts: caseNumber, action, result, workflowPhase, offset, limit.
  • Viewer-layer filtering: Badge/ID filter matches against entry.details.userProfileDetails?.badgeId. For confirmation-import entries, entry.details.reviewerBadgeId is also surfaced in the viewer.
  • Worker default behavior when no date range is provided: returns today's file for the user.

Export-specific read behavior:

  • Case audit PDF export uses a dedicated read path and does not mirror the currently selected viewer filters.
  • For active cases, export query is bounded by full case history (case.createdAt -> now) with case-level filtering.
  • For bundled archived read-only cases, export omits date constraints so all bundled entries are included.

Badge/ID filter behavior notes:

  • Badge ID filtering is client-side only; it cannot be pushed to the worker query.
  • Because AuditService caches the badge ID and backfills it into every entry during a session, entries logged before the first explicit badge ID call will also match once the badge is provided.
  • reviewerBadgeId on confirmation-import entries captures the reviewing examiner's badge. This is separate from userProfileDetails.badgeId (which tracks the session user's badge) and both are shown in the viewer.

Archive Bundling and Signed Integrity

Archive audit trail bundling

When a case is exported as an encrypted archive (ZIP), the audit trail for that case is bundled into the archive as audit/case-audit-trail.json with a corresponding signature at audit/case-audit-signature.json.

This is handled in case-manage/archive-package-builder.ts via the shared buildArchivePackage() function, which calls signAuditExport from audit-export-signing.ts. The builder is invoked by both the initial archiveCase() operation and archived case re-exports from download-handlers.ts.

Signing flow

  • The audit trail JSON is hashed with SHA-256 via calculateSHA256Secure.
  • Audit export signing metadata is built with AUDIT_EXPORT_SIGNATURE_VERSION (1.0).
  • signAuditExport (in audit-export-signing.ts) builds the signing payload and delegates to signAuditExportData (in signing-operations.ts), which sends the request to the Data Worker endpoint /api/forensic/sign-audit-export.
  • The signing payload includes exportFormat, exportType, scopeType, scopeIdentifier, generatedAt, totalEntries, and the SHA-256 hash (uppercased).
  • Bundled audit trails include hash and signature metadata to support downstream verification on import.

API Boundary Summary

Audit entry persistence and retrieval:

  • POST /audit/?userId={userId} — append entry.
  • GET /audit/?userId={userId}[&startDate=YYYY-MM-DD&endDate=YYYY-MM-DD] — retrieve entries (newest-first).

The Audit Worker is called via the env.AUDIT_WORKER service binding from the Pages audit proxy. No shared-secret auth header is required in the proxy-to-worker hop.

Audit Worker validation:

  • POST requires timestamp, userId, and action in body.
  • POST validates that the userId query parameter matches the entry body userId.
  • Additional fields are accepted and persisted.

For full request/response examples and status codes, use API Reference.

Known Behavior and Limitations

  • Audit logging is non-blocking by design: failures are logged and do not typically stop primary workflows.
  • UserAuditViewer does not run continuous polling; data is loaded on open and when filters/time range are applied.
  • getAuditEntriesForUser may fall back to in-memory buffered entries when worker retrieval fails.
  • The Audit Worker returns entries sorted newest-first by default.
  • The in-memory audit buffer grows throughout the session and is not cleared automatically.
  • No DELETE endpoint exists; audit entries are immutable once written.
  • Some export/report output structure is intentionally verbose to preserve forensic context.
  • Imported archived read-only cases may rely on bundled audit trail entries (bundledAuditTrail.source = archive-bundle) for viewer context.
  • If an archived package is imported without bundled audit entries, the viewer surfaces a warning and case-level audit data may be unavailable.
  • Case audit PDF export includes a helper note in the viewer indicating that export always uses full case history regardless of active filter selections.

Troubleshooting

No entries returned

  1. Confirm userId is present in request path/query flow.
  2. Verify selected date window includes expected events.
  3. Check that app is pointing to the correct audit worker URL.

Export failures

  1. Verify Data Worker signing endpoint is reachable.
  2. Verify signing configuration and auth key retrieval are valid.
  3. Check browser download restrictions if file generation succeeded but no download appears.

Unexpected filter results

  1. Validate whether filter is worker-side or client-side.
  2. Check legacy action plus workflow-phase combinations (import/export/confirm).

Related Documentation