fix(compliance): port DSR/consent/audit/retention audit fixes
Ports the upstream compliance-core audit fixes onto the kept core-dsr, core-consent, core-audit, core-cms and core-shared packages: - core-dsr: scope DSR operations to the caller's own subject (A11); include the subject's audit trail in exports; resolve the per-request binding from ctx instead of a throwing singleton proxy. - core-consent: build the consent router from the shared superjson transformer (A10); merge per-category on persist instead of replacing; validate migrated categories against an allow-list. - core-audit: keyed 128-bit pseudonyms + salted DSR certificate; add the audit-logs collection and the req-scoped GDPR audit-erasure afterDelete hook (A6). - core-shared: grace-purge soft-deleted rows via a retention-purge task + tombstone field and boot registration (A2/A3); add the require-authenticated tRPC helper; derive clientIp + resolve the session user in createTrpcContext (B2/A11). - core-cms: register audit-logs, wire the audit-erasure hook and retention-purge tasks; adapted to the clean-slate collection set (users only — no workspaces feature on this branch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
89
packages/core-shared/src/trpc/context.test.ts
Normal file
89
packages/core-shared/src/trpc/context.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { clientIpFromHeaders, createTrpcContext } from "@/trpc/context";
|
||||
|
||||
describe("clientIpFromHeaders", () => {
|
||||
it("takes the first x-forwarded-for hop", () => {
|
||||
const headers = new Headers({
|
||||
"x-forwarded-for": "203.0.113.7, 10.0.0.1, 10.0.0.2",
|
||||
});
|
||||
expect(clientIpFromHeaders(headers)).toBe("203.0.113.7");
|
||||
});
|
||||
|
||||
it("trims whitespace around the first hop", () => {
|
||||
const headers = new Headers({
|
||||
"x-forwarded-for": " 203.0.113.7 , 10.0.0.1",
|
||||
});
|
||||
expect(clientIpFromHeaders(headers)).toBe("203.0.113.7");
|
||||
});
|
||||
|
||||
it("falls back to x-real-ip when x-forwarded-for is absent", () => {
|
||||
const headers = new Headers({ "x-real-ip": "198.51.100.4" });
|
||||
expect(clientIpFromHeaders(headers)).toBe("198.51.100.4");
|
||||
});
|
||||
|
||||
it("returns undefined when neither header is present", () => {
|
||||
expect(clientIpFromHeaders(new Headers())).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for empty header values", () => {
|
||||
const headers = new Headers({ "x-forwarded-for": " ", "x-real-ip": "" });
|
||||
expect(clientIpFromHeaders(headers)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTrpcContext", () => {
|
||||
it("attaches the derived clientIp from the request", async () => {
|
||||
const req = new Request("https://example.test/api/trpc", {
|
||||
headers: { "x-forwarded-for": "203.0.113.7" },
|
||||
});
|
||||
await expect(createTrpcContext(req)).resolves.toEqual({
|
||||
clientIp: "203.0.113.7",
|
||||
});
|
||||
});
|
||||
|
||||
it("yields an undefined clientIp without a request", async () => {
|
||||
await expect(createTrpcContext()).resolves.toEqual({
|
||||
clientIp: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches the resolved user and mirrors userId (A11)", async () => {
|
||||
const req = new Request("https://example.test/api/trpc");
|
||||
const ctx = await createTrpcContext(req, {
|
||||
resolveUser: async () => ({ id: "user-1", roles: ["admin"] }),
|
||||
});
|
||||
expect(ctx.user).toEqual({ id: "user-1", roles: ["admin"] });
|
||||
expect(ctx.userId).toBe("user-1");
|
||||
});
|
||||
|
||||
it("treats a null resolver result as anonymous", async () => {
|
||||
const req = new Request("https://example.test/api/trpc");
|
||||
const ctx = await createTrpcContext(req, {
|
||||
resolveUser: async () => null,
|
||||
});
|
||||
expect(ctx.user).toBeUndefined();
|
||||
expect(ctx.userId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("treats a throwing resolver as anonymous instead of failing", async () => {
|
||||
const req = new Request("https://example.test/api/trpc");
|
||||
const ctx = await createTrpcContext(req, {
|
||||
resolveUser: async () => {
|
||||
throw new Error("expired session");
|
||||
},
|
||||
});
|
||||
expect(ctx.user).toBeUndefined();
|
||||
expect(ctx.clientIp).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not invoke the resolver without a request", async () => {
|
||||
let called = false;
|
||||
await createTrpcContext(undefined, {
|
||||
resolveUser: async () => {
|
||||
called = true;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user