mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
docs(sharing): correct the four statements the self-hosting guide gets wrong
Measured against the route declarations on this tree: The health-background section names allergies, family history and the profile facts, and omits visits, the practices behind them and the vaccination record — all three of which `scope.ts` puts in that section, so a grant is narrower on the page than it is in the product. "Aggregate surfaces appear only on a whole-record grant" is true and incomplete: a whole-record grant does not open them equally either. The daily digest and the dashboard's snapshot answer at read, the health score, the dashboard's summary and the insights need manage. The write level lists seven creates against twelve WRITE declarations. A visit, a practice, a vaccination dose and the booster reminder it suggests were missing, and the guide said nothing about the creates that need manage inside a section a write grant can otherwise add to, or about the vault taking no delegated write at any level. "Exports, the health-record archive" sat under what a delegate cannot reach, while `POST /api/export/health-record` answers at manage on a whole-record grant and its own comment says why. The row is about the full exports and the encrypted archive; the doctor report is named as the exception it is. The sharing-denial guard's docblock carried the same class of stale claim — 116 fenced modules, 26 never published — and that gap is closed on this tree. The caveat becomes an assertion: a new leg recomputes the fenced modules from the route sources and requires every one to be published, and it names no number, because a hard-coded figure is what turned the paragraph into a false statement about the contract. It fails when a published path is withheld from it.
This commit is contained in:
1 parent
0e4619a0c0
commit
70e40e097d
2 files changed
+107
-27
No files matched your search
@@ -49,16 +49,16 @@ Either the entire record, which is what sharing has always meant and what
|
||||
every grant written before v1.37.0 carries, or a set of named sections
|
||||
chosen when the invitation is written:
|
||||
|
||||
| Section | What it covers |
|
||||
| ----------------- | -------------------------------------------------------------------------- |
|
||||
| Readings | Weight, blood pressure, pulse, glucose, sleep, workouts and custom metrics |
|
||||
| Medications | The medication list, its schedules, doses taken, side effects and stock |
|
||||
| Lab results | Lab results and the analytes behind them |
|
||||
| Health background | Allergies, family history and the facts in the health profile |
|
||||
| Illness | Illness episodes and the day logs kept under them |
|
||||
| Mood and mind | Mood entries and the mental-health questionnaire history |
|
||||
| Cycle | Cycle tracking |
|
||||
| Documents | Everything in the document vault |
|
||||
| Section | What it covers |
|
||||
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Readings | Weight, blood pressure, pulse, glucose, sleep, workouts and custom metrics |
|
||||
| Medications | The medication list, its schedules, doses taken, side effects and stock |
|
||||
| Lab results | Lab results and the analytes behind them |
|
||||
| Health background | Allergies, family history, the facts in the health profile, visits and the practices behind them, and the vaccination record |
|
||||
| Illness | Illness episodes and the day logs kept under them |
|
||||
| Mood and mind | Mood entries and the mental-health questionnaire history |
|
||||
| Cycle | Cycle tracking |
|
||||
| Documents | Everything in the document vault |
|
||||
|
||||
A section that was not shared is refused exactly the way a record nobody
|
||||
shared is, so the shape of what was held back is not readable from
|
||||
@@ -68,8 +68,12 @@ shared. The invitation screen says this in as many words, because a note
|
||||
or a document name inside one section can be about any other.
|
||||
|
||||
Aggregate surfaces (the dashboard overview, the health score, the daily
|
||||
digest) appear only on a whole-record grant. A figure derived from part of
|
||||
a record reads as a figure about the person, and it would not be one.
|
||||
digest, every insight) read across sections, so a grant naming sections
|
||||
never opens one. A figure derived from part of a record reads as a figure
|
||||
about the person, and it would not be one. Whole-record grants do not all
|
||||
open them equally either: the daily digest and the dashboard's snapshot
|
||||
answer at read, while the health score, the dashboard's summary and the
|
||||
insights need manage.
|
||||
|
||||
A stored scope never grows. A section added by a later release is not in
|
||||
a set somebody ticked before it existed.
|
||||
@@ -80,11 +84,17 @@ Three levels, fixed when the invitation is written. Raising one means
|
||||
revoking and inviting again, because widening a grant in place would carry
|
||||
a consent the other person never gave.
|
||||
|
||||
| Level | Adds |
|
||||
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Read | Read what the grant opens, and change nothing. |
|
||||
| Write | Add entries: a reading, a lab result, an analyte, an illness entry, a side effect, a medication, and marking a dose taken or skipped. Editing, deleting and restoring stay with you. |
|
||||
| Manage | Change and remove entries, including ones the owner wrote; record the health background; read the insights generated from the record. Always the entire record. |
|
||||
| Level | Adds |
|
||||
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Read | Read what the grant opens, and change nothing. |
|
||||
| Write | Add entries: a reading, a lab result, an analyte, an illness entry, a side effect, a medication, marking a dose taken or skipped, a visit, a practice, a vaccination dose and the booster reminder it suggests. Editing, deleting and restoring stay with you. |
|
||||
| Manage | Change and remove entries, including ones the owner wrote; record the health background; read the insights generated from the record. Always the entire record. |
|
||||
|
||||
Some entries are creates only **manage** reaches, and the split runs by
|
||||
entry rather than by section: an allergy, a family-history entry, a mood
|
||||
entry, a screener, a cycle day, a water entry and a preventive-care
|
||||
reminder are all created at manage, in sections a write grant can
|
||||
otherwise add to. Nothing in the document vault is writable at any level.
|
||||
|
||||
Offering **manage** asks the owner for a fresh second factor if they have
|
||||
one enrolled, which makes it a browser-only act either way: a Bearer
|
||||
@@ -106,12 +116,19 @@ whole feature is built on, and it is drawn on the server:
|
||||
| API tokens, the MCP connector | A durable credential that outlives the access |
|
||||
| Connected services (Withings, Fitbit, WHOOP, Google Health, Polar, Oura, Strava, Nightscout) | Re-pointing a sync, or feeding somebody else's wearable into this record |
|
||||
| Notification channels and push devices | The owner's health alerts redirected to another phone or chat |
|
||||
| Exports, the health-record archive, clinician share links | A one-click copy of everything, or a new door that outlives the access |
|
||||
| Full exports, the encrypted archive, clinician share links | A one-click copy of everything, or a new door that outlives the access |
|
||||
| Asking an AI provider anything | AI processing of the owner's data, under the owner's own consent |
|
||||
| Modules, thresholds, units, the record's language and timezone | Configuration of the account, not content of the record |
|
||||
| Sharing itself | Inviting somebody else, widening the access, or passing it on |
|
||||
| Locale, theme, dashboard layout | Presentation belongs to the person, not to the record |
|
||||
|
||||
One export is the exception, and it is deliberate: the doctor report
|
||||
(`POST /api/export/health-record`) answers at manage on a whole-record
|
||||
grant. It renders a prepared artefact from a selection the caller declares
|
||||
rather than handing the record over whole, which is the difference the row
|
||||
above is drawing. The full exports and the passphrase-encrypted archive
|
||||
stay with the owner.
|
||||
|
||||
This table is about a grant between two accounts. A managed profile is
|
||||
the one place where some of it is deliberately different, because a record
|
||||
with no login has nobody else to keep its own configuration: see "Records
|
||||
|
||||
@@ -45,16 +45,16 @@
|
||||
* - Comments are stripped before matching. `.../guardians/route.ts` names both
|
||||
* fence helpers in a docblock explaining why it uses neither, and counting
|
||||
* prose would classify an actor surface as delegable.
|
||||
* - It covers the PUBLISHED surface, which is not the whole delegable surface.
|
||||
* 116 route modules resolve the fence; 90 of them are in the OpenAPI document
|
||||
* and 26 have never been (mood entries, the record-settings tree, the
|
||||
* medication intake tree, several insight reads). That gap predates this
|
||||
* release and is not closed here — "frozen" means the published contract is
|
||||
* complete and consistent, not that every delegable route is described. What
|
||||
* this guard does guarantee is that one of those 26 cannot be published
|
||||
* without its refusal, because publishing it puts it in scope on the next run.
|
||||
* - It covered the PUBLISHED surface and not the whole delegable one. That was
|
||||
* the honest limit at v1.37.0, when 116 route modules resolved the fence and
|
||||
* 26 of them were in no OpenAPI document at all. The gap is closed on this
|
||||
* tree, so the limit is now an assertion rather than a caveat: the last leg
|
||||
* below recomputes the fenced modules from the route sources and requires
|
||||
* every one of them to be published. Two counts is one too many, so it names
|
||||
* no number — a hard-coded figure is what turned the paragraph it replaces
|
||||
* into a false claim about the contract.
|
||||
*/
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -84,6 +84,42 @@ function stripComments(text: string): string {
|
||||
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Every `route.ts` under `src/app/api` whose handlers resolve the record fence.
|
||||
*
|
||||
* Comments stripped first, for the reason the operation matcher strips them:
|
||||
* one module names both fence helpers in a docblock explaining why it uses
|
||||
* neither.
|
||||
*/
|
||||
function fencedRouteModules(): string[] {
|
||||
const out: string[] = [];
|
||||
const walk = (dir: string) => {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
continue;
|
||||
}
|
||||
if (entry.name !== "route.ts") continue;
|
||||
const source = stripComments(readFileSync(full, "utf8"));
|
||||
if (/require(RecordAuth|GuardianAuth)\s*\(/.test(source)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(join(ROOT, "src/app/api"));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `…/src/app/api/labs/[id]/route.ts` → `/api/labs/{id}`. */
|
||||
function apiPathFor(moduleFile: string): string {
|
||||
return moduleFile
|
||||
.slice(join(ROOT, "src/app").length)
|
||||
.replaceAll("\\", "/")
|
||||
.replace(/\/route\.ts$/, "")
|
||||
.replace(/\[([^\]]+)\]/g, "{$1}");
|
||||
}
|
||||
|
||||
function routeModulePath(apiPath: string): string {
|
||||
return join(
|
||||
ROOT,
|
||||
@@ -334,6 +370,33 @@ describe("the sharing refusal every delegable path publishes", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("publishes every module that resolves the fence", () => {
|
||||
// The reverse direction of the leg above, and the one the docblock used to
|
||||
// apologise for: that one holds every PUBLISHED fenced operation to the
|
||||
// refusal, and says nothing about a fenced module nobody published. A
|
||||
// module missing from the document is a delegable surface a generated
|
||||
// client cannot see and a reader of the contract cannot find, which is
|
||||
// exactly how the mood-entry tree and the record-settings tree stayed
|
||||
// invisible for a release.
|
||||
const modules = fencedRouteModules();
|
||||
expect(
|
||||
modules.length,
|
||||
"no fenced route module was discovered — the matcher found nothing",
|
||||
).toBeGreaterThan(100);
|
||||
|
||||
const published = new Set(Object.keys(openApiPaths));
|
||||
const missing = modules
|
||||
.map((rel) => apiPathFor(rel))
|
||||
.filter((apiPath) => !published.has(apiPath));
|
||||
|
||||
expect(
|
||||
missing,
|
||||
`these route modules resolve the record fence and are in no published path:\n${missing
|
||||
.map((apiPath) => ` ❌ ${apiPath}`)
|
||||
.join("\n")}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("carries the refusal into the generated document", () => {
|
||||
const spec = readFileSync(join(ROOT, "docs/api/openapi.yaml"), "utf8");
|
||||
expect(spec).toContain(SHARING_ACCESS_DENIED_DESCRIPTION.slice(0, 60));
|
||||
|
||||
Reference in new issue
Block a user