feat(generators): wire audit entry + e2e byte-identical reconstruction test
This commit is contained in:
50
turbo/generators/__tests__/core-package-audit.e2e.test.ts
Normal file
50
turbo/generators/__tests__/core-package-audit.e2e.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { mkdtempSync, cpSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { computeSnapshot } from "../lib/snapshot.js";
|
||||||
|
import expectedSnapshot from "../__snapshots__/core-package/audit.snapshot.json" with { type: "json" };
|
||||||
|
|
||||||
|
// Repo root is 2 levels up from turbo/generators/__tests__
|
||||||
|
const REPO_ROOT = resolve(fileURLToPath(import.meta.url), "..", "..", "..", "..");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip "@repo/core-audit" from a package.json file in the tmp tree.
|
||||||
|
* Required when simulating a fresh scaffold: core-audit does not exist yet
|
||||||
|
* but the snapshot was captured before removal. Apps in the current tree
|
||||||
|
* may still list it as a dependency.
|
||||||
|
*/
|
||||||
|
function stripCoreAuditDep(pkgJsonPath: string): void {
|
||||||
|
const raw = readFileSync(pkgJsonPath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as Record<string, Record<string, string>>;
|
||||||
|
for (const section of ["dependencies", "devDependencies", "peerDependencies"] as const) {
|
||||||
|
if (parsed[section]?.["@repo/core-audit"]) {
|
||||||
|
delete parsed[section]["@repo/core-audit"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeFileSync(pkgJsonPath, JSON.stringify(parsed, null, 2) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("e2e: core-package audit", () => {
|
||||||
|
it("byte-identical reconstruction matches snapshot", { timeout: 120_000 }, () => {
|
||||||
|
const tmp = mkdtempSync(join(tmpdir(), "e2e-audit-"));
|
||||||
|
cpSync(REPO_ROOT, tmp, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (src) =>
|
||||||
|
!src.includes("node_modules") &&
|
||||||
|
!src.includes(".turbo") &&
|
||||||
|
!src.includes("packages/core-audit"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Strip @repo/core-audit from apps/web-next/package.json so pnpm install
|
||||||
|
// succeeds without the package being present (simulating the post-removal state).
|
||||||
|
stripCoreAuditDep(join(tmp, "apps", "web-next", "package.json"));
|
||||||
|
|
||||||
|
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { stdio: "ignore" });
|
||||||
|
execSync(`cd ${tmp} && pnpm turbo gen core-package --args audit`, { stdio: "ignore" });
|
||||||
|
const result = computeSnapshot(join(tmp, "packages/core-audit"));
|
||||||
|
expect(result).toEqual(expectedSnapshot);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -666,16 +666,28 @@ import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";
|
|||||||
},
|
},
|
||||||
printUiNextSteps,
|
printUiNextSteps,
|
||||||
],
|
],
|
||||||
|
audit: () => [
|
||||||
|
() => {
|
||||||
|
assertOptionalPackageNotPresent("core-audit");
|
||||||
|
return "Guard passed — packages/core-audit does not exist yet.";
|
||||||
|
},
|
||||||
|
...emitTemplateTree("core-package/audit", "packages/core-audit"),
|
||||||
|
() => {
|
||||||
|
addToTranspilePackages("apps/web-next/next.config.mjs", "@repo/core-audit");
|
||||||
|
return "Added @repo/core-audit to transpilePackages.";
|
||||||
|
},
|
||||||
|
printAuditNextSteps,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
plop.setGenerator("core-package", {
|
plop.setGenerator("core-package", {
|
||||||
description: "Scaffold an optional core package (realtime, events, trpc, ui)",
|
description: "Scaffold an optional core package (realtime, events, trpc, ui, audit)",
|
||||||
prompts: [
|
prompts: [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
name: "name",
|
name: "name",
|
||||||
message: "Which optional core package?",
|
message: "Which optional core package?",
|
||||||
choices: ["realtime", "events", "trpc", "ui"],
|
choices: ["realtime", "events", "trpc", "ui", "audit"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: (answers) => {
|
actions: (answers) => {
|
||||||
@@ -1324,6 +1336,54 @@ function printRealtimeNextSteps(): string {
|
|||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printAuditNextSteps(): string {
|
||||||
|
return [
|
||||||
|
"─────────────────────────────────────────────────────────────",
|
||||||
|
"@repo/core-audit scaffolded into packages/core-audit/.",
|
||||||
|
"",
|
||||||
|
"Manual wiring required (compliance-critical):",
|
||||||
|
"",
|
||||||
|
"1. Set AUDIT_PSEUDONYM_SALT env var (production REQUIRED):",
|
||||||
|
' export AUDIT_PSEUDONYM_SALT="$(openssl rand -hex 32)"',
|
||||||
|
" Add to your deployment secrets manager.",
|
||||||
|
"",
|
||||||
|
"2. Mount the audit-logs Payload collection in packages/core-cms/src/payload.config.ts:",
|
||||||
|
' import { auditLogsCollection } from "@repo/core-audit/collection";',
|
||||||
|
" // collections: [..., auditLogsCollection],",
|
||||||
|
"",
|
||||||
|
"3. Mount the admin tRPC router in packages/core-api/src/root.ts:",
|
||||||
|
' import { createAuditRouter } from "@repo/core-audit/api";',
|
||||||
|
" // const { auditLog } = bindAudit(container, { payloadConfig, sinks: [\"payload\", \"stdout\"] });",
|
||||||
|
" // routers: { ..., audit: createAuditRouter(auditLog) },",
|
||||||
|
"",
|
||||||
|
"4. Bind audit in apps/web-next/src/server/bind-production.ts:",
|
||||||
|
' const { bindAudit } = await import("@repo/core-audit/di");',
|
||||||
|
" const { auditLog } = bindAudit(sharedContainer, {",
|
||||||
|
" payloadConfig: resolvedConfig,",
|
||||||
|
' sinks: ["payload", "stdout"],',
|
||||||
|
" });",
|
||||||
|
"",
|
||||||
|
"5. Install user-collection hooks (recommended for DPA compliance):",
|
||||||
|
" In packages/auth/src/di/bind-production.ts, gate on ctx.auditLog:",
|
||||||
|
" if (ctx.auditLog) {",
|
||||||
|
' const { createAuditErasureHook, createAuditAfterReadHook } =',
|
||||||
|
' await import("@repo/core-audit/hooks");',
|
||||||
|
" // wire onto users collection — see docs/guides/audit-and-compliance.md",
|
||||||
|
" }",
|
||||||
|
"",
|
||||||
|
"6. Set up a log shipper (Vector / Fluent Bit) to forward stdout JSON to",
|
||||||
|
" your aggregator. See docs/guides/audit-and-compliance.md for configs.",
|
||||||
|
"",
|
||||||
|
"7. Verify:",
|
||||||
|
" pnpm install",
|
||||||
|
" pnpm lint && pnpm typecheck && pnpm test",
|
||||||
|
" pnpm turbo boundaries",
|
||||||
|
"",
|
||||||
|
"See docs/guides/audit-and-compliance.md for the full guide.",
|
||||||
|
"─────────────────────────────────────────────────────────────",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function coreUiComponentActions(a: {
|
function coreUiComponentActions(a: {
|
||||||
tier: "atom" | "molecule" | "organism";
|
tier: "atom" | "molecule" | "organism";
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user