mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
feat: lay down the shared v1.42 contract
Land every file the v1.42 work shares before any of it is built, so the parts built side by side never edit the same file. Schema and migrations 0379-0386: the HEALTH_CONNECT measurement source, ImportJob.kind, the air-quality columns and the per-user switch on the environment tables, sealed environment locations with nullable plaintext, managed-profile handover links, the per-type HealthKit arrival ledger, lab unit-conversion provenance, and user-defined vaccines. Every new column is annotated for the column-reader sweep. HEALTH_CONNECT reaches every exhaustive list: the source labels, the read enum (not the write allowlist), the source-priority ladder cap, now the number of sources rather than a fixed eight, and the write-path smoke test. RMSSD gets its own priority class and a 300 ms ceiling. The batch syncTrigger gains `manual`, with its label on the Apple Health card. Queues for the Health Connect import, the compaction-tombstone purge and the measurement-table maintenance are registered with stub handlers; host-metric sampling moves to every five minutes. Environment and work_mem variables reach the compose whitelist, the production example and the manifest. The proxy admits the handover link and redirects it at the edge, and the Health Connect upload bypasses the proxy body limit. The new routes answer 501 behind their final authentication and are published in the contract; backup, wipe and key rotation classify the new models and columns; the seven message bundles carry the base copy. Guards that list release-pending keys and columns fail once an entry is wired, so those lists empty themselves before the tag. Also re-anchor a login-throttle fixture to the wall clock, where its expiry is compared, and re-base the aggregate bundle cap on a measured 12 KB of new locale strings.
This commit is contained in:
1 parent
125b15bfb7
commit
e63be67b5a
82 files changed
+5099
-727
No files matched your search
@@ -98,6 +98,7 @@ const LEGIT_IDENTICAL = new Set<string>([
|
||||
"insights.derived.composite.READINESS.component.hrv",
|
||||
"labs.catalog.hs-crp",
|
||||
"measurements.typeHrvRmssd",
|
||||
"settings.sections.sources.metrics.hrvRmssd",
|
||||
"measurements.typeVo2Max",
|
||||
// "{hours} h {minutes} min": the SI symbols for hour and minute.
|
||||
"measurements.durationHoursMinutes",
|
||||
@@ -112,6 +113,7 @@ const LEGIT_IDENTICAL = new Set<string>([
|
||||
"admin.webPushVapidTitle",
|
||||
"measurements.sourceAppleHealth",
|
||||
"measurements.sourceGoogleHealth",
|
||||
"measurements.sourceHealthConnect",
|
||||
"medications.sourceAppleHealth",
|
||||
"settings.ai.providerChain.types.anthropic",
|
||||
"settings.ai.providerChain.types.codex",
|
||||
@@ -119,6 +121,7 @@ const LEGIT_IDENTICAL = new Set<string>([
|
||||
"settings.sections.export.import.appleHealth.title",
|
||||
"settings.sections.sources.sourceLabels.APPLE_HEALTH",
|
||||
"settings.sections.sources.sourceLabels.GOOGLE_HEALTH",
|
||||
"settings.sections.sources.sourceLabels.HEALTH_CONNECT",
|
||||
"settings.appleHealth.title",
|
||||
"mood.tag.fastFood",
|
||||
// Technical / config labels conventionally kept verbatim.
|
||||
|
||||
@@ -66,6 +66,42 @@ const DYNAMIC_ALLOWLIST_PREFIXES = [
|
||||
"apiErrors",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Keys a release lands ahead of their call sites, so that the packages built
|
||||
* in parallel on the release branch share one bundle and never edit it
|
||||
* themselves. Each entry names the package that wires the call sites and the
|
||||
* issue it serves.
|
||||
*
|
||||
* This list expires by construction: the companion test fails as soon as an
|
||||
* entry no longer covers a single orphan, so a prefix whose call sites have
|
||||
* all landed must be dropped in the same change, and the list is empty again
|
||||
* before the release is tagged. It is NOT a place for keys nobody will wire.
|
||||
*/
|
||||
const RELEASE_PENDING_PREFIXES: Record<string, string> = {
|
||||
"auth.claim": "v1.42 managed-profile claim page (#959)",
|
||||
"recordSharing.managed.handover":
|
||||
"v1.42 managed-profile handover dialog (#959)",
|
||||
"settings.sections.export.import.healthConnect":
|
||||
"v1.42 Health Connect import card (#972)",
|
||||
"settings.sections.environment.airQuality":
|
||||
"v1.42 environment settings, air quality (#615)",
|
||||
"environment.fields.apparentMax": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.pm10": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.no2": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.europeanAqi": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.uvIndexMax": "v1.42 air-quality field label (#615)",
|
||||
"environment.fields.dustMax": "v1.42 air-quality field label (#615)",
|
||||
"insights.correlation.dailySameDay":
|
||||
"v1.42 same-day environment correlation templates (#615)",
|
||||
"settings.appleHealth.freshness":
|
||||
"v1.42 per-type HealthKit freshness list (#1173)",
|
||||
"insights.workouts.manual.editTitle": "v1.42 manual workout edit (#1162)",
|
||||
"insights.workouts.manual.editAction": "v1.42 manual workout edit (#1162)",
|
||||
"insights.workouts.manual.updated": "v1.42 manual workout edit (#1162)",
|
||||
"vaccinations.custom": "v1.42 user-defined vaccines (#1005)",
|
||||
"labs.convertedFrom": "v1.42 lab unit conversion note (#1095)",
|
||||
};
|
||||
|
||||
/**
|
||||
* Interpolating strings that LOOK like key templates and are not. Each entry
|
||||
* whitelists nothing — the template still contributes no reachable keys — it
|
||||
@@ -118,9 +154,14 @@ function nodePaths(obj: unknown, prefix: string, out: Set<string>): void {
|
||||
}
|
||||
}
|
||||
|
||||
function underPrefix(leaf: string, prefix: string): boolean {
|
||||
return leaf === prefix || leaf.startsWith(`${prefix}.`);
|
||||
}
|
||||
|
||||
function isAllowlisted(leaf: string): boolean {
|
||||
return DYNAMIC_ALLOWLIST_PREFIXES.some(
|
||||
(p) => leaf === p || leaf.startsWith(`${p}.`),
|
||||
return (
|
||||
DYNAMIC_ALLOWLIST_PREFIXES.some((p) => underPrefix(leaf, p)) ||
|
||||
Object.keys(RELEASE_PENDING_PREFIXES).some((p) => underPrefix(leaf, p))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,4 +226,18 @@ describe("i18n reverse coverage", () => {
|
||||
);
|
||||
}
|
||||
}, 15_000);
|
||||
|
||||
it("every release-pending prefix still covers an unwired key", () => {
|
||||
const { leaves, ref } = scan();
|
||||
const stale = Object.keys(RELEASE_PENDING_PREFIXES).filter(
|
||||
(prefix) =>
|
||||
!leaves.some(
|
||||
(leaf) => underPrefix(leaf, prefix) && !isReferenced(leaf, ref),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
stale,
|
||||
"These RELEASE_PENDING_PREFIXES entries have all their call sites now — drop them",
|
||||
).toEqual([]);
|
||||
}, 15_000);
|
||||
});
|
||||
@@ -79,6 +79,10 @@ const UNAUTHENTICATED: Readonly<Record<string, string>> = {
|
||||
"Says whether single sign-on is configured, so the sign-in screen can offer it.",
|
||||
"/api/notifications/vapid":
|
||||
"The instance's public Web Push key. Public by definition.",
|
||||
"/api/auth/claim/preview":
|
||||
"Previews a managed-profile handover link for a person who has no credential yet; the one-time token in the body is the credential.",
|
||||
"/api/auth/claim":
|
||||
"Turns a managed profile into the claimant's own account and creates their first credential; the one-time token in the body authorises it.",
|
||||
// The tombstones, sourced from the registry rather than restated one by one.
|
||||
// A retired path answers 410 from the proxy before any handler runs, and that
|
||||
// answer never reads the request's cookies or Authorization header — so it is
|
||||
|
||||
@@ -51,18 +51,21 @@ describe("User column backup classification", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// The person's own data, sealed at rest for privacy rather than a secret
|
||||
// that grants anything: the insurance number, and since v1.42 the home
|
||||
// location the environment module resolves weather against.
|
||||
const SEALED_SETTINGS = ["insuranceNumberEncrypted", "homeLocationEncrypted"];
|
||||
|
||||
it("carries no credential-shaped column as a setting", () => {
|
||||
// The insurance number is the one sealed setting: it is the person's own
|
||||
// data, sealed at rest for privacy, not a secret that grants anything.
|
||||
const sealedSettings = ACCOUNT_SETTING_COLUMNS.filter((column) =>
|
||||
/Encrypted$|Token|Secret|password|totp|oidc/i.test(column),
|
||||
);
|
||||
expect(sealedSettings).toEqual(["insuranceNumberEncrypted"]);
|
||||
expect(sealedSettings.sort()).toEqual([...SEALED_SETTINGS].sort());
|
||||
});
|
||||
|
||||
it("never classifies a sealed column other than the insurance number as anything but a credential", () => {
|
||||
it("never classifies a sealed column other than the sealed settings as anything but a credential", () => {
|
||||
const sealed = columns.filter(
|
||||
(c) => /Encrypted$/.test(c) && c !== "insuranceNumberEncrypted",
|
||||
(c) => /Encrypted$/.test(c) && !SEALED_SETTINGS.includes(c),
|
||||
);
|
||||
expect(sealed.length).toBeGreaterThan(20);
|
||||
expect(sealed.filter((c) => classified[c] !== "CREDENTIAL")).toEqual([]);
|
||||
|
||||
Reference in new issue
Block a user