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:
@@ -21,6 +21,17 @@ import {
|
||||
NoopRateLimit,
|
||||
type RateLimitBudget,
|
||||
} from "@repo/core-shared/rate-limit";
|
||||
import { bindAudit, type IAuditLog } from "@repo/core-audit";
|
||||
import {
|
||||
bindProductionConsent,
|
||||
bindDevSeedConsent,
|
||||
type ConsentFactory,
|
||||
} from "@repo/core-consent";
|
||||
import {
|
||||
bindProductionDsr,
|
||||
bindDevSeedDsr,
|
||||
type DsrBinding,
|
||||
} from "@repo/core-dsr";
|
||||
import { authManifest } from "@repo/auth";
|
||||
import { bindProductionBlog } from "@repo/blog/di/bind-production";
|
||||
import { bindProductionAuth } from "@repo/auth/di/bind-production";
|
||||
@@ -44,6 +55,45 @@ let resolvedTracer: ITracer | null = null;
|
||||
let resolvedLogger: ILogger | null = null;
|
||||
let resolvedQueue: IJobQueue | null = null;
|
||||
|
||||
/**
|
||||
* Compliance bindings constructed once per boot (audit finding A11): the
|
||||
* consent factory and DSR binding that the tRPC createContext threads into
|
||||
* every request so the mounted consent/dsr routers are live. `auditLog` is
|
||||
* present only on the production path.
|
||||
*/
|
||||
export type ComplianceBindings = {
|
||||
consentFactory: ConsentFactory;
|
||||
dsrBinding: DsrBinding;
|
||||
auditLog?: IAuditLog;
|
||||
};
|
||||
|
||||
let complianceBindings: ComplianceBindings | null = null;
|
||||
|
||||
export type BindingMode = "production" | "dev-seed";
|
||||
|
||||
/** Env → binding mode, mirroring bindAll()'s resolution rules. */
|
||||
export function resolveBindingMode(): BindingMode {
|
||||
if (process.env.USE_DEV_SEED === "false") return "production";
|
||||
if (process.env.USE_DEV_SEED === "true") return "dev-seed";
|
||||
if (process.env.NODE_ENV === "production") return "production";
|
||||
return "dev-seed";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the boot-time compliance bindings, running bindAll() first if
|
||||
* needed. Called by the app's tRPC createContext on every request (cheap
|
||||
* after the first call — bindAll is memoized).
|
||||
*/
|
||||
export async function getComplianceBindings(): Promise<ComplianceBindings> {
|
||||
await bindAll();
|
||||
if (!complianceBindings) {
|
||||
throw new Error(
|
||||
"compliance bindings missing after bindAll() — binder did not construct them",
|
||||
);
|
||||
}
|
||||
return complianceBindings;
|
||||
}
|
||||
|
||||
/** Rule 0: pick instrumentation backend from DSN env (orthogonal to repo mode). */
|
||||
function resolveInstrumentation(): { tracer: ITracer; logger: ILogger } {
|
||||
if (resolvedTracer && resolvedLogger) {
|
||||
@@ -108,11 +158,28 @@ export async function bindAllProduction(): Promise<void> {
|
||||
const { queue } = await resolveJobsProduction();
|
||||
const resolvedConfig = await config;
|
||||
|
||||
// Compliance cores (A6/A11): the audit log fans into the Payload
|
||||
// `audit-logs` collection + stdout; consent + DSR bindings share it so
|
||||
// every grant/withdraw/export/delete leaves an audit trail.
|
||||
const { auditLog } = bindAudit(sharedContainer, {
|
||||
payloadConfig: resolvedConfig,
|
||||
});
|
||||
const { consentFactory } = bindProductionConsent({
|
||||
config: resolvedConfig,
|
||||
auditLog,
|
||||
});
|
||||
const dsrBinding = bindProductionDsr({ config: resolvedConfig, auditLog });
|
||||
complianceBindings = { consentFactory, dsrBinding, auditLog };
|
||||
|
||||
const ctx: BindProductionContext = {
|
||||
config: resolvedConfig,
|
||||
tracer,
|
||||
logger,
|
||||
queue,
|
||||
auditLog,
|
||||
// Enables the anonymous→authenticated consent migration inside the auth
|
||||
// sign-up use case (audit finding A12).
|
||||
consentFactory,
|
||||
// Real limiter in production (audit finding A4/B3): budgets come from the
|
||||
// feature manifests, so manifest edits change enforcement without touching
|
||||
// this file. In-memory ⇒ per-process counters; multi-instance deployments
|
||||
@@ -136,10 +203,17 @@ export async function bindAllDevSeed(): Promise<void> {
|
||||
const { tracer, logger } = resolveInstrumentation(); // Rule 0
|
||||
const { queue } = resolveJobsDevSeed();
|
||||
|
||||
// In-memory compliance bindings so the mounted consent/dsr routers work
|
||||
// without Payload booted (A11). No audit sink in dev seed.
|
||||
const { consentFactory } = bindDevSeedConsent();
|
||||
const dsrBinding = bindDevSeedDsr();
|
||||
complianceBindings = { consentFactory, dsrBinding };
|
||||
|
||||
const ctx: BindContext = {
|
||||
tracer,
|
||||
logger,
|
||||
queue,
|
||||
consentFactory,
|
||||
// Dev seed intentionally keeps the no-op limiter so local iteration and
|
||||
// seeded demos are never throttled; production binds InMemoryRateLimit.
|
||||
rateLimit: new NoopRateLimit(),
|
||||
@@ -172,15 +246,10 @@ export async function bindAllDevSeed(): Promise<void> {
|
||||
export function bindAll(): Promise<void> {
|
||||
if (bindPromise) return bindPromise;
|
||||
|
||||
if (process.env.USE_DEV_SEED === "false") {
|
||||
bindPromise = bindAllProduction();
|
||||
} else if (process.env.USE_DEV_SEED === "true") {
|
||||
bindPromise = bindAllDevSeed();
|
||||
} else if (process.env.NODE_ENV === "production") {
|
||||
bindPromise = bindAllProduction();
|
||||
} else {
|
||||
bindPromise = bindAllDevSeed();
|
||||
}
|
||||
bindPromise =
|
||||
resolveBindingMode() === "production"
|
||||
? bindAllProduction()
|
||||
: bindAllDevSeed();
|
||||
|
||||
return bindPromise;
|
||||
}
|
||||
@@ -191,6 +260,7 @@ export function __resetBindStateForTests(): void {
|
||||
resolvedTracer = null;
|
||||
resolvedLogger = null;
|
||||
resolvedQueue = null;
|
||||
complianceBindings = null;
|
||||
}
|
||||
|
||||
/** Test-only accessor for resolved instrumentation. */
|
||||
|
||||
Reference in New Issue
Block a user