Merge branch 'fix/v1439' into release/v1.43.9

This commit is contained in:
Marc-André Bombeck committed 2026-10-20 22:49:33 +02:00
commit b4618f2ec3
55 files changed
+1547 -270

No files matched your search

@@ -0,0 +1,137 @@
/**
* The caller lookups load a projection of `User`, never the whole row.
*
* Every authenticated request resolves its caller through the session lookup
* (`lib/auth/session.ts`), the Bearer resolver (`lib/auth/bearer.ts`) or, for a
* switched record, the owner load in `lib/api-handler.ts`. They used to
* `include: { user: true }` / `findUnique({ where })`, which copied the profile
* picture (megabytes), the cached briefing text and every stored credential
* into memory on each request. They now name `SESSION_USER_SELECT`.
*
* Two halves:
* 1. each of those lookups carries the projection, and no file anywhere loads
* a related user row whole with `include: { user: true }`;
* 2. the projection itself leaves out every `Bytes` column of the model and
* every secret-shaped column, derived from `schema.prisma` so a new
* column of either kind is covered without editing this file.
*/
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { walkSourceFiles } from "./helpers/source-files";
import { SESSION_USER_SELECT } from "@/lib/auth/session-user";
const SRC = join(process.cwd(), "src");
function read(rel: string): string {
return readFileSync(join(SRC, rel), "utf8");
}
function stripComments(src: string): string {
return src
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/(^|[^:])\/\/.*$/gm, "$1");
}
/** The `{ ... }` argument of every call matching `callee`, braces balanced. */
function callObjects(src: string, callee: RegExp): string[] {
const code = stripComments(src);
const out: string[] = [];
const re = new RegExp(callee.source + /\s*\(\s*\{/.source, "g");
for (let m = re.exec(code); m; m = re.exec(code)) {
const open = m.index + m[0].length - 1;
let depth = 0;
let i = open;
for (; i < code.length; i++) {
if (code[i] === "{") depth++;
else if (code[i] === "}" && --depth === 0) break;
}
out.push(code.slice(open, i + 1));
}
return out;
}
const USER_LOOKUP = /prisma\.user\.find(?:Unique|First)(?:OrThrow)?/;
const SESSION_LOOKUP = /prisma\.session\.find(?:Unique|First)(?:OrThrow)?/;
describe("caller lookups load the session projection", () => {
it("the session lookups include the user through SESSION_USER_SELECT", () => {
const calls = callObjects(
read("lib/auth/session.ts"),
SESSION_LOOKUP,
).filter((body) => /\binclude\s*:/.test(body));
expect(calls.length).toBeGreaterThanOrEqual(2);
for (const body of calls) {
expect(body).toMatch(
/include\s*:\s*\{\s*user\s*:\s*\{\s*select\s*:\s*SESSION_USER_SELECT\s*\}\s*\}/,
);
}
});
it("the Bearer resolver and the switched-owner load select SESSION_USER_SELECT", () => {
for (const rel of ["lib/auth/bearer.ts", "lib/api-handler.ts"]) {
const calls = callObjects(read(rel), USER_LOOKUP);
expect(calls.length, rel).toBeGreaterThan(0);
for (const body of calls) {
expect(body, rel).toMatch(/\bselect\s*:/);
}
expect(
calls.some((body) => /select\s*:\s*SESSION_USER_SELECT\b/.test(body)),
rel,
).toBe(true);
}
});
it("no file loads a related user row whole", () => {
const files = walkSourceFiles(SRC, { floor: 3000 })
.filter((p) => !p.startsWith("generated/"))
.filter((p) => !p.includes("__tests__"))
.filter((p) => !/\.test\.tsx?$/.test(p));
expect(files.length).toBeGreaterThan(0);
const offenders = files.filter((rel) =>
/\buser\s*:\s*true\b/.test(
stripComments(read(rel)).replace(/select\s*:\s*\{[^{}]*\}/g, ""),
),
);
expect(offenders).toEqual([]);
});
});
describe("SESSION_USER_SELECT leaves out bytes and secrets", () => {
const schema = readFileSync(
join(process.cwd(), "prisma", "schema.prisma"),
"utf8",
);
const model = schema.match(/^model User \{([\s\S]*?)^\}/m)?.[1] ?? "";
const columns = [
...model.matchAll(/^\s+(\w+)\s+([A-Z]\w*)(\??)(?:\s|$)/gm),
].map(([, name, type]) => ({ name, type }));
const selected = Object.keys(SESSION_USER_SELECT);
it("reads the User model", () => {
expect(columns.length).toBeGreaterThan(50);
expect(selected.length).toBeGreaterThan(0);
});
it("names only columns the model has", () => {
const names = new Set(columns.map((c) => c.name));
expect(selected.filter((key) => !names.has(key))).toEqual([]);
});
it("selects no Bytes column", () => {
const bytes = columns.filter((c) => c.type === "Bytes").map((c) => c.name);
expect(bytes).toContain("avatarBytes");
expect(selected.filter((key) => bytes.includes(key))).toEqual([]);
});
it("selects no credential, secret or large cached text", () => {
const secretShaped =
/Encrypted$|^passwordHash$|^totpSecret|^totpLastStep$|^telegramBotToken$|^insightsCachedText$|^environmentAqHistoryJson$/;
const secrets = columns
.map((c) => c.name)
.filter((name) => secretShaped.test(name));
expect(secrets.length).toBeGreaterThan(10);
expect(selected.filter((key) => secrets.includes(key))).toEqual([]);
});
});