- Split globals.css into globals.css + theme.css so Storybook can
import design tokens without the @import "tailwindcss" directive
that breaks Vite's preview module loading
- Prepend @tailwindcss/vite plugin for correct processing order
- Add @source to scan core-ui components for utility class generation
Webpack (Next.js transpilePackages) resolves from .ts source directly
and cannot find .js files. The bind-production binders already used
extensionless imports; align bind-dev-seed to match. Also fixes the
turbo gen feature template so new features are consistent.
turbo.json's boundary config already allows `feature -> feature`, and
the cross-feature event system depends on it — a consumer must import
the publisher's event contract from `@repo/<publisher>`. But the ESLint
boundaries config, ADR-010, and AGENTS.md still declared
`feature -> [core, tooling]`, contradicting turbo.json and the shipped
code (marketing-pages imports @repo/auth).
Align all three to turbo.json: a feature may import another feature's
published public exports. Internals stay sealed by the `exports` map,
and cross-feature behaviour still flows through IEventBus.
Two CLAUDE.md conventions had no mechanical gate, so both drifted:
entity models shipped without sibling tests, and feature test files
imported src modules via `../` instead of the `@/` alias.
- `entity-must-have-test` — every entities/models/<x>.ts needs a sibling
<x>.test.ts (errors and barrels excluded).
- `no-relative-parent-import-in-tests` — feature test files must import
src via `@/`, not `../`. Scoped to feature packages; core packages are
governed by their own generator templates.
Both register at warn level, bringing the conformance rule count to 15.
The rule only matched bus.publish("string-literal", ...), but the
canonical pattern that `gen event` prescribes is
bus.publish(eventDescriptor, payload) — an imported identifier, never a
literal. The rule therefore never fired on real code, which is how the
auth signUp publish drifted from its manifest undetected.
Add `_event-ast.js`: resolves a `bus.publish(<identifier>)` argument by
following the import to the event-contract file and extracting the name
from either `defineEvent("...", schema)` or an inline `{ name }` object.
Unresolvable arguments are skipped, so the rule never false-positives.
bind-production.types.test.ts imported feature.manifest and the sign-in
use case with `../` parent paths. Test files import src modules via the
`@/` alias per the repo convention — this clears the way for the new
no-relative-parent-import-in-tests rule.
container.test.ts imported cross-directory modules with `../...` paths.
Test files use the `@/` alias for src imports per the repo convention;
this was the outlier vs auth/media/navigation.
cookie.ts was the only entity model in the feature packages without a
sibling test. Covers minimal/full attributes, invalid sameSite, and a
missing required field.
The signUp use case calls `bus.publish(userSignedUpEvent, ...)` but its
manifest entry declared `publishes: []`. The conformance event graph was
blind to the entire auth -> marketing-pages welcome-email flow as a
result. Declaring "auth.user.signed-up" makes `pnpm conformance` surface
the event and its publisher.
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>
Captures consume + reset call arguments verbatim via consumeCalls and
resetCalls accessors. Uses local IRateLimit type alias (no core-shared
dep) following the recording-job-queue pattern.
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>
Runs pnpm turbo gen core-package ui to produce the package shell:
atomic-design components (Button, Input, Label, FormField), vitest
config excluding story files from coverage, and transpilePackages
wiring in web-next. Adds @vitest/coverage-v8 devDep and
label.stories.tsx to satisfy lint/coverage gates.
Also fixes scripts/library-decisions/check.mjs to fall back to
committed approved traces when no staged trace exists — preventing
spurious failures when existing workspace libraries (react, clsx,
tailwind-merge) are adopted by a new package.
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>
Add four protocol-agnostic handlers (export, delete, rectify, restrict)
returning normalized { status, body, headers } responses, and a tRPC
dsrRouter via createDsrRouter(binding) following the factory pattern.
Auth checks: requireAuthenticated middleware gates all four procedures;
cascade-hard delete additionally requires admin role. Integration tests
assert happy-path response shapes, UNAUTHORIZED/FORBIDDEN error codes,
and error passthrough from the DSR service layer.
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>
Extends the conformance ESLint layer with the consent-check rule:
- `no-undeclared-consent-check` (warn): `consent.isGranted("X")` in a
use-case file must match a category declared in `manifest.requiresConsent`;
also warns when requiresConsent is declared but no isGranted call is found.
- `_manifest-ast.js`: adds `parseManifestFully` which extracts top-level
`name`, `requiredCores`, `requiresConsent`, and per-use-case maps from the
manifest AST; `requiresConsent` extraction tested in `_manifest-ast.test.js`.
- `_rule-context.js` / `_rule-schema.js`: shared helpers extracted from the
existing per-rule files so the new rule can resolve use-case name + feature
root without duplication.
- Existing rules (`no-undeclared-audit`, `no-undeclared-event-publish`,
`no-undeclared-analytics-event`) updated to use the shared helpers.
- `plugin.js` + `base.js` register the rule at warn severity.
- CLAUDE.md + conformance-quickref.md: rule count advanced from 11 → 12.
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 custom.retention (monthly purge, 90-day post-deletion hard-delete)
to the media Payload collection. No uploadedBy field exists in the
collection so no custom.pii annotation is needed.
Monthly purge schedule with 90-day post-deletion hard-delete window.
Part of compliance backfill (Story 05).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>