feat(workspaces): connectWorkspace use case with audit

Manifest-first: connectWorkspace declared mutates:true with the
workspace-connected audit event, requiredCores gains audit. Workspace
entity gains gitUrl + persisted status enum (created/connecting/ready/
error). Input takes name + git URL + PAT; the output schema is the
credential-free workspace entity, so the PAT can never round-trip.
Audit emission asserted with RecordingAuditLog; binders wire the use
case through wireUseCase with the __audited brand, and web-next
bindAll now binds core-audit (payload+stdout sinks in production,
stdout in dev-seed) so boot conformance passes in both modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 21:54:00 +02:00
parent 6216897680
commit 8219c1fabb
27 changed files with 783 additions and 26 deletions

View File

@@ -16,6 +16,7 @@
"dependencies": {
"@repo/auth": "workspace:*",
"@repo/core-api": "workspace:*",
"@repo/core-audit": "workspace:*",
"@repo/core-cms": "workspace:*",
"@repo/core-shared": "workspace:*",
"@repo/core-trpc": "workspace:^",

View File

@@ -8,6 +8,18 @@ vi.mock("@repo/auth/di/bind-production", () => ({
bindProductionAuth: vi.fn(),
}));
vi.mock("@repo/auth/di/bind-dev-seed", () => ({ bindDevSeedAuth: vi.fn() }));
vi.mock("@repo/workspaces/di/bind-production", () => ({
bindProductionWorkspaces: vi.fn(),
}));
vi.mock("@repo/workspaces/di/bind-dev-seed", () => ({
bindDevSeedWorkspaces: vi.fn(),
}));
// bindAudit enforces AUDIT_PSEUDONYM_SALT under NODE_ENV=production — mocked
// here so dispatcher-routing tests stay focused on routing. The real audit
// binding is exercised by bind-production.smoke.test.ts (dev-seed path).
vi.mock("@repo/core-audit/di", () => ({
bindAudit: vi.fn(() => ({ auditLog: { record: vi.fn() } })),
}));
vi.mock("@repo/core-shared/instrumentation", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@repo/core-shared/instrumentation")>();

View File

@@ -17,6 +17,7 @@ import {
type IJobQueue,
} from "@repo/core-shared/jobs";
import { NoopRateLimit } from "@repo/core-shared/rate-limit";
import { bindAudit } from "@repo/core-audit/di";
import { bindProductionAuth } from "@repo/auth/di/bind-production";
import { bindDevSeedAuth } from "@repo/auth/di/bind-dev-seed";
import { bindProductionWorkspaces } from "@repo/workspaces/di/bind-production";
@@ -85,11 +86,19 @@ export async function bindAllProduction(): Promise<void> {
const { queue } = await resolveJobsProduction();
const resolvedConfig = await config;
// Audit trail (core-audit): Payload hot store + stdout JSON for the log
// shipper. Entries are trace-id enriched from the active OTel span.
const { auditLog } = bindAudit(sharedContainer, {
payloadConfig: resolvedConfig,
sinks: ["payload", "stdout"],
});
const ctx: BindProductionContext = {
config: resolvedConfig,
tracer,
logger,
queue,
auditLog,
rateLimit: new NoopRateLimit(),
};
@@ -106,10 +115,16 @@ export async function bindAllDevSeed(): Promise<void> {
const { tracer, logger } = resolveInstrumentation(); // Rule 0
const { queue } = resolveJobsDevSeed();
// Dev-seed audit trail: stdout JSON only (no Payload booted). Keeps the
// __audited brand path identical to production so the boot conformance
// assertion exercises the same wiring.
const { auditLog } = bindAudit(sharedContainer, { sinks: ["stdout"] });
const ctx: BindContext = {
tracer,
logger,
queue,
auditLog,
rateLimit: new NoopRateLimit(),
};