Commit Graph

1028 Commits

Author SHA1 Message Date
08bc19293a ci(release): attach CycloneDX SBOM to every GitHub release
Amends release-please.yml with conditional steps that run only when
release-please cuts a release:
- checkout + pnpm install to give @cyclonedx/cyclonedx-npm the full
  resolved workspace graph
- pnpm dlx @cyclonedx/cyclonedx-npm generates a CycloneDX 1.6 JSON SBOM
  named sbom-<tag>.cdx.json; --ignore-npm-errors is required because
  npm ls exits non-zero for dev-deps-of-dev-deps pnpm correctly elides
- softprops/action-gh-release@<SHA> (v3.0.0, Renovate-managed) attaches
  the file to the GitHub release as a downloadable asset

Adds ADR-023 §9 amendment documenting the step shape, rationale for
pnpm dlx (avoids lockfile per ADR-022), --ignore-npm-errors behaviour,
SHA pinning per ADR-023 §1, and the extended failure-mode table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 11:31:08 +00:00
224a5d78c8 chore(work): finish 09-app-wiring-web-tanstack-and-cms 2026-05-20 13:22:08 +02:00
082d4d2369 feat(cms): wire security headers middleware
Add Next.js middleware.ts to apps/cms that applies all six security
headers on every response using the framework-agnostic
buildSecurityHeaders builder. No nonce is generated or forwarded —
the CMS is server-side only so CSP nonces are not required.

Includes a test suite mirroring the web-next pattern that asserts all
six headers are set, no x-nonce is emitted, and CSP mode switches
correctly between dev (unsafe-inline) and prod (strict-dynamic).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 11:18:30 +00:00
f0968b7a3c chore(work): tick task in 09-app-wiring-web-tanstack-and-cms 2026-05-20 13:11:05 +02:00
5fd483af39 feat(web-tanstack): register security middleware and wire nonce to __root
- Add @tanstack/start + vinxi to deps so defineConfig is available
- Uncomment defineConfig registration in app.config.ts — middleware
  is now actually wired into the Nitro server hook, not just defined
- Update __root.tsx loader to call getNonce(getEvent().node.req)
  from @repo/core-shared/security/tanstack so the per-request nonce
  is read server-side and injected via <meta name="csp-nonce">
- Update __root.test.tsx: mock provides useLoaderData and asserts
  the nonce meta tag is rendered with the correct content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 11:06:26 +00:00
8d35fabaa5 fix(scripts): add app and sentry-init exclusions to coverage diff gate
Apps (web-next, web-tanstack, cms) do not configure @vitest/coverage-v8
so their source files never appear in the merged lcov; gate them out of
the cover-the-diff check.

