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
- Overview
- Architecture
- Core Components
- Audit Event Model
- Logging Events
- Query and Filter Behavior
- Export and Signed Integrity
- API Boundary Summary
- Known Behavior and Limitations
- Troubleshooting
- Related Documentation
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_AUDITbucket. - 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 (
POSTandGET). NoDELETEendpoint exists; audit entries are immutable once written.
Core Components
1. Audit service
Source: app/services/audit/audit.service.ts
Primary responsibilities:
- Build normalized
ValidationAuditEntryobjects 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:
logEventis 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.
logEventpropagates knownbadgeIdintodetails.userProfileDetails.badgeIdwhen 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:
- If
params.userProfileDetails.badgeIdis provided, it is stored in the cache for that userId. - If no badge ID is provided in the current call,
resolveBadgeIdForUserchecks the cache first, then falls back to Firebase lookup. - The resolved Badge/ID is written into
details.userProfileDetails.badgeIdon every persisted entry. confirmation-importentries additionally capturedetails.reviewerBadgeIdto 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(insigning-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
UserAuditViewercomponent.
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
AuditTrailsummaries 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
canAccessCasepermission 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/*withdata.reportMode = "audit-trail"anddata.auditTrailReportpayload. - 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
workflowPhasecontext (for exampleexportwithcase-export,importwithconfirmation).
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. Forconfirmation-importentries,entry.details.reviewerBadgeIdis 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
AuditServicecaches 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. reviewerBadgeIdonconfirmation-importentries captures the reviewing examiner's badge. This is separate fromuserProfileDetails.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(inaudit-export-signing.ts) builds the signing payload and delegates tosignAuditExportData(insigning-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:
POSTrequirestimestamp,userId, andactionin body.POSTvalidates that theuserIdquery parameter matches the entry bodyuserId.- 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.
getAuditEntriesForUsermay 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
DELETEendpoint 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
- Confirm
userIdis present in request path/query flow. - Verify selected date window includes expected events.
- Check that app is pointing to the correct audit worker URL.
Export failures
- Verify Data Worker signing endpoint is reachable.
- Verify signing configuration and auth key retrieval are valid.
- Check browser download restrictions if file generation succeeded but no download appears.
Unexpected filter results
- Validate whether filter is worker-side or client-side.
- Check legacy action plus workflow-phase combinations (
import/export/confirm).