Merge branch 'fix/encrypt-free-text-and-local-ai' into release/v1.39.3

# Conflicts:
#	src/app/api/import/csv/route.ts
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 21:50:05 +02:00
commit 66b1aef40c
69 files changed
+3625 -367

No files matched your search

@@ -23,10 +23,11 @@
*
* ## Why the matchers are what they are
*
* A grep for `process.env.X` alone is not the read set. Four modules resolve a
* name through a helper and index `process.env` dynamically, so their reads
* are invisible to the direct matcher — `ALLOW_LOCAL_AI_PRIVATE_HOSTS` and
* every `OIDC_*` among them. Each is registered below with its own call-shape
* A grep for `process.env.X` alone is not the read set. Three modules resolve
* a name through a helper and index `process.env` dynamically, so their reads
* are invisible to the direct matcher — every `OIDC_*` among them. (The AI
* private-host grants were a fourth until v1.39.3, when they became direct
* `process.env` reads.) Each is registered below with its own call-shape
* matcher AND its own non-zero assertion, so a refactor that renames the
* helper fails this file loudly instead of quietly shrinking the read set.
*
@@ -79,12 +80,6 @@ const INDIRECT_READERS: ReadonlyArray<{
minimum: 1,
why: "the retention windows are parsed through a local intEnv() helper",
},
{
file: "lib/ai/local-host-allowlist.ts",
pattern: /ENV_VAR\s*=\s*["']([A-Z][A-Z0-9_]*)["']/g,
minimum: 1,
why: "the name is hoisted into an ENV_VAR constant, then indexed",
},
{
file: "lib/boot/readiness-summary.ts",
pattern: /\benv\.([A-Z][A-Z0-9_]*)/g,
@@ -0,0 +1,71 @@
/**
* The native OIDC leg ends at `healthlog://oidc-callback?error=<reason>`, and
* the app maps each reason to its own message. The only written contract for
* that vocabulary is the login route's 302 description in the OpenAPI
* registry (the callback itself is provider-driven and has no entry).
*
* An app build once matched against hyphenated shorthand from a coordination
* note (`identity-conflict`) while the server sent `oidc_identity_conflict`,
* so every reason fell through to the generic message. This binds the two
* ends that can be read exactly: every reason either route can send on a
* redirect a native client may receive is named in that description, and
* nothing is named there that no route sends.
*
* Its limit: it reads string literals passed to the redirect helpers, so a
* reason assembled at runtime would slip past it. None is today.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const ROOT = join(__dirname, "..");
const read = (rel: string) => readFileSync(join(ROOT, rel), "utf8");
/** Literal reasons passed to one of the named redirect helpers. */
function reasons(source: string, helpers: string[]): Set<string> {
const found = new Set<string>();
const pattern = new RegExp(
`\\b(?:${helpers.join("|")})\\(\\s*"(oidc_[a-z_]+)"\\s*\\)`,
"g",
);
for (const m of source.matchAll(pattern)) found.add(m[1]);
return found;
}
describe("native OIDC error reasons are documented", () => {
// The login route answers both legs through `loginError`; the callback's
// native-aware helper is `failRedirect`. `errorRedirect` in the callback is
// the browser-only branch taken before the state (and so the native flag)
// could be read, and never reaches the app.
const sent = new Set([
...reasons(read("app/api/auth/oidc/login/route.ts"), ["loginError"]),
...reasons(read("app/api/auth/oidc/callback/route.ts"), ["failRedirect"]),
]);
// The login route's 302 description is one string literal on one line.
const description =
read("lib/openapi/routes/auth.ts")
.split("\n")
.find(
(line) =>
line.includes("healthlog://oidc-callback?error=<reason>") &&
line.includes("oidc_invalid_request"),
) ?? "";
const documented = new Set(
[...description.matchAll(/`(oidc_[a-z_]+)`/g)].map((m) => m[1]),
);
it("finds the reasons on both ends", () => {
expect(sent.size).toBeGreaterThanOrEqual(8);
expect(documented.size).toBeGreaterThanOrEqual(8);
});
it("documents every reason a route sends", () => {
expect([...sent].filter((r) => !documented.has(r))).toEqual([]);
});
it("documents no reason that no route sends", () => {
expect([...documented].filter((r) => !sent.has(r))).toEqual([]);
});
});