Files
HealthLog/src/lib/sharing/scope.ts
T
Marc-André Bombeck b97988279e feat(vaccinations): wire the query keys, the contract and the consent copy
- a query-key module and a dependent bundle that also evicts the
  preventive-care root, because logging a dose settles a booster and the
  checkups list goes stale in the same tick
- the OpenAPI module publishes the resolved series with the contract
  written into the description: render text from these numbers, never
  recompute them
- the health-background consent sentence now names the immunization
  history in all six languages, so the widening is stated where it is
  agreed to rather than inferred from a code comment
- no server-side cache helper: nothing cached reads a dose, and a sweep
  over caches nobody reads would look like coverage without being it
2026-08-08 14:43:59 +02:00

111 lines
4.9 KiB
TypeScript

/**
* v1.37.0 — the vocabulary a scoped grant is written in.
*
* A grant may open the whole record, or it may open some of it. This file
* holds the closed set of names the second case is allowed to use, and it
* holds nothing else: no predicate, no storage, no route knowledge. The
* predicates live in `grants.ts` beside the rest of what a grant means, and
* the per-route classification lives on the call sites, frozen by
* `src/__tests__/sharing-surface-guard.test.ts`.
*
* Two properties are the whole reason the file exists separately.
*
* * **It is a sharing vocabulary, not the module vocabulary.** There is no
* conversion to `ModuleKey`, no import from `src/lib/modules/`, and there
* never is one. The two masks answer different questions — "did the owner
* turn this part of the product on" and "did the owner open this part of
* their record to that person" — and a route carrying both runs both. The
* day they share a spelling is the day somebody makes one of them imply
* the other, and the implication would run in the dangerous direction: a
* module the owner enabled is not a section the owner shared.
* * **The names are the ones a consent screen can say out loud.** They were
* derived by clustering the delegable read surfaces the product already
* has, not by inventing categories, so each one is a thing a person
* recognises as one thing. Finer splits subjects nobody separates (lab
* results from the analyte list); coarser re-merges the exact split
* somebody asked for.
*
* A stored scope never grows by release. A grant written when this list held
* eight names opens those of the eight it named and nothing added later: the
* set is stored as the keys the owner ticked, and a key that did not exist
* when they ticked cannot be in it. That is why a new domain is a consent
* question rather than a schema question.
*/
/**
* The sections of a record a grant can be narrowed to.
*
* Order is the consent screen's reading order, not an ordering of weight —
* nothing here ranks above anything else, and no code may treat the index as
* meaning anything.
*/
export const SHARE_DOMAINS = [
/** Readings: weight, blood pressure, pulse, glucose, sleep, workouts, nutrients, tracked values. */
"measurements",
/** The medication list, its schedules, doses, side effects and inventory. */
"medications",
/** Lab results and the biomarker catalogue behind them. */
"labs",
/**
* Health background: allergies, family history, anamnesis facts, visits and
* practitioners, and the immunization history.
*
* Visits joined this domain rather than getting one of their own, and the
* cost is worth naming: somebody granted `profile` so a relative could see
* an allergy list also gains the record of every appointment and the address
* book behind it. The mitigation is not this comment — it is the consent
* copy, which names visits in all six locales, so the widening is stated
* where it is agreed to rather than inferred from the code.
*
* The immunization log joined on the same argument and carries the same
* cost, stated the same way: a delegate holding this domain for the allergy
* list also sees every dose the person has had, and the consent copy names
* it. Vaccinations are a tracked clinical vertical with their own module
* toggle, which is a separate question from consent — a module the owner
* enabled is not a section the owner shared, and a person granting health
* background is granting the section whether or not the surfaces are on.
*/
"profile",
/** Illness episodes and the day logs under them. */
"illness",
/** Mood entries and mental-health screener history. */
"mind",
/** Cycle tracking. */
"cycle",
/** The document vault. */
"documents",
] as const;
/** One section of a record. */
export type ShareDomain = (typeof SHARE_DOMAINS)[number];
/**
* The distinguished value for "the entire record", which is what every grant
* written before this release was consented as.
*
* It is deliberately NOT a member of {@link SHARE_DOMAINS}: a scoped grant
* naming it would be a scope that means "no scope", and the one thing a
* narrowing must never be able to express is its own absence. A route
* declaring `record` is a route that reads across sections, and a scoped
* grant never reaches one.
*/
export const ENTIRE_RECORD = "record";
/**
* What a delegable route declares it touches: one section, or the record as a
* whole.
*/
export type ShareScope = ShareDomain | typeof ENTIRE_RECORD;
const DOMAIN_SET: ReadonlySet<string> = new Set<string>(SHARE_DOMAINS);
/**
* Is this a section name?
*
* The one place an untrusted string becomes a {@link ShareDomain}. `record`
* answers false here on purpose — see {@link ENTIRE_RECORD}.
*/
export function isShareDomain(value: unknown): value is ShareDomain {
return typeof value === "string" && DOMAIN_SET.has(value);
}