Zum Inhalt springen
Docs

Befehlspalette

Zu einer Seite oder Ansicht springen oder ein Todo per Name finden.

Änderungen

KIM through the service's own client module

The repository's architectural decision, rationale and consequences.

Markdown

Status: accepted · Date: 2026-11-01

Enforcement: packages/api/src/platform/kim/ ships the KimTransport interface and both implementations; createKimTransport() picks fake or smtp by env.KIM_TRANSPORT; packages/api/src/features/reports/reports.jobs.ts calls the transport only through this interface; reports.deliver.test.ts asserts the fake delivery path, the lease retry on failure, the KEK-wrapped credentials and the print-in-memory invariant.

Context

Ticket 56 ships the one send path a nurse touches in v1 (US-31, F4, C5): a report leaves the device over KIM, or as a share link, or straight to the printer. KIM is the Telematikinfrastruktur's secure mail: delivery over a service-run SMTP bridge with STARTTLS 587 to a specialist operator, authenticated with the service's SMC-B certificate. There are two things we cannot do from the Cloudflare Workers / Expo runtime:

  • Open a raw SMTP connection from the device or a Worker — neither environment exposes raw TCP + STARTTLS, and we never want the device holding KIM credentials.
  • Call a vendor-hosted REST API from either side — none of the KIM bridges we evaluated expose one as of this writing.

A third option is a service-run KIM bridge (self-hosted container, Cloudflare Tunnel to the bridge, or a managed KIM-SMTP relay) addressed by a per-organization address pair + credentials. The bridge speaks SMTP + STARTTLS + S/MIME, which Workers can reach over fetch-less transports is still [TBD], but a worker-mailer SMTP client over an outbound TCP connection is the current working plan (plan §1.8). The bridge is not provisioned in v1; the API ships the interface so the owning team can plug the bridge in without touching the feature code.

Dev and tests must still exercise the whole flow without hitting a bridge.

Decision

Reports feature talks to KIM through a tiny internal KimTransport interface implemented in two places:

  • kim.fake.ts — selected only by KIM_TRANSPORT=fake with WUNDKLAR_DEV_KIM_INBOX=1, a local BETTER_AUTH_URL, and a non-production runtime. It reports delivered with the supplied Message-ID and keeps the decrypted PDF in memory. /dev/kim-inbox additionally requires VITE_WUNDKLAR_DEV_KIM_INBOX=1 in a development build.
  • kim.smtp.ts — the default transport (KIM_TRANSPORT=smtp): a placeholder that reports a retryable failure code until the service-run bridge is wired in. The leased reportsKim job retries with backoff — nothing is lost, the operator sees the failure in kim_outbox.last_error.

Per-organization credentials (SMTP host, port, user, password) are KEK-wrapped under the organisation's ODK with purpose kim and stored in care_service.kim_config_ct. reports.kimConfig returns only the public KIM address + a hasCredentials boolean; the ciphertext never leaves the server. reports.kimTest runs a single KimTransport.send and reports a status code only.

Uncertain delivery and retries

Before invoking the transport, the job records an attempt delivery event under its fenced lease and supplies a stable Message-ID derived from the delivery ID. The bridge does not yet promise Message-ID deduplication. If a send may have been accepted but its outcome is lost, or a lease is reclaimed with an unfinished attempt, the job records unknown and stops. The nurse must check the recipient's inbox before another send. An explicit KIM_BRIDGE_UNCONFIGURED response confirms that no send was attempted; it remains queued with backoff without an attempt limit. Lease-token checks fence all settlement and release writes.

Printing

The device decrypts the stored PDF in memory. Native builds pass a PDF data URI to the Expo print dialog; web uses a short-lived blob URL. The native module is patched and built from source so PDF bytes cannot become the print job name or an error message. A runtime marker refuses printing with an older or unpatched native binary. The operating system manages its own print spool. A report.print_requested trail event records the dialog handoff: Android does not report whether the user actually printed or cancelled.

Why not route KIM through a shared email package

packages/email is the Mailchimp-driven marketing / transactional path (ticket 76). KIM is a different lifecycle:

  • Credentials are per-organization and KEK-wrapped, not a single global secret.
  • Delivery writes delivery_event + a report.delivered trail event + schedules a report_no_reply reminder — none of which packages/email knows about.
  • The job leases through kim_outbox and respects clinicalJobAccess ("write"); emails do not.

A shared package would collapse these semantics into one surface and surface the KIM state in the wrong feature. The ticket's acceptance criteria name the KIM lifecycle fields directly.

Consequences

  • Adding a KIM provider is a single file in packages/api/src/platform/kim/ + a two-line change in index.ts. The job, the trail receipt and the reminder stay the same.
  • The device never holds KIM credentials and never speaks SMTP. The transport is strictly a server-side concern.
  • Credentials never cross the API boundary: kimConfig.get reports presence, kimConfig.set wraps and stores, kimTest runs the transport without echoing the credentials.
  • /dev/kim-inbox is a dev-only route: the backend returns 404 unless KIM_TRANSPORT=fake and the Vite build strips the component chunk out of a production bundle (same shape as /dev/enrol, ticket 29).
  • The bridge is still [TBD]; the production transport reports KIM_BRIDGE_UNCONFIGURED and the row stays queued. This is intentional — the acceptance of a KIM send against a real bridge is deferred to ticket 78.

Alternatives considered

  • Direct SMTP from a Worker — rejected: no raw outbound SMTP from the current runtime without a bridge.
  • A per-tenant HTTP KIM gateway — rejected for v1: none of the surveyed providers expose one; adding our own gateway duplicates the bridge work we already need.
  • Vendor email library embedded on the device — rejected on principle: credentials never belong on the device, and signed-sender SMC-B is a server identity.
Auf GitHub bearbeitenDie Dokumentation ist auf Englisch.