Files
HealthLog/tests/fixtures/v137/sharing-matrix.ts
T
Marc-André Bombeck 8fd1d543a6 fix(medications): let a manager end a course and serve End only to who may press it
POST /api/medications/{id}/courses/end resolved the caller with
requireAuth(), so a delegate managing the record got 403 while the
medication PUT that used to carry the end date admits MANAGE, and the
reads still served canEndCourse true. The action now resolves the record
at MANAGE over medications and files the previous end (C4) with the
count of tombstoned placeholders.

The medication reads narrow canEndCourse and canStartCourse to the
caller: End needs MANAGE, starting a course stays owner-only. The list
cell stays per record and the narrowing runs after it. Create and edit
answers pass the grant they were resolved under instead of null.

Registered in the sharing surface guard, the MANAGE handler matrix and
the activity feed's verb lines.

Refs #1280
2026-10-25 00:26:55 +02:00

838 lines
17 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/biomarkers/[id]/route.ts",
"/api/biomarkers/[id]",
"DELETE",
"labs",
"manage",
"delete",
),
handler(
"app/api/biomarkers/[id]/merge/route.ts",
"/api/biomarkers/[id]/merge",
"POST",
"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]/courses/end/route.ts",
"/api/medications/[id]/courses/end",
"POST",
"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/anamnesis/audit-c/route.ts",
"/api/anamnesis/audit-c",
"POST",
"profile",
"manage",
"create",
),
handler(
"app/api/anamnesis/audit-c/[id]/route.ts",
"/api/anamnesis/audit-c/[id]",
"DELETE",
"profile",
"manage",
"delete",
),
handler(
"app/api/medications/[id]/ingredients/route.ts",
"/api/medications/[id]/ingredients",
"PUT",
"medications",
"manage",
"update",
),
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[];