Repository navigation
feat(audit): AuditEvent table and write path for existing state transitions - #158
Merged
afeezorobsco-cyber merged 1 commit intoOct 18, 2026
Conversation
…itions - AuditEvent model + migration: actor user/role, entity type/id, action, from/to state, request id, minimal validated metadata, createdAt. Append-only: no update/delete path in code, and DB triggers reject UPDATE/DELETE (row) and TRUNCATE (statement). No FKs, on purpose. - New audit module. record(tx, actor, event) refuses the root client, so an event can only be written inside the transaction of the change it describes. Metadata is limited to flat scalars, bounded, and keys that could carry PHI or secrets are refused. - X-Request-Id middleware (keeps a sane inbound id, otherwise mints a UUID); controllers pass auditActor(req). - Wired into appointment booking (inside bookSlot's existing transaction, after the slot claim), doctor verification (previous status read FOR UPDATE; a no-op records nothing), and payment fund/release/refund (status writes conditional on the observed state; a racing second action gets 409 and records nothing). - GET /admin/audit-events: admin-only, entity/actor/date filters, (createdAt, id) keyset cursor, limit capped at 200. - Escrow SDK test seam so the payment flows are tested without testnet. Phase 3/4 transitions are explicitly left to wire when they're built. Closes DocForum#43 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@zainabwahab-eth Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This was referenced Oct 18, 2026
zainabwahab-eth
added a commit
to zainabwahab-eth/docforum-core
that referenced
this pull request
Oct 19, 2026
Brings in DocForum#158 (AuditEvent), DocForum#147 (access-control matrix) and DocForum#150 (field-level encryption), and fixes what broke CI on the PR merge ref: - Conflict resolution keeps both sides in app.ts, appointments, payments and doctors services/repositories, using DocForum#43's tx-aware updateVerificationStatus/markEscrowed/markStatus. - The patient-export suite moved from port 5438 to 5442. DocForum#147's access-control suite also uses 5438, and the parallel integration files collided. - The three /patients/me/export routes are added to DocForum#147's access-control matrix (KNOWN_ROUTES + expected status per actor). - ARCHITECTURE: the export model section is renumbered to §4.11 (§4.10 is now the audit trail). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zainabwahab-eth
added a commit
to zainabwahab-eth/docforum-core
that referenced
this pull request
Oct 19, 2026
Brings in DocForum#158 (AuditEvent), DocForum#147 (access-control matrix) and DocForum#150. Conflicts in app.ts, ROADMAP.md and docs/api resolved by keeping both sides. DELETE /patients/me is added to DocForum#147's access-control matrix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zainabwahab-eth
added a commit
to zainabwahab-eth/docforum-core
that referenced
this pull request
Oct 19, 2026
Brings in DocForum#158 (AuditEvent), DocForum#147 (access-control matrix) and DocForum#150. - bookSlot keeps both changes: bookSlot(actor, slotId, patientProfileId?). Guardian authorisation (resolveActingProfile) still runs before the transaction, and DocForum#43's audit write stays inside it. The event's metadata now names the patientProfileId booked for, so a guardian booking is attributable (actor = guardian), and an integration test asserts it. - app.ts mounts both /patients and /admin. - POST/GET /patients/me/dependents are added to DocForum#147's access-control matrix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #43
What changed
AuditEventmodel (ARCHITECTURE.md §4.10, migration20261017212335_add_audit_event). Fields:actorUserId,actorRole,entityType,entityId,action,fromState,toState,requestId,metadata(jsonb),createdAt. Indexes on(entityType, entityId, createdAt),(actorUserId, createdAt)and(createdAt, id).insertAuditEventandfindAuditEvents, and a test asserts exactly that). In the database, aBEFORE UPDATE OR DELETErow trigger and aBEFORE TRUNCATEstatement trigger reject mutation. A future retention policy has to drop and recreate them in its own reviewed migration.ON DELETEaction would have to rewrite audit rows, which the trigger forbids.RESTRICTwould let the audit trail block account lifecycle (e.g. [Phase 8] Account deletion with clinical-record retention: anonymise identity, keep the audit trail #41's purge). Ids are recorded as facts.auditmodule (routes/controllers/services/repositories):record(tx, actor, event)refuses the root Prisma client at runtime, so it can only run inside the transaction of the change it describes. If the change rolls back, so does its event.name,email,phone,dob,notes,intake,symptom,password,secret,token,key,body,payload, ...) is refused. Callers pass only ids and codes.X-Request-Idor mints a UUID, and echoes it on every response. Controllers passauditActor(req)(user id, role, request id).Wired transitions. States are taken from the real implementation, not invented:
appointment.bookednull → scheduledbookSlot's existing transaction, afterclaimOpenSlotand theAppointmentinsert. The concurrency mechanism is untouched; race losers fail at step 1 and record nothing.doctor.verification_status_changedFOR UPDATEin the same transaction, so concurrent admins each record the real from-state. Setting the same status records nothing.payment_intent.funded/.released/.refundedcreated → escrowed,escrowed → released,escrowed → refundedBehaviour change in payments, needed for a truthful from-state. The status writes are now conditional on the observed state (
UPDATE ... WHERE status = 'escrowed'). Previously a racing second fund/release/refund silently overwrote the first; now it gets409and records nothing. If the on-chain call already succeeded for the losing request, the ledger is ahead of the database. That gap already existed for crashes at the same point; reconciling it is #24's scope (fundPaymentIntentcheck-then-act).GET /admin/audit-events(admin only):entityType,entityId,actorUserId,from/to.limitis 1–200 (default 50), so there's no unbounded dump. Unknown parameters return400.(createdAt DESC, id DESC)with an opaque keyset cursor on that pair, returned as{ items, nextCursor }. No existing pagination pattern was in the repo to reuse.Docs
auditmodule, and the rule for Phase 3/4) and §4.10 (model and recorded transitions)docs/api/README.md(endpoint,X-Request-Id, the payments409)Tests
tests/integration/audit.test.mts(20, own embedded Postgres on port 5441). These run the real flows over HTTP, not the service in isolation. The escrow SDK is faked through a new test seam (setEscrowSdkForTests).BEFORE INSERTtrigger that fails the audit write, installed in the test DB (no mocks).500, the slot staysopen, there is no appointment and no event. The retry then succeeds.pending→verified→rejectedrecorded with the admin actor; a no-op set records nothing.created→escrowed→releasedandescrowed→refunded, with tx hash in metadata and no platform secrets (sentinel values checked)escrowedand records noreleasedevent409and no event200and one409, and exactly one terminal event matching the final statusupdate,updateMany,delete,deleteManyandTRUNCATEare all rejected by the database, and the rows are unchanged.record()with the root client is refused.401and403for patient and doctor400createdAt: every event exactly once, in a total order, across 5 pages, with a newer event inserted mid-scan not disturbing the remaining pages; a repeat scan is deterministictests/unit/audit.test.ts(26): metadata rules (16 forbidden-key cases, nested values, long strings, NaN, too many keys),record()guards, cursor round-trip and rejection, query schema, request-id middleware.Mutation checks:
record()into a separate transaction after the booking fails the booking and rollback tests.These were run in Ubuntu (WSL) on Node 20 to match CI, with
--hookTimeoutraised locally (the existing suites'beforeAllexceeds 10s on this machine).tests/testnet-integration/payments.test.mtswas updated for the newactorargument and typechecks, but was not run: it needs live testnet and funded platform identities.Not done here (explicitly)
record()in its own transaction when built.POST /auth/login-attempts/clearstill logs to console only. It's out of the issue's three flows, and the comment and docs now say so.Notes for reviewers
backend/src/modules/audit/(forAccessLog, read-access logging, a different concern). Whichever merges second needs to fold the two into one module.app.ts,ROADMAP.mdandARCHITECTURE.md. Expect trivial doc and line conflicts only.🤖 Generated with Claude Code