core-shared/src/instrumentation/sentry/** is explicitly excluded from
per-package vitest coverage (see core-shared/vitest.config.ts) — exclude
from the diff gate to mirror that decision.

Adds tests for both new exclusion patterns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:36:55 +00:00
a540f3afb1 feat(web-tanstack): wire security headers middleware and nonce threading
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>
2026-05-20 10:33:09 +00:00
dc718fd9c8 chore(work): finish 08-app-wiring-web-next 2026-05-20 12:14:20 +02:00
b681e906ea feat(web-next): wire security headers middleware and nonce threading
- 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>
2026-05-20 10:10:12 +00:00
de458a6d1e chore(work): finish 07-security-header-adapters 2026-05-20 11:58:22 +02:00
a48af7e91c feat(core-shared): add TanStack Start security header adapter
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>
2026-05-20 09:56:03 +00:00
6903c59cc7 chore(work): tick task in 07-security-header-adapters 2026-05-20 11:48:13 +02:00
a736ed621d feat(core-shared): add Next.js security header middleware adapter
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>
2026-05-20 09:46:07 +00:00
6575a4857e chore(work): finish 06-security-headers-core-module 2026-05-20 11:35:46 +02:00
dd2af0c902 docs: seed PRD for compliance docs scaffolds epic (ADR-025 Epic D)
Implementation seed for ADR-025 Epic D, the final epic: seven fill-in
policy templates under docs/compliance/templates/ (2 anchored, 5
skeleton with not-code-enforced banner), a two-column pre-launch
compliance checklist mapping playbook obligations to template
mechanisms, a compliance-overview.md hub, and a landed + refreshed
operator-checklist.md. Pure docs — no code, no conformance changes.
Status: approved — ready for pnpm work decompose.
2026-05-20 11:34:29 +02:00
96274ba856 feat(core-shared): add security headers module with CSP builder and nonce util
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>
2026-05-20 09:33:13 +00:00
650a97b1bb chore(work): finish 05-auth-signin-rate-limit-backfill 2026-05-20 11:27:19 +02:00
4cc85b71ee chore(scripts): exclude core-eslint from coverage diff gate
core-eslint has no @vitest/coverage-v8 so it never generates lcov data.
Add it to the tooling-packages allowlist alongside core-testing so changes
to rule source files don't trip the no-coverage-data gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:23:28 +00:00
b61bb0c11e feat(auth): add signIn rate-limit backfill with dual ip/account budgets
Wires the rate-limit primitive end-to-end through auth.signIn as the
canonical credential-stuffing defence example:

- manifest: rateLimit [ip 5/1m, account 10/1h] on signIn use case
- use case: rateLimit: IRateLimit dep; dual consume + TooManyRequestsError
- binders: ctx.rateLimit ?? new NoopRateLimit() in bind-production + bind-dev-seed
- tRPC: TooManyRequestsError → TOO_MANY_REQUESTS error code in authProcedure
- tests: RecordingRateLimit dual-consume assertion; InMemoryRateLimit
  budget-1 ip + account rejection; coverage 100% on use-cases layer
- ESLint: _manifest-ast.js extractRateLimitNames handles RateLimitBudget
  objects ({name,window,budget}) in addition to plain string literals,
  no-undeclared-rate-limit passes on both "ip" and "account" call sites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:22:41 +00:00
91d7a24ed9 chore(work): finish 04-with-rate-limit-wrapper-and-conformance 2026-05-20 11:05:31 +02:00
cb61f51ee1 feat(core-shared): add withRateLimit wrapper and conformance enforcement
- 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>
2026-05-20 09:00:31 +00:00
24b2490d86 chore(work): finish 03-no-undeclared-rate-limit-eslint-rule 2026-05-20 10:45:02 +02:00
18cef2f889 feat(core-eslint): add no-undeclared-rate-limit conformance rule
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>
2026-05-20 08:43:30 +00:00
a478a8e6ea chore(work): finish 02-rate-limit-implementations 2026-05-20 10:38:24 +02:00
5efa0220d0 feat(core-testing): add RecordingRateLimit for test assertions
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>
2026-05-20 08:36:37 +00:00
f2449c9d65 chore(work): tick task in 02-rate-limit-implementations 2026-05-20 10:32:31 +02:00
5ddc8e6484 feat(core-shared): add NoopRateLimit and InMemoryRateLimit implementations
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>
2026-05-20 08:30:35 +00:00
e2a5278899 chore(work): finish 01-rate-limit-type-primitives 2026-05-20 10:24:32 +02:00
c62de6fce9 feat(core-shared): add rate-limit type primitives and manifest field
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>
2026-05-20 08:21:42 +00:00
a633561c82 chore(work): decompose security-headers-rate-limit-sbom epic
Generated 1 epic + 11 stories under docs/work/epics/ from the approved
PRD docs/work/prds/security-headers-rate-limit-sbom.prd.md. Stories
cover rate-limit type primitives + implementations + ESLint rule +
withRateLimit wrapper, auth.signIn rate-limit backfill, security
headers core module + framework adapters, app wiring across web-next /
web-tanstack / cms, SBOM CI workflow, and documentation. Ready for
`pnpm work dispatch --execute`.
2026-05-20 10:14:54 +02:00
352a7c64c1 chore(work): finish epic dsr-consent-and-cookie-banner 2026-05-20 00:09:29 +02:00
a3505f2e69 docs(compliance): add DSR guide, consent guide, subject-linkage example, glossary terms
- docs/guides/dsr.md: GDPR Art. 15/16/17/18/20 interface mapping, tRPC
  router wiring, multi-subject handling, soft vs cascade-hard semantics,
  DeletionCertificate format and storage requirements
- docs/guides/consent.md: requiresConsent manifest field, withConsent DI
  wiring, runtime isGranted pattern, IConsent audit trail, anonymous→
  authenticated migration, cookie _v versioning, SSR-safe banner loading,
  CNIL/EDPB equal-prominence requirement
- docs/compliance/subject-linkage.example.md: SubjectLink kind discriminator
  with worked support-ticket example (owner submitter + reference assignee)
- docs/glossary.md: SubjectLink, DeletionCertificate, UserConsentState,
  ConsentChecked entries; Manifest definition updated with requiresConsent
- CLAUDE.md: lint comment 8→12 conformance rules; conformance section notes
  requiresConsent; brand composition order updated to full 5-wrapper chain
- docs/guides/conformance-quickref.md: requiresConsent field added to
  manifest table; component-must-have-story, component-must-have-test,
  atomic-tier-import-direction added to ESLint rules table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 22:07:50 +00:00
a1587539de chore(work): finish 10-auth-signup-migration 2026-05-19 23:56:58 +02:00
6b66064386 feat(auth): migrate anonymous consent on signUp when cc_consent cookie present
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>
2026-05-19 21:52:08 +00:00
5151454783 chore(work): finish 09-cookie-consent-banner 2026-05-19 23:38:46 +02:00
051bfbf062 test(core-ui): add axe-core a11y assertions to CookieConsentBanner
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>
2026-05-19 21:36:26 +00:00
cbe7412a58 fix(scripts): exempt Storybook stories from coverage:diff gate
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>
2026-05-19 21:24:02 +00:00
1b6f2d1e36 feat(core-ui): add CookieConsentBanner headless component
Implements the EU-compliant cookie consent banner with:
- modal + banner variants, CNIL equal-prominence Reject/Accept buttons
- granular category toggles (essential non-toggleable)
- __consent_state cookie management (SameSite=Lax, Secure, 1-yr, _v:1)
- render-prop overrides: renderHeader, renderCategoryRow, renderActions
- useConsent() integration when ConsentProvider is present
- CookieConsentBannerLoader SSR-safe wrapper
- RTL behavioral tests: Reject All, Save Selected, ESC=Reject, focus-trap
- Storybook stories for modal, banner, render-prop, and a11y tab demo
- jsdom configured with HTTPS origin for Secure cookie testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:21:05 +00:00
de178035bd chore(work): finish 08-core-ui-scaffold 2026-05-19 23:03:57 +02:00
bce9ded915 feat(core-ui): scaffold @repo/core-ui via generator
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>
2026-05-19 20:59:48 +00:00
81898d9902 chore(work): finish 07-core-api-router-composition 2026-05-19 22:52:11 +02:00
759fd4cbb1 feat(core-api): compose dsrRouter + consentRouter into appRouter
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>
2026-05-19 20:49:20 +00:00
b2bfb5bfce chore(work): finish 06-core-dsr 2026-05-19 22:39:13 +02:00
6fd0a7ad4b test(core-dsr): cover dsrRouter singleton guard proxy path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:36:42 +00:00
46e575a5a6 feat(core-dsr): handlers, dsrRouter, integration tests
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>
2026-05-19 20:34:51 +00:00
6f56a04335 chore(work): tick task in 06-core-dsr 2026-05-19 22:14:15 +02:00
cbea635fac fix(core-dsr): flip processingRestrictedAt in soft-delete for self role
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>
2026-05-19 20:12:41 +00:00
6606b59d1e feat(core-dsr): Payload impls, recording doubles, DI binders, contract tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:05:06 +00:00
8068d1bf98 chore(work): tick task in 06-core-dsr 2026-05-19 21:35:10 +02:00
e378c950a9 feat(core-dsr): scaffold package + GDPR DSR interfaces and types
- Add pnpm turbo gen core-package dsr generator template and register
  dsr in CORE_PACKAGE_GENERATORS / choices list
- Run generator to produce packages/core-dsr/ shell
- Define IDataExport (Art. 15/20), IDataDelete (Art. 17),
  IDataRectify (Art. 16), IProcessingRestriction (Art. 18) interfaces
- Add UserDataBundle and DeletionCertificate types in dsr-types.ts
- Ship core-dsr/contexts/user-data.jsonld schema.org JSON-LD @context
- Wire @repo/core-dsr into transpilePackages (web-next)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 19:32:03 +00:00