Files
HealthLog/tests/fixtures/v137/sharing-matrix.ts
T
Marc-André Bombeck f950dffad4 fix(sharing): keep life events to their owner
Life events rode the profile sharing domain, so a profile share, a
legacy whole-record share and a MANAGE grant all read them, and MANAGE
could write them. No share level was ever worded for them, so in this
release they are the owner's only.

The life-event routes take a bare requireAuth, which refuses under an
acting-account switch. The day section and the timeline's life lane map
to no domain, so a delegate sees the section named not_shared and never
gets the lane, its readiness row or the day index marks. The profile
consent copy no longer names life events, the sharing guard and the
mutation inventory drop the four entries, and the OpenAPI descriptions
say owner-only.
2026-10-08 22:37:57 +02:00

790 lines
16 KiB
TypeScript

export const SHARING_DOMAINS = [
"measurements",
"medications",
"labs",
"profile",
"illness",
"mind",
"cycle",
"documents",
] as const;
export type SharingDomain = (typeof SHARING_DOMAINS)[number];
export type SharingScope = SharingDomain | "record";
export type DelegatedLevel = "write" | "manage";
export type MutatingAction = "POST" | "PUT" | "PATCH" | "DELETE";
export type RecordKind = "shared" | "managed";
export type MutationEffect =
"create" | "update" | "delete" | "restore" | "upsert" | "export";
export interface MutatingHandlerContract {
readonly handlerModule: `app/api/${string}/route.ts`;
readonly route: `/api/${string}`;
readonly action: MutatingAction;
readonly recordKind: RecordKind;
readonly scope: SharingScope;
readonly level: DelegatedLevel;
readonly ownRecord: {
readonly actor: "record-owner";
readonly expected: "allow";
};
readonly deniedLevel: {
readonly actor: "delegate";
readonly level: "read" | "write";
readonly expected: "deny";
};
readonly admittedLevel: {
readonly actor: "delegate";
readonly expected: "allow";
};
readonly actorAttribution: {
readonly field: "actorUserId";
readonly expected: "acting-delegate";
};
readonly resultingEffect: {
readonly expected: "applied-once";
readonly mutation: MutationEffect;
};
readonly sideEffect: {
readonly recordTarget: "resolved-record";
readonly delegatedProviderEgress: "deny";
};
readonly encryptedFieldCarveOut: "never-assert-decrypted-field-content";
}
function handler(
handlerModule: `app/api/${string}/route.ts`,
route: `/api/${string}`,
action: MutatingAction,
scope: SharingScope,
level: DelegatedLevel,
mutation: MutationEffect,
): MutatingHandlerContract {
return {
handlerModule,
route,
action,
recordKind: "shared",
scope,
level,
ownRecord: { actor: "record-owner", expected: "allow" },
deniedLevel: {
actor: "delegate",
level: level === "write" ? "read" : "write",
expected: "deny",
},
admittedLevel: { actor: "delegate", expected: "allow" },
actorAttribution: {
field: "actorUserId",
expected: "acting-delegate",
},
resultingEffect: { expected: "applied-once", mutation },
sideEffect: {
recordTarget: "resolved-record",
delegatedProviderEgress: "deny",
},
encryptedFieldCarveOut: "never-assert-decrypted-field-content",
};
}
export const ADMITTED_MUTATING_HANDLERS = [
handler(
"app/api/measurements/route.ts",
"/api/measurements",
"POST",
"measurements",
"write",
"create",
),
handler(
"app/api/labs/route.ts",
"/api/labs",
"POST",
"labs",
"write",
"create",
),
handler(
"app/api/biomarkers/route.ts",
"/api/biomarkers",
"POST",
"labs",
"write",
"create",
),
handler(
"app/api/illness/episodes/route.ts",
"/api/illness/episodes",
"POST",
"illness",
"write",
"create",
),
handler(
"app/api/encounters/route.ts",
"/api/encounters",
"POST",
"profile",
"write",
"create",
),
handler(
"app/api/practitioners/route.ts",
"/api/practitioners",
"POST",
"profile",
"write",
"create",
),
handler(
"app/api/medications/[id]/side-effects/route.ts",
"/api/medications/[id]/side-effects",
"POST",
"medications",
"write",
"create",
),
handler(
"app/api/medications/route.ts",
"/api/medications",
"POST",
"medications",
"write",
"create",
),
handler(
"app/api/medications/intake/route.ts",
"/api/medications/intake",
"POST",
"medications",
"write",
"create",
),
handler(
"app/api/medications/[id]/intake/route.ts",
"/api/medications/[id]/intake",
"POST",
"medications",
"write",
"create",
),
handler(
"app/api/measurements/[id]/route.ts",
"/api/measurements/[id]",
"PUT",
"measurements",
"manage",
"update",
),
handler(
"app/api/measurements/[id]/route.ts",
"/api/measurements/[id]",
"DELETE",
"measurements",
"manage",
"delete",
),
handler(
"app/api/measurements/bulk-delete/route.ts",
"/api/measurements/bulk-delete",
"POST",
"measurements",
"manage",
"delete",
),
handler(
"app/api/measurements/restore/route.ts",
"/api/measurements/restore",
"POST",
"measurements",
"manage",
"restore",
),
handler(
"app/api/measurement-reminders/route.ts",
"/api/measurement-reminders",
"POST",
"measurements",
"manage",
"create",
),
handler(
"app/api/measurement-reminders/[id]/route.ts",
"/api/measurement-reminders/[id]",
"PATCH",
"measurements",
"manage",
"update",
),
handler(
"app/api/measurement-reminders/[id]/route.ts",
"/api/measurement-reminders/[id]",
"DELETE",
"measurements",
"manage",
"delete",
),
handler(
"app/api/measurement-reminders/[id]/complete/route.ts",
"/api/measurement-reminders/[id]/complete",
"POST",
"measurements",
"manage",
"update",
),
handler(
"app/api/measurement-reminders/[id]/satisfy/route.ts",
"/api/measurement-reminders/[id]/satisfy",
"POST",
"measurements",
"manage",
"update",
),
handler(
"app/api/measurement-reminders/[id]/skip/route.ts",
"/api/measurement-reminders/[id]/skip",
"POST",
"measurements",
"manage",
"update",
),
handler(
"app/api/measurement-reminders/[id]/snooze/route.ts",
"/api/measurement-reminders/[id]/snooze",
"POST",
"measurements",
"manage",
"update",
),
handler(
"app/api/labs/[id]/route.ts",
"/api/labs/[id]",
"PUT",
"labs",
"manage",
"update",
),
handler(
"app/api/labs/[id]/route.ts",
"/api/labs/[id]",
"DELETE",
"labs",
"manage",
"delete",
),
handler(
"app/api/labs/restore/route.ts",
"/api/labs/restore",
"POST",
"labs",
"manage",
"restore",
),
handler(
"app/api/biomarkers/[id]/route.ts",
"/api/biomarkers/[id]",
"PUT",
"labs",
"manage",
"update",
),
handler(
"app/api/allergies/route.ts",
"/api/allergies",
"POST",
"profile",
"manage",
"create",
),
handler(
"app/api/allergies/[id]/route.ts",
"/api/allergies/[id]",
"PATCH",
"profile",
"manage",
"update",
),
handler(
"app/api/allergies/[id]/route.ts",
"/api/allergies/[id]",
"DELETE",
"profile",
"manage",
"delete",
),
handler(
"app/api/family-history/route.ts",
"/api/family-history",
"POST",
"profile",
"manage",
"create",
),
handler(
"app/api/family-history/[id]/route.ts",
"/api/family-history/[id]",
"PATCH",
"profile",
"manage",
"update",
),
handler(
"app/api/family-history/[id]/route.ts",
"/api/family-history/[id]",
"DELETE",
"profile",
"manage",
"delete",
),
handler(
"app/api/illness/episodes/[id]/route.ts",
"/api/illness/episodes/[id]",
"PATCH",
"illness",
"manage",
"update",
),
handler(
"app/api/illness/episodes/[id]/route.ts",
"/api/illness/episodes/[id]",
"DELETE",
"illness",
"manage",
"delete",
),
handler(
"app/api/illness/episodes/[id]/resolve/route.ts",
"/api/illness/episodes/[id]/resolve",
"PATCH",
"illness",
"manage",
"update",
),
handler(
"app/api/illness/episodes/[id]/restore/route.ts",
"/api/illness/episodes/[id]/restore",
"POST",
"illness",
"manage",
"restore",
),
handler(
"app/api/encounters/[id]/route.ts",
"/api/encounters/[id]",
"PATCH",
"profile",
"manage",
"update",
),
handler(
"app/api/encounters/[id]/route.ts",
"/api/encounters/[id]",
"DELETE",
"profile",
"manage",
"delete",
),
handler(
"app/api/encounters/[id]/restore/route.ts",
"/api/encounters/[id]/restore",
"POST",
"profile",
"manage",
"restore",
),
handler(
"app/api/practitioners/[id]/route.ts",
"/api/practitioners/[id]",
"PATCH",
"profile",
"manage",
"update",
),
handler(
"app/api/practitioners/[id]/route.ts",
"/api/practitioners/[id]",
"DELETE",
"profile",
"manage",
"delete",
),
handler(
"app/api/practitioners/[id]/restore/route.ts",
"/api/practitioners/[id]/restore",
"POST",
"profile",
"manage",
"restore",
),
// v1.38.0 — the immunization log.
handler(
"app/api/vaccinations/route.ts",
"/api/vaccinations",
"POST",
"profile",
"write",
"create",
),
handler(
"app/api/vaccinations/[id]/route.ts",
"/api/vaccinations/[id]",
"PATCH",
"profile",
"manage",
"update",
),
handler(
"app/api/vaccinations/[id]/route.ts",
"/api/vaccinations/[id]",
"DELETE",
"profile",
"manage",
"delete",
),
handler(
"app/api/vaccinations/[id]/restore/route.ts",
"/api/vaccinations/[id]/restore",
"POST",
"profile",
"manage",
"restore",
),
handler(
"app/api/vaccinations/[id]/booster/route.ts",
"/api/vaccinations/[id]/booster",
"POST",
"profile",
"write",
"upsert",
),
// v1.42 (#1005) — the record's own vaccine definitions.
handler(
"app/api/vaccinations/custom/route.ts",
"/api/vaccinations/custom",
"POST",
"profile",
"write",
"create",
),
handler(
"app/api/vaccinations/custom/[id]/route.ts",
"/api/vaccinations/custom/[id]",
"PATCH",
"profile",
"manage",
"update",
),
handler(
"app/api/vaccinations/custom/[id]/route.ts",
"/api/vaccinations/custom/[id]",
"DELETE",
"profile",
"manage",
"delete",
),
handler(
"app/api/illness/episodes/[id]/day-logs/route.ts",
"/api/illness/episodes/[id]/day-logs",
"POST",
"illness",
"manage",
"upsert",
),
handler(
"app/api/medications/[id]/route.ts",
"/api/medications/[id]",
"PUT",
"medications",
"manage",
"update",
),
handler(
"app/api/medications/[id]/schedule-revisions/route.ts",
"/api/medications/[id]/schedule-revisions",
"POST",
"medications",
"manage",
"create",
),
handler(
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts",
"/api/medications/[id]/schedule-revisions/[revisionId]",
"PATCH",
"medications",
"manage",
"update",
),
handler(
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts",
"/api/medications/[id]/schedule-revisions/[revisionId]",
"DELETE",
"medications",
"manage",
"delete",
),
handler(
"app/api/medications/[id]/inventory/route.ts",
"/api/medications/[id]/inventory",
"POST",
"medications",
"manage",
"create",
),
handler(
"app/api/medications/[id]/inventory/[itemId]/route.ts",
"/api/medications/[id]/inventory/[itemId]",
"PATCH",
"medications",
"manage",
"update",
),
handler(
"app/api/medications/[id]/inventory/[itemId]/route.ts",
"/api/medications/[id]/inventory/[itemId]",
"DELETE",
"medications",
"manage",
"delete",
),
handler(
"app/api/medications/[id]/side-effects/[logId]/route.ts",
"/api/medications/[id]/side-effects/[logId]",
"DELETE",
"medications",
"manage",
"delete",
),
handler(
"app/api/medications/[id]/glp1/route.ts",
"/api/medications/[id]/glp1",
"POST",
"medications",
"manage",
"create",
),
handler(
"app/api/medications/[id]/intake/[eventId]/route.ts",
"/api/medications/[id]/intake/[eventId]",
"PUT",
"medications",
"manage",
"update",
),
handler(
"app/api/medications/[id]/intake/[eventId]/route.ts",
"/api/medications/[id]/intake/[eventId]",
"DELETE",
"medications",
"manage",
"delete",
),
handler(
"app/api/medications/[id]/intake/bulk-delete/route.ts",
"/api/medications/[id]/intake/bulk-delete",
"POST",
"medications",
"manage",
"delete",
),
handler(
"app/api/medications/[id]/intake/purge/route.ts",
"/api/medications/[id]/intake/purge",
"DELETE",
"medications",
"manage",
"delete",
),
handler(
"app/api/medications/[id]/intake/import/route.ts",
"/api/medications/[id]/intake/import",
"POST",
"medications",
"manage",
"create",
),
handler(
"app/api/medications/intake/dose-history-import/route.ts",
"/api/medications/intake/dose-history-import",
"POST",
"medications",
"manage",
"create",
),
handler(
"app/api/mood-entries/route.ts",
"/api/mood-entries",
"POST",
"mind",
"manage",
"create",
),
handler(
"app/api/mood-entries/[id]/route.ts",
"/api/mood-entries/[id]",
"PUT",
"mind",
"manage",
"update",
),
handler(
"app/api/mood-entries/[id]/route.ts",
"/api/mood-entries/[id]",
"DELETE",
"mind",
"manage",
"delete",
),
handler(
"app/api/mood-entries/bulk-delete/route.ts",
"/api/mood-entries/bulk-delete",
"POST",
"mind",
"manage",
"delete",
),
handler(
"app/api/mood-entries/restore/route.ts",
"/api/mood-entries/restore",
"POST",
"mind",
"manage",
"restore",
),
handler(
"app/api/mental-health/assessments/route.ts",
"/api/mental-health/assessments",
"POST",
"mind",
"manage",
"create",
),
handler(
"app/api/cycle/cycles/[id]/route.ts",
"/api/cycle/cycles/[id]",
"DELETE",
"cycle",
"manage",
"delete",
),
handler(
"app/api/cycle/day-logs/route.ts",
"/api/cycle/day-logs",
"POST",
"cycle",
"manage",
"upsert",
),
handler(
"app/api/cycle/day-logs/[id]/route.ts",
"/api/cycle/day-logs/[id]",
"PATCH",
"cycle",
"manage",
"update",
),
handler(
"app/api/cycle/day-logs/[id]/route.ts",
"/api/cycle/day-logs/[id]",
"DELETE",
"cycle",
"manage",
"delete",
),
handler(
"app/api/cycle/period/route.ts",
"/api/cycle/period",
"POST",
"cycle",
"manage",
"update",
),
handler(
"app/api/cycle/symptoms/custom/route.ts",
"/api/cycle/symptoms/custom",
"POST",
"cycle",
"manage",
"create",
),
handler(
"app/api/medications/categories/route.ts",
"/api/medications/categories",
"POST",
"medications",
"manage",
"create",
),
handler(
"app/api/nutrients/water/route.ts",
"/api/nutrients/water",
"POST",
"measurements",
"manage",
"upsert",
),
handler(
"app/api/insights/patterns/[id]/route.ts",
"/api/insights/patterns/[id]",
"PATCH",
"record",
"manage",
"update",
),
handler(
"app/api/export/health-record/route.ts",
"/api/export/health-record",
"POST",
"record",
"manage",
"export",
),
handler(
"app/api/symptoms/definitions/route.ts",
"/api/symptoms/definitions",
"POST",
"illness",
"manage",
"create",
),
handler(
"app/api/symptoms/definitions/[id]/route.ts",
"/api/symptoms/definitions/[id]",
"PATCH",
"illness",
"manage",
"update",
),
handler(
"app/api/symptoms/definitions/[id]/route.ts",
"/api/symptoms/definitions/[id]",
"DELETE",
"illness",
"manage",
"delete",
),
handler(
"app/api/symptoms/events/route.ts",
"/api/symptoms/events",
"POST",
"illness",
"write",
"create",
),
handler(
"app/api/symptoms/events/[id]/route.ts",
"/api/symptoms/events/[id]",
"PATCH",
"illness",
"manage",
"update",
),
handler(
"app/api/symptoms/events/[id]/route.ts",
"/api/symptoms/events/[id]",
"DELETE",
"illness",
"manage",
"delete",
),
] as const satisfies readonly MutatingHandlerContract[];