Register core-shared/security/tanstack server middleware in app.config.ts
as a Nitro/H3 hook that emits the six security headers and forwards the
per-request nonce. Update instrumentation-client to read the nonce from
<meta name="csp-nonce"> and pass it to initSentryClientReact.
Add nonce support to initSentryClientReact (feedbackIntegration receives
styleNonce/scriptNonce), mirroring the initSentryClient pattern already
in place for web-next.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add apps/web-next/middleware.ts calling withSecurityHeaders() from
core-shared/security/next; exports matcher config excluding static assets
- Update layout.tsx to call getNonce() and render <meta name="csp-nonce">
so client-side JS can read the per-request nonce
- Update instrumentation-client.ts to read nonce from csp-nonce meta tag
and pass it to initSentryClient for feedbackIntegration CSP compliance
- Add nonce option to initSentryClient (InitClientOpts.nonce) and thread
styleNonce + scriptNonce into feedbackIntegration when provided
- Add middleware test asserting all six headers, prod/dev CSP shape, and
x-nonce presence; add feedbackIntegration nonce tests to core-shared
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exports withSecurityHeaders() and getNonce() from the
./security/tanstack subpath. withSecurityHeaders() returns all six
security headers plus x-nonce for use inside a TanStack/Nitro H3
server middleware; getNonce() reads x-nonce from the node request
headers forwarded by that middleware.
Mirrors the ./security/next adapter pattern while staying free of
any @tanstack/start dependency — the adapter works with plain H3
IncomingMessage types that TanStack Start exposes at wiring time
(Story 09).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements security/next subpath with withSecurityHeaders() middleware
and getNonce() Server Component helper. Middleware generates a per-request
nonce, calls buildSecurityHeaders, sets all six headers + x-nonce on the
response, and forwards the nonce via request headers for Server Component
access. Adds next as optional peer + dev dependency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds framework-agnostic security headers module to core-shared/security:
- SecurityHeadersConfig + CspMode types
- generateNonce() using crypto.randomBytes(16)
- buildSecurityHeaders() emitting all six headers (HSTS, X-Frame-Options,
X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP) with
prod (strict-dynamic + nonce threading) and dev (unsafe-inline/eval +
ws/localhost) CSP modes; URL validation throwing InvalidSecurityHeadersConfig
on malformed allowedConnect/Img/FontOrigins
- Full unit test suite (24 tests, 100% coverage on runtime files)
- Exported from core-shared barrel and ./security subpath
Blocks story 07 (framework adapters) and stories 08-09 (app wiring).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add withRateLimit(rateLimit, fn) in rate-limit/with-rate-limit.ts,
attaching the RateLimited brand at DI bind time
- Extend wireUseCase to accept optional rateLimit?: IRateLimit and
compose withRateLimit innermost (before analytics/audit); propagate
__rateLimited through analytics + audit inline wrappers
- Extend withSpan and withCapture PROPAGATED_BRANDS to include
__rateLimited so the outermost binding carries the brand
- Extend assertFeatureConformance to require __rateLimited brand when
manifest.useCases[name].rateLimit.length > 0; refactored into
helper functions to stay within complexity thresholds
- Add rateLimit?: IRateLimit to BindContext; default to NoopRateLimit
in web-next bindAllProduction and bindAllDevSeed aggregators
- Unit tests for withRateLimit brand attachment, factory passthrough,
and composition; synthetic fixture tests for conformance errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the `no-undeclared-rate-limit` ESLint rule (warn severity) that
enforces rate-limit drift at lint time:
- Warns when rateLimit.consume("X", _) is called inside a use-case but
"X" is absent from manifest.useCases[name].rateLimit
- Warns when a declared rateLimit budget has no matching consume call
in the use-case body (unusedDeclaration)
- Is a no-op outside use-case files
Extends _manifest-ast.js to extract the rateLimit[] field from both
parseManifestUseCases and parseManifestFully. Updates _manifest-ast
tests to include the new field in expected shapes. Registers the rule
at warn severity in plugin.js and base.js. Adds RuleTester fixtures
for all four cases (declared+matching, undeclared, unused, non-use-case).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NoopRateLimit always allows with Infinity remaining — zero-overhead default
for apps without a wired rate-limit impl. InMemoryRateLimit uses a Map-backed
fixed-window with check-at-read expiry and an injected clock for testability.
Both exported from the core-shared barrel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds IRateLimit, RateLimitBudget, RateLimitDecision to a new
rate-limit sub-module; adds RateLimited<F> brand and isRateLimited
predicate following the Captured/ConsentChecked pattern; extends
UseCaseManifest with rateLimit?: readonly RateLimitBudget[] so
features can declare rate-limit gates in their manifests.
Exports new types from @repo/core-shared/rate-limit and
@repo/core-shared/conformance. Blocks stories 02, 03, and 04.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ConsentFactoryProtocol / ConsentGrantMeta / ConsentProtocol to
core-shared/di/bind-protocols so feature binders can wire per-user
consent without a hard dep on the optional @repo/core-consent package.
BindContext gains an optional consentFactory? field following the same
pattern as bus?, auditLog?, etc.
signUpUseCase gains a 4th optional dep (consentFactory). When present
and the input includes a cookieHeader containing cc_consent=<categories>,
the use case calls consent.grant for each category with
method:"signup-migration" and returns a clearCookie payload (Max-Age:0)
so the anonymous cookie is cleared on the HTTP response.
Tests use RecordingConsent from @repo/core-testing to assert migration
call shape and cookie-clear; no-cookie and no-factory branches are also
covered. All coverage bands hold at 100% for use-cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous attempt was rejected because the axe-core a11y requirement
had no test infrastructure — ARIA roles were correct but unverified by
a scanner. This adds jest-axe (approved via library-decision trace) and
asserts toHaveNoViolations() for both modal and banner variants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Story files are excluded from vitest by design (they run in Storybook
runner, not vitest). Add *.stories.{ts,tsx} to ALLOWED_GLOBS so the
L1 diff gate doesn't flag them as "new untested file".
Also add error-handling test for useOptionalConsent rethrow path
(cookie-consent-banner lines 52-53) achieving 100% statement coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add @repo/core-dsr and @repo/core-consent as dependencies and wire
dsrRouter + consentRouter into appRouter via the existing router
composition pattern. Integration tests cover all eight procedures
(dsr.export, dsr.delete, dsr.rectify, dsr.restrict, consent.grant,
consent.withdraw, consent.isGranted, consent.getCategories) with auth
and response-shape assertions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PayloadDataDelete.deleteSubjectData('soft') was NULLing PII fields and
emitting RESTRICT audit entries, but never setting processingRestrictedAt
on self-kind rows — violating GDPR Art. 17 restriction semantics.
softRedactOwnerRows now accepts optional extraData; processOwnerRows
passes { processingRestrictedAt: new Date().toISOString() } when
kind === 'self' and mode === 'soft'. Owner-kind rows are intentionally
excluded (restriction flag belongs on the subject record, not owned rows).
Added two dedicated tests: one asserting the field is present for self,
one asserting it is absent for owner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol-agnostic handlers (grant, withdraw, isGranted, getCategories)
in core-consent/handlers/ call IConsent methods and return typed results.
consentRouter uses a consent-specific tRPC context (userId + consentFactory)
so each procedure can resolve the per-user IConsent instance at call time.
Auth middleware guards all four procedures and maps UnauthenticatedError →
UNAUTHORIZED via defineErrorMiddleware from core-shared (no local duplicate).
76 tests passing; new handler and router code at 100% branch coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration helpers let the auth signUp flow transfer anonymous cookie consent
to an authenticated user's record. extractAnonymousConsent parses the raw
Cookie header; migrateAnonymousConsent calls IConsent.grant with
method: "signup-migration" for each granted category, making the migration
traceable in the audit log. No-op when the consent cookie is absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add symbols.test.ts to cover CONSENT_SYMBOLS constant.
Add recording-consent.test.ts to core-testing (follows the pattern
of all other recording doubles which each have a sibling test file).
Refactor payload-consent.test.ts to extract a makeConsent() helper,
reducing clone groups from 7 to 2.
Add new test to cover grantedAt=null branch in deserializeEntry (withdraw
before any grant → persisted as null → loaded as undefined).
Extend coverage/diff.mjs allowlist for packages/core-testing/ since that
tooling package does not install @vitest/coverage-v8 and therefore produces
no lcov data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the Payload-backed IConsent that reads/writes users.consentState
and emits CONSENT_GRANT/CONSENT_WITHDRAW audit entries via injected auditLog.
Adds RecordingConsent test double in core-testing for unit-test injection.
Adds bindProductionConsent/bindDevSeedConsent DI binders and InMemoryConsent
for dev/seed contexts. Contract tests cover grant/withdraw/isGranted round-trip,
audit entry shape, metadata persistence (bannerVersion/policyVersion/method),
and getCategories reflection of state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add consent to CORE_PACKAGE_GENERATORS in turbo/generators/config.ts so
pnpm turbo gen core-package consent is a valid command (not hand-rollable)
- Create turbo/generators/templates/core-package/consent/ mirroring the
analytics template shape (AGENTS.md, package.json, tsconfig, turbo, vitest,
eslint, src/index.ts scaffolds)
- Regenerate packages/core-consent/ from the new template (replaces the
previous hand-rolled attempt that violated the generator-first rule)
- Add __consentChecked to withCapture PROPAGATED_BRANDS so the brand bubbles
through the full withSpan→withCapture wrapper chain to the outermost binding
that assertFeatureConformance reads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add packages/core-consent with ConsentCategory, ConsentState,
UserConsentState types and IConsent interface
- Add withConsent wrapper attaching __consentChecked brand at bind time;
unit tests assert brand attachment and factory passthrough
- Add ConsentChecked<F> type to core-shared/conformance/brands.ts and
isConsentChecked helper to brand-runtime.ts
- Extend FeatureManifest with requiresConsent?: readonly string[] field
- Extend assertFeatureConformance to require __consentChecked brand when
requiresConsent.length > 0; synthetic fixture tests cover pass/fail cases
- Propagate __consentChecked in withSpan PROPAGATED_BRANDS so the outermost
binding carries the brand
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds CONSENT_GRANT, CONSENT_WITHDRAW, RESTRICT, UNRESTRICT to the
AuditAction closed enum per GDPR Art. 7 and Art. 18 requirements.
core-consent and core-dsr optional cores (Epic B Stories 03/06) emit
these action types via core-audit's IAuditLog channel; the values must
exist in core-shared's enum before either optional core can be built.
No change to IAuditLog's interface surface — new values flow through
AuditEntry.action automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces SubjectLinkKind, SubjectLink, and CollectionSubject types to
packages/core-shared/src/payload/subject-linkage-types.ts, establishes the
ambient CollectionCustom.subject declaration (parallel to custom.pii / custom.retention
from Epic A), and extends PAYLOAD_AUTH_PII_DEFAULTS with processingRestrictedAt and
consentState as DSR-managed excluded fields. Applies the first canonical usage of
custom.subject = { kind: "self", field: "id" } on the auth users collection.
Adds docs/compliance/ as the canonical onboarding reference for the
compliance module, covering every field in each generated YAML artifact
with inline annotations and explaining the docs/compliance/ (examples)
vs compliance/ (live artifacts) split.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds scripts/compliance/emit-data-map.mjs which walks Payload collection
configs (packages/*/integrations/cms/collections/*.ts), applies
PAYLOAD_AUTH_PII_DEFAULTS + custom.authPii overrides, and emits a
deterministic YAML PII inventory at compliance/data-map.yml.
Supports --print (stdout) and --check (diff vs committed, exit 1 on
mismatch) modes. Ships with 26 unit tests covering happy path, auth
defaults, authPii overrides, --check match/mismatch, and empty
collections. Wired as `compliance:data-map` root package script.
Adds @typescript-eslint/parser to root devDependencies (already in
workspace via core-eslint, now made explicit for scripts/ usage).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tag displayName as identification-username PII and declare daily purge
with 30-day post-deletion hard-delete retention. PAYLOAD_AUTH_PII_DEFAULTS
covers email/credentials automatically — no authPii override needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds retention-purge.job.ts to packages/core-shared/src/payload/retention-purge/.
Walks every Payload collection's custom.retention.purgeSchedule, registers one
scheduled job per collection via IJobQueue, queries rows past their activeRetention
period (by createdAt for from-creation, updatedAt for from-last-access), and applies
pseudonymize (null PII fields) or hard-delete per postDeletion.action. Emits one
IAuditLog.record entry per processed row; gracefully skips when auditLog is absent.
Unit tests cover: schedule registration, trigger-type routing, hard-delete branch,
pseudonymize branch, audit emission, graceful auditLog skip, no-activeRetention
short-circuit, and input validation error cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the `conformance/pii-declaration-must-be-complete` ESLint rule at
warn severity. The rule detects `custom: { pii: { ... } }` blocks in
Payload config files and warns when any of the four required sub-fields
(`category`, `purpose`, `exportable`, `restrictable`) is missing.
Incomplete PII declarations can produce incorrect audit reports —
sub-second editor feedback catches the gap before it reaches
compliance/data-map.yml.
- Rule + 7 RuleTester fixtures (complete passes, each missing field
warns, non-pii custom block is no-op, malformed custom.pii is no-op)
- Registered in plugin.js + base.js at "warn"
- Conformance rule count bumped 7 → 8 in CLAUDE.md +
conformance-quickref.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ambient declaration files have no runtime code so v8 coverage
never generates DA records for them. Without an allowlist entry,
coverage:diff reports no-coverage-data for every .d.ts in the
diff. Add /\.d\.ts$/ to ALLOWED_GLOBS with a companion test.
Also configure @vitest/coverage-v8 for core-shared and add
targeted vitest exclusions for infrastructure files that are not
unit-testable (DI symbols, interface files, tRPC context, Sentry
SDK init) — bringing core-shared into the L2 aggregate and making
the L1 diff gate enforce coverage on new executable code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces PiiCategory, DataProcessingPurpose, RetentionTrigger,
RetentionAction, FieldPii, FieldRetention, PAYLOAD_AUTH_PII_DEFAULTS,
PurgeSchedule, and CollectionRetention in core-shared/payload/.
Augments payload's FieldCustom and CollectionCustom interfaces via
ambient declaration so downstream collection configs gain typed
custom.pii and custom.retention / custom.authPii fields.
Credential fields (password, salt, hash, resetPasswordToken,
resetPasswordExpiration, loginAttempts, lockUntil, apiKey, apiKeyIndex)
are null in PAYLOAD_AUTH_PII_DEFAULTS to exclude security material
from DPA mapping. Adds @vitest/coverage-v8 and coverage exclusions
for boilerplate infrastructure files so coverage:diff is gated on
new executable code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a ./react subpath export to @repo/core-analytics containing
<AnalyticsProvider value={IAnalytics}> and useAnalytics(): IAnalytics.
useAnalytics() throws AnalyticsContextError when called outside a provider.
React Testing Library test verifies track() flows through context using
RecordingAnalytics. Switches vitest config to pick up .tsx test files
via environmentMatchGlobs and extends tsconfig to react-library.json
for JSX support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the existing Audited check: when a use case declares
analyticsEvents.length > 0 and the bound function lacks the __analyzed
brand, assertFeatureConformance throws ConformanceError at boot time.
Adds three synthetic conformance tests: passes when brand present +
events declared, throws naming Analyzed when events declared + brand
missing, passes when events empty + brand absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `withAnalytics(analytics, factory)` to packages/core-analytics —
mirrors the `withAudit` pattern: thin forwarding closure that attaches
the `__analyzed` brand via `attachBrand` from `@repo/core-shared/conformance`
without mutating the original factory. Exports `Analyzed<F>` type and
`withAnalytics` from the `@repo/core-analytics` root barrel.
Adds `with-analytics.test.ts` asserting brand is present after wrapping,
absent on the original fn, output passes through unchanged, and errors
propagate. Adds `@repo/core-shared` as a production dependency.
Also fixes `scripts/library-decisions/check.mjs` to exempt workspace-protocol
entries (`workspace:*`) from the library trace requirement — internal monorepo
packages are not third-party libraries and were incorrectly gated. Adds a
regression test in `check.test.mjs` covering the exemption.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace inline withSpan(withCapture(factory(deps))) form in the binder
templates with wireUseCase({...}) calls so newly scaffolded features are
consistent with the migrated production features.
Also add assertFeatureConformance to bind-dev-seed.ts.hbs (aligns with
the migrated auth/navigation pattern) and fix bind-dev-seed.test.ts.hbs
to call binders with the ctx object form (BindContext) instead of the old
two-argument (tracer, logger) form.
Verified by running turbo gen feature testfeature and confirming:
- Generated binders use wireUseCase for use cases
- All 5 conformance gates pass on the scaffold
- Scaffold cleaned up post-verification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace inline withSpan + withCapture blocks for signIn, signUp, and
signOut use cases in both bind-production.ts and bind-dev-seed.ts with
wireUseCase calls. Removes 27 lines of boilerplate per binder file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the per-layer threshold gaps surfaced by the 2026-05-13 PRD
audit. After this commit all five features pass their declared
100%/100%/95%/100% bands on entities + use-cases + controllers.
media (was: missing @vitest/coverage-v8 + missing vitest config block +
one controller at 86.66% lines / 75% branches)
- Added @vitest/coverage-v8 dev dep
- Applied the standard helper-driven vitest config
- Declared the coverage section in feature.manifest.ts
- Added 2 tests to list-media.controller.test.ts covering the
InputParseError branch (unknown fields + invalid limit)
- Now: 16 files / 80 tests / 97.12% / controllers 100%
marketing-pages (was: get-site-settings.controller at 93.54% lines /
90.9% branches)
- Added 1 test to get-site-settings.controller.test.ts covering the
InputParseError branch on unknown fields
- Now: 22 files / 68 tests / 95.66% / controllers 100%
navigation (was: entities/errors/common.ts at 50% function hits +
get-header.controller at 86.66% lines / 80% branches)
- Root cause: InputParseError class never instantiated in any test
- Added 2 tests to get-header.controller.test.ts covering the
InputParseError branch + verifying the Zod cause is preserved.
One test exercises both gap files at once (controller throws,
InputParseError class is constructed).
- Wired navigation/vitest.config.ts through the shared helper
- Declared the coverage section in feature.manifest.ts
- Now: 11 files / 45 tests / 98.04% / entities + controllers 100%
All 5 features now drive thresholds from the manifest via the helper.
The duplication problem the keystone eliminates is gone.
Repo-wide via `pnpm coverage:aggregate`:
- statements 95.87% (lh 2994 / lf 3123)
- branches 88.91% (brh 433 / brf 487)
- functions 100% (fnh 142 / fnf 142)
- lines 95.87%
`pnpm coverage:diff -- --base HEAD~1` reports status: pass.
coverage/summary.json refreshed in the same commit so the trend
captures the post-unification state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands L2 of the agent-first coverage architecture (ADR-020) — the
aggregated trend store.
Script: scripts/coverage/aggregate.mjs (zero-dep Node ESM)
- discoverLcovs: walks packages/* and apps/* for coverage/lcov.info
- normalizeLcov: rewrites SF entries from package-relative (vitest's
output) to repo-relative, so the merged file matches git diff paths
- summarizeLcov: computes statement/branch/function/line percentages
from LF/LH/BRF/BRH/FNF/FNH summary records
- aggregate: merges all lcovs and returns mergedLcov + summary
- Writes coverage/lcov.info (gitignored — large) and
coverage/summary.json (committed — trend via git log -- ...) with
timestamp, short commit SHA, repo + per-package percentages
Test surface: scripts/coverage/aggregate.test.mjs (10 tests, all green)
- Fixtures at __fixtures__/aggregate-pkg-a.lcov +
aggregate-pkg-b.lcov (synthetic, structured to make percentages
deterministic)
- Covers: path normalization (prefix, absolute, double-prefix
avoidance), summary computation (percentages, zero-division,
rounding), discovery (packages + apps, missing dirs), full
aggregation in a tmp repo
Wired:
- root package.json adds "coverage:aggregate" script
- .gitignore restructured: per-package coverage/ stays ignored,
aggregated /coverage/ ignored EXCEPT summary.json (committed for
trend) and .gitkeep markers
L1 allowlist fix folded in (scripts/coverage/diff.mjs):
- The previous (^|/)coverage/ regex accidentally caught
scripts/coverage/* — replaced with anchored patterns
(^coverage/, ^packages/*/coverage/, ^apps/*/coverage/)
- Allowlist scripts/ and turbo/generators/ since they're dev tooling
tested via node --test, outside vitest's v8 lcov pipeline
Smoke-tested end-to-end:
- pnpm coverage:aggregate merged 3 lcovs (auth + media + navigation
from this session's earlier runs), repo coverage 95.22% statements
- pnpm coverage:diff against HEAD~1 with the new merged lcov reports
PASS — all 6 diff files correctly allowlisted
First committed snapshot of coverage/summary.json lands with this
commit, anchoring the trend history at this state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>