feat(documents): view and download the original inbound document

The inbound-document store keeps the uploaded doctor letter encrypted at
rest (InboundDocument.contentEncrypted, AES-256-GCM), but there was no way
to read the original back — the decrypt helper had been dropped as unused.

- Restore decryptDocumentFromBytes in the document store, fail-closed on a
  bad / missing key id.
- Add GET /api/documents/inbound/[id]/original: owner-scoped (userId-narrowed
  query), module-gated, returns the decrypted bytes with the stored MIME
  type. A PDF / image is served inline so the browser can render it; other
  types download as an attachment. The filename is sanitised before it
  reaches the Content-Disposition header. The bytes are PHI — never logged,
  never served cross-user, and a decrypt error fails closed (500) rather than
  returning ciphertext. Cache-Control is private, no-store.
- Surface a 'View original' action in the document review screen.
- Document the route in the OpenAPI registry and regenerate the spec.
- Cover the route with a test: owner-scoped success, attachment fallback,
  404 for another user's document, and fail-closed on decrypt error.

i18n: viewOriginal across all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 20:33:29 +02:00
1 parent 6e204d054a
commit 08314c7c09
12 files changed
+418 -10

No files matched your search

+34
View File
@@ -8696,6 +8696,40 @@ paths:
"401": *a1
"422": *a3
"429": *a4
/api/documents/inbound/{id}/original:
get:
tags:
- Documents
summary: View / download the original document
description: "Decrypts and serves the raw uploaded document (the bytes stored encrypted at rest under
`InboundDocument.contentEncrypted`). Owner-scoped + gated on the opt-in `inboundDocuments` module. The response
carries the stored `mimeType`: a PDF / image is served inline (`Content-Disposition: inline`) so the browser can
render it; any other type downloads as an attachment. Fails closed (500) on a decrypt error — it never returns
the ciphertext."
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The decrypted original document bytes with the stored MIME type.
content:
application/pdf:
schema:
type: string
format: binary
image/*:
schema:
type: string
format: binary
text/plain:
schema:
type: string
"401": *a1
"422": *a3
"429": *a4
/api/documents/inbound/{id}/facts/{factId}:
patch:
tags: