feat(medications,measurements): derive write provenance from transport and surface it

Root-cause the intake source attribution and give the HealthKit batch a
diagnostic sync-trigger, one attribution surface for both.

AuthContext now carries a read-only authMethod ("cookie" | "bearer"),
derived from the same branch that sets the wide event's auth_method. The
medication-intake route maps it to the stored IntakeSource: a Bearer
(native) call records API, a cookie (browser) call records WEB. The other
producer values are untouched — a converge onto a pending reminder row
updates through applyCanonicalSlotWrite, which never rewrites source, so
REMINDER / IMPORT / APPLE_HEALTH provenance is preserved. The value is
never client-asserted, the same posture as userId being narrowed from auth.

The dose-history ledger now selects and returns source per intake row so a
client can label how each dose was recorded. The endpoint gains its first
OpenAPI entry (path plus response schema) so the field lands in the
iOS-consumed contract.

The measurements batch accepts an optional per-request syncTrigger
(foreground | background | push). It is diagnostic-only: recorded on the
measurement.batch.ingest wide event and nothing else — never persisted,
never part of the dedup key, never an input to attribution or how a sample
is stored. Sending it or omitting it produces byte-identical row outcomes.

Regenerated docs/api/openapi.yaml.
This commit is contained in:
Marc-André Bombeck committed 2026-07-23 21:15:17 +02:00
1 parent d321391024
commit 27b548ab13
18 files changed
+426 -9

No files matched your search

@@ -40,6 +40,7 @@ import { requireAdmin, HttpError } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
const ADMIN_CTX = {
authMethod: "cookie" as const,
session: { id: "s1", expiresAt: new Date(Date.now() + 3_600_000) },
user: {
id: "admin-1",
@@ -36,6 +36,7 @@ describe("GET /api/admin/users", () => {
beforeEach(() => {
vi.clearAllMocks();
mockRequireAdmin.mockResolvedValue({
authMethod: "cookie" as const,
session: { id: "s1", expiresAt: new Date(Date.now() + 3_600_000) },
user: {
id: "admin-1",