Commit Graph

496 Commits

Author SHA1 Message Date
bab8fb634f fix(marketing-pages): throw PageNotFoundError for missing slugs
getPageBySlug returned undefined while the feature already mapped
PageNotFoundError to NOT_FOUND and blog throws for missing slugs (B11).
Unify on the throwing contract: use case throws, controller narrows to
a non-optional presenter, the server component catches the domain error
to render its not-found state, and the router now surfaces NOT_FOUND.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:25:41 +02:00
88ac2649b7 fix(media): return the exact window for non-aligned offsets
Same B9 defect as blog: Payload paginates by page, so a non-aligned
offset returned the wrong window. Fetch the straddled pages and slice
the intra-page remainder. Identical window tests now run against the
mock and the fake-payload stub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:29:16 +02:00
50c30e9c1b fix(blog): return the exact window for non-aligned offsets
Payload paginates by page, so floor(offset/limit)+1 alone returned the
wrong window whenever offset % limit != 0 (B9). Fetch the straddled
pages and slice the intra-page remainder. The test stub now honours
page like real Payload (it silently ignored it before), and the
contract pins aligned + non-aligned windows on mock and stub alike.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:29:06 +02:00
b66759a1ab fix(auth): derive clientIp server-side, drop it from sign-in input
clientIp was part of the public signInInputSchema, so any client could
spoof its own rate-limit bucket or dodge IP throttling entirely (audit
finding B2). The schema no longer carries it (strict parsing rejects it
with BAD_REQUEST); instead the web-next tRPC fetch adapter derives it in
createTrpcContext from x-forwarded-for (first hop) / x-real-ip — trust
caveat documented — and the router threads ctx.clientIp to the
controller as a second, server-only argument typed outside the input
schema (SignInRequestContext).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:28:56 +02:00
4d6734448a test(auth): cover signToken/verifyToken/validateSession crypto paths
The session methods had zero coverage because they call getPayload()
(audit finding B8). Stub the payload module (secret + findByID only) and
exercise the real crypto paths: round-trip, tampered signature, swapped
payload, expired exp, malformed segments, wrong secret, jti-less token
(fail closed), post-revocation rejection, and the per-instance denylist
limit. No running Payload needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:26:01 +02:00
db2afde0dc feat(auth): revoke sessions server-side via a jti denylist
invalidateSession previously ignored its argument, so a stateless JWT
stayed valid until exp after logout (audit finding B5). createSession
now embeds the minted session.id in the token as jti; invalidateSession
records that jti in an in-memory denylist with expiry-based pruning, and
validateSession rejects denylisted (and jti-less, failing closed)
tokens. The denylist is per-process — the single-process limitation and
the shared-store upgrade path are documented in session-denylist.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:25:51 +02:00
32163312e3 chore(core-typescript): put its TS sources under the typecheck gate
The package's vitest base configs were outside every gate — no tsconfig
and no typecheck script meant tsc never saw them. Add a tsconfig with
allowImportingTsExtensions (vitest.base.jsdom imports ./vitest.base.node.ts
by extension) and an explicit include of the base configs + vitest
config, plus the typescript/@types/node devDeps to run it. (S6)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:24:09 +02:00
00fcc9d9a1 fix(core-shared): set CSP on forwarded request headers for nonce
Next.js only injects the nonce into its own scripts when it can read it
from the request's Content-Security-Policy header. Setting the CSP only
on the response left hydration scripts un-nonced in production (A9).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:24:01 +02:00
2747feab46 fix(auth): stop leaking usernames via emailDomain span attribute
getUserByUsername emitted the FULL username as the emailDomain span
attribute whenever the username contained no "@" (audit finding B6),
violating the PII-free telemetry rule (ADR-017 §7). Emit only a boolean
hasAtSign in both the production repository and its mock; regression
tests pin that no username-derived string reaches span attributes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:22:00 +02:00
27787193c0 fix(core-testing): align RecordingEventBus with real bus semantics
Handlers and the published record now receive the zod-PARSED payload
(defaults/coercions applied) instead of the raw input, and fan-out uses
Promise.allSettled with errors swallowed by default plus an opt-in
failFast — matching the InMemoryEventBus the events core-package
generator scaffolds. The previous sequential fail-fast divergence was
undocumented, so it is aligned rather than kept. Regression tests pin
parsed-payload delivery, non-short-circuiting fan-out, and failFast
rethrow. (S4; the generator bus templates already carry the fix.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:21:52 +02:00
c9db7c8cd7 fix(auth): add username + passwordHash fields to users collection
The production UsersRepository reads and writes username + passwordHash
via the Payload local API, but the users collection never declared them,
so production sign-up/sign-in was broken (audit finding B1). passwordHash
uses access.read: () => false so credential material never serializes
through any Payload API surface; the repository still reads it with
overrideAccess: true. A contract-shaped test pins every repo-used field
(USERS_REPOSITORY_FIELDS) against the collection config so drift fails
at test time without a database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:21:16 +02:00
bb2751eede docs: correct stale no-op comments in node test setup
setup/node.ts installs the no-instrumentation guard (vi.mock against
real Sentry/OTel SDK init) via its import — it was never a no-op. Fix
the same lie in core-typescript's jsdom base-config test comments and
test title. (S2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:20:27 +02:00
3cf2572c85 chore(fallow): make the whole-codebase gate pass without hiding signal
pnpm fallow failed on 14 dead-code issues, 200 clone groups (4.3% >
3.0% threshold) and one cognitive-complexity breach. Changes:
- duplicates.ignore covers convention-mandated boilerplate only (test
  files, stories, fixtures/factories/seeds, binders, symbols, feature
  manifests, vitest configs, mock/repo twins, sentry init twins,
  compliance emitters, rule-meta boilerplate) at threshold 3.0
- usedClassMembers: validateSession (interface-implemented, not yet
  called); ignoreExports: Next's generateMetadata convention export +
  the __getInstrumentationForTests test knob
- duplicate-exports off: client/server RSC twins export the same
  component name by design
- @trpc/client + @trpc/react-query added to ignoreDependencies (peer
  resolution for feature ./ui hooks); *.test.mjs marked dynamically
  loaded (node:test files fallow saw as unreachable)
- delete packages/auth/src/ui/query.ts (empty export{} placeholder
  shadowed by ui/index.ts, genuinely dead)
- extract checkDepTrace/decisionError from checkLibraryDecisions
  (cognitive 32 > 30) — behavior unchanged, tests pass

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:53:16 +02:00
5f38a3efce build: bump vitest past GHSA-5xrq-8626-4rwp; coverage-v8 everywhere
pnpm audit --prod --audit-level=critical (the new CI gate) failed on
vitest <3.2.6 (critical, arbitrary file read/execute via the UI
server). Bump vitest + @vitest/coverage-v8 in-range across the
workspace and add @vitest/coverage-v8 to every package that runs
vitest but lacked it (core-audit, core-cms, core-eslint, core-testing,
core-trpc, apps/cms, web-next, web-tanstack, turbo/generators) so
'pnpm test -- --coverage' works in every package. No compound test
scripts exist, so the vitest-last pass-through concern does not apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:48 +02:00
1e3220aef5 fix(core-ui): stop excluding Storybook stories from typecheck
*.stories.tsx / *.stories.ts were excluded from tsconfig, so story
files never typechecked and could drift against component props
silently. Include them (they already typecheck cleanly); build is
tsc --noEmit so nothing new is emitted. The Select scrollIntoView port
from the downstream fork does not apply — this tree has no Select atom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:33:41 +02:00
16310c5d62 test(core-shared): assert PII scrub runs before Sentry processors
The previous init-server-node tests only checked that an SDK handle
came back — the ADR-017 §7 invariant (PiiScrubSpanProcessor /
PiiScrubLogRecordProcessor registered BEFORE any exporter-facing
processor) was untested. Capture the NodeSDK constructor config via a
local vi.mock override and assert processor ordering for both the
empty-DSN and DSN-set paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:32:47 +02:00
0234e18425 fix(core-testing): handle batched paths + transformed errors in mock tRPC
httpBatchLink joins same-tick calls into one request whose path is a
comma-separated list of procedure paths; the stub matched the joined
string against a single mock key, so any batched pair failed with 'No
mock for a.one,a.two'. It now answers one element per procedure, in
order. Error bodies are also superjson-serialized — raw error JSON made
the client throw 'Unable to transform response' instead of surfacing
the intended error. Regression tests added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:32:03 +02:00
6fd746d3bd fix(core-eslint): activate React rules-of-hooks for all TSX surfaces
next.js and react-internal.js are plain re-exports of base, so no
rules-of-hooks checking was active anywhere despite three React apps
and core-ui. Wire eslint-plugin-react-hooks in base.js scoped to
**/*.tsx (non-React packages untouched). Fixes the one violation it
surfaced: web-tanstack's root route called Route.useLoaderData inside
an anonymous component callback — extracted to a named RootComponent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:27:16 +02:00
9b04fae975 fix(core-shared): derive Analyzed + RateLimited brands in binding slot
ProductionUseCase<I, O, M> only demanded Instrumented + Captured (+
Audited for mutating-with-audits). The boot assertion additionally
requires __analyzed for non-empty analyticsEvents and __rateLimited for
non-empty rateLimit, so the type-level gate under-promised what boot
enforces. The slot now derives both from the manifest entry; the
feature-scoped requiresConsent brand stays boot-only (documented).
Also make IAnalytics extend AnalyticsProtocol from
core-shared/di/bind-protocols so narrowing the ctx protocol fails
typecheck in core-analytics instead of drifting silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:25:09 +02:00
bf04ad70b2 chore: repair pre-existing lint failures blocking the lint gate
- turbo/generators/config.ts used require() inside the reader generator
  action (no-require-imports); use the top-level node:fs imports
- auth authentication.service.ts used a literal self-assertion
  ("users" as "users"); prefer-as-const
- apps/cms lacked web-next's next-env.d.ts triple-slash-reference
  override, and the committed next-env.d.ts now references
  .next/types/routes.d.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:21:26 +02:00
498f1fb57a fix(core-eslint): enforce ADR-017 OTel SDK import restriction
The base config documented (and carried allowlist off-blocks for) the
OTel-SDK import restriction but the no-restricted-imports pattern group
only covered @sentry/*. Add @opentelemetry/sdk-*, exporter-*,
instrumentation-*, resources and semantic-conventions to the restricted
group, and extend the off-block for core-audit's trace-id enrichment
test, which legitimately builds an in-memory sdk-trace-base tracer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:21:25 +02:00
0c1df7f5d4 fix(core-eslint): fail loudly on unparseable manifest in wiring gate
usecase-must-be-wired returned {} when the manifest parsed to null,
silently disabling the error-level gate when the manifest existed but
could not be read. It now reports unparseableManifest on Program in
that case; a genuinely missing manifest stays a no-op (that is
feature-must-have-manifest's job).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:11:59 +02:00
3bf0c652e7 fix(core-eslint): parse satisfies-shaped manifests; unify manifest parser
A manifest written as `{...} satisfies FeatureManifest` (or the combined
`as const satisfies` idiom) parsed to null, silently no-oping the
error-level conformance rules. unwrapAsConst now strips TSAsExpression
and TSSatisfiesExpression in a loop. The file also carried a verbatim
second copy of its own parser for parseManifestFully; both public entry
points now share one implementation. The template's field set (audits,
rateLimit, requiresConsent) is preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:10:35 +02:00
danijel-lf
0a34b45bb7 feat(auth): implement session methods with Payload-backed JWT
Some checks failed
CI / typecheck + lint + boundaries + test + build (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Coverage snapshot / snapshot (push) Has been cancelled
Release Please / release-please (push) Has been cancelled
Sentry PII guard (R31) / pii-guard (push) Has been cancelled
CI / Playwright e2e (push) Has been cancelled
CI / Storybook smoke tests + visual regression (push) Has been cancelled
Mutation testing (nightly) / mutate (push) Has been cancelled
Library trace revalidation (weekly) / revalidate (push) Has been cancelled
Replace NotImplementedError stubs in AuthenticationService with working
implementations: createSession signs a HS256 JWT using Payload's instance
secret, validateSession verifies and decodes the token then looks up the
user, invalidateSession returns a blank cookie with maxAge 0. No external
JWT dependency — uses Node crypto HMAC directly.

Also clarify withAudit/withAnalytics comments: the wrappers intentionally
delegate recording to the use case body (only it knows which fields to
extract), so the TODO was misleading.
2026-05-28 22:41:30 +02:00
danijel-lf
5b74939a51 feat(conformance): implement ReadOnly brand and reader generator
- Add ReadOnly<F> phantom brand to core-shared/conformance (compile-time
  enforcement that readers only wrap non-mutating use cases)
- Add isReadOnly runtime predicate for boot-time assertReaderPurity
- Scaffold pnpm turbo gen reader: creates integrations/readers/ with
  interface, implementation, test, barrel, and adds ./reader export
  subpath to package.json
2026-05-28 22:01:51 +02:00
danijel-lf
b97e6105d3 feat(conformance): wire cross-feature reader pattern into docs and schema
Add reads field to UseCaseManifest, update CLAUDE.md with Q0-Q3 rules,
add ./reader subpath to AGENTS.md exports table, and cascade reader
conventions through conformance quickref, adding-a-feature guide, and
scaffolding guide. Moves gen reader from deferred to planned.
2026-05-28 20:55:34 +02:00
danijel-lf
7ef0411ffa refactor: split feature UI into .server/.client component pairs
Server components (.server.tsx) resolve controllers from DI, prefetch
data, and wrap client components in HydrationBoundary. Client components
(.client.tsx) use hooks for hydration + background refetch. Barrel
exports server components under clean names — consumers never see the
server/client split.
2026-05-26 15:57:38 +02:00
danijel-lf
a28de6884c feat: add feature UI components with useQuery hooks
- navigation: SiteHeader component + useHeader hook
- blog: ArticleCard, ArticleList, ArticleDetail + useArticleList,
  useArticleBySlug hooks
- marketing-pages: PageHero, PageContent + usePageBySlug,
  useSiteSettings hooks
- App pages now thin server wrappers that prefetch + hydrate;
  feature components own their data fetching via useSuspenseQuery
2026-05-26 14:12:29 +02:00
danijel-lf
3ce71447b3 feat(core-trpc): scaffold tRPC client with React Query providers
- createTRPCContext + useTRPC hook for typed client access
- NextTrpcProvider with SSR-safe absolute URL resolution
- TanstackTrpcProvider for TanStack Start apps
- /api/trpc catch-all route handler in web-next
- Wire NextTrpcProvider into app providers
- Add @repo/core-trpc to transpilePackages
2026-05-26 14:11:27 +02:00
danijel-lf
bca04f4cef fix(storybook): wire Tailwind v4 theme into Storybook preview
Some checks failed
CI / typecheck + lint + boundaries + test + build (push) Has been cancelled
CI / Playwright e2e (push) Has been cancelled
CI / Storybook smoke tests + visual regression (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Coverage snapshot / snapshot (push) Has been cancelled
Release Please / release-please (push) Has been cancelled
Sentry PII guard (R31) / pii-guard (push) Has been cancelled
- 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
2026-05-26 13:22:17 +02:00
danijel-lf
29eaefe47e fix: drop .js extensions from remaining relative imports
Some checks failed
CI / typecheck + lint + boundaries + test + build (push) Has been cancelled
CI / Playwright e2e (push) Has been cancelled
CI / Storybook smoke tests + visual regression (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Coverage snapshot / snapshot (push) Has been cancelled
Release Please / release-please (push) Has been cancelled
Sentry PII guard (R31) / pii-guard (push) Has been cancelled
Extends 8111639 to cover __factories__, __seeds__, __contracts__, and
core-testing barrels.
2026-05-26 12:04:44 +02:00
danijel-lf
3eaf50151f chore(core-cms): regenerate Payload types with consentState field
Some checks failed
CI / typecheck + lint + boundaries + test + build (push) Has been cancelled
CI / Playwright e2e (push) Has been cancelled
CI / Storybook smoke tests + visual regression (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Coverage snapshot / snapshot (push) Has been cancelled
Release Please / release-please (push) Has been cancelled
Sentry PII guard (R31) / pii-guard (push) Has been cancelled
2026-05-26 11:46:39 +02:00
danijel-lf
8111639660 fix(di): drop .js extensions from bind-dev-seed imports
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.
2026-05-26 11:45:37 +02:00
f3182537c1 fix(boundaries): allow feature-to-feature imports of public exports
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.
2026-05-21 14:30:37 +02:00
d3944f40db feat(core-eslint): add entity-must-have-test and no-relative-parent-import rules
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.
2026-05-21 11:49:45 +02:00
c099d7182b fix(core-eslint): resolve event descriptors in no-undeclared-event-publish
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.
2026-05-21 11:49:36 +02:00
f2d633c02a test(auth): use @/ alias in bind-production types test
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.
2026-05-21 11:49:26 +02:00
ae4062b607 test(blog): use @/ alias in container test imports
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.
2026-05-21 11:25:48 +02:00
affcb28c24 test(auth): add cookie schema test
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.
2026-05-21 11:25:42 +02:00
714209e63e fix(auth): declare userSignedUpEvent in signUp manifest publishes
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.
2026-05-21 11:25:35 +02: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
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
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
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
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
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
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
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
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
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