feat(web-next): resolve the session user + live compliance context

The tRPC createContext was () => ({}) — the mounted dsr/consent routers
401'd every call and the dsr singleton stub threw (audit finding A11).
createTrpcContext now accepts an app resolveUser hook; web-next resolves
the session cookie through the auth feature's validateSession (denylist
included) plus a role snapshot, and threads bindProductionDsr/Consent
(or dev-seed) bindings into every request. The dsr router resolves its
binding from ctx.dsrBinding per request instead of a throwing proxy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 17:55:05 +02:00
parent 8b78563881
commit 49241845b5
12 changed files with 453 additions and 48 deletions

View File

@@ -295,15 +295,40 @@ describe("dsrRouter subject scoping (A1 — IDOR)", () => {
});
});
describe("dsrRouter singleton guard", () => {
it("throws when procedures are called without a real DsrBinding", async () => {
// The singleton uses a Proxy that throws on any binding property access.
// Procedures access binding lazily, so the Proxy error surfaces at call time.
describe("dsrRouter singleton (context-time binding, A11)", () => {
it("fails loudly when neither ctx.dsrBinding nor a creation binding exists", async () => {
const caller = dsrRouter.createCaller({
user: authenticatedUser,
} as Record<string, unknown>);
await expect(
caller.export({ subjectId: "alice", format: "json" }),
).rejects.toThrow(/dsrRouter singleton/);
).rejects.toMatchObject({
code: "INTERNAL_SERVER_ERROR",
message: expect.stringContaining("DsrBinding missing"),
});
});
it("serves requests when the app provides ctx.dsrBinding", async () => {
const binding = makeBinding();
const caller = dsrRouter.createCaller({
user: authenticatedUser,
dsrBinding: binding,
} as Record<string, unknown>);
const result = await caller.export({ subjectId: "alice", format: "json" });
expect(result.subjectId).toBe("alice");
expect(binding.dataExport.calls).toHaveLength(1);
});
it("prefers ctx.dsrBinding over the creation-time binding", async () => {
const creationBinding = makeBinding();
const ctxBinding = makeBinding();
const router = createDsrRouter(creationBinding as unknown as DsrBinding);
const caller = router.createCaller({
user: authenticatedUser,
dsrBinding: ctxBinding,
} as Record<string, unknown>);
await caller.export({ subjectId: "alice", format: "json" });
expect(ctxBinding.dataExport.calls).toHaveLength(1);
expect(creationBinding.dataExport.calls).toHaveLength(0);
});
});