---
id: 05-auth-signin-rate-limit-backfill
epic: security-headers-rate-limit-sbom
title: auth.signIn rate-limit backfill
type: technical-story
status: done
feature: auth
depends-on:
  [
    01-rate-limit-type-primitives,
    02-rate-limit-implementations,
    03-no-undeclared-rate-limit-eslint-rule,
    04-with-rate-limit-wrapper-and-conformance,
  ]
blocks: []
created: 2026-05-20T00:00:00Z
updated: 2026-05-20T09:27:20.152Z
---

## Goal

Apply the rate-limit primitive to `auth.signIn` as the canonical reference example that every downstream consumer will copy — manifest declaration, dual `consume` calls in the use-case body with `TooManyRequestsError` throws, binder wiring, and extended tests — demonstrating end-to-end credential-stuffing defence that passes lint and conformance.

## Why

`auth.signIn` is the highest-risk write path in the template. Without rate-limit gates, credential-stuffing and account-enumeration windows stay open until a real consumer notices their auth logs. Backfilling this use case with the complete pattern (manifest → use-case body → binders → tests) also validates that all four preceding stories form a coherent system: if any type, impl, ESLint rule, or wrapper is mis-shaped, this story's conformance gate will surface it.

## Done when

- `packages/auth/src/feature.manifest.ts` `signIn` entry declares `rateLimit: [{ name: "ip", window: "1m", budget: 5 }, { name: "account", window: "1h", budget: 10 }]`.
- `signInUseCase` deps include `rateLimit: IRateLimit`; body calls `rateLimit.consume("ip", \`signIn:ip:${input.clientIp}\`)` and `rateLimit.consume("account", \`signIn:account:${input.email}\`)`, throwing `TooManyRequestsError`on`!allowed`.
- `packages/auth/src/di/bind-production.ts` + `bind-dev-seed.ts` pass `ctx.rateLimit ?? new NoopRateLimit()` into signIn's `wireUseCase`.
- Existing signIn unit tests extended: `RecordingRateLimit` asserts both `consume` calls captured; `InMemoryRateLimit` at budget 1 asserts second call throws `TooManyRequestsError`.
- `no-undeclared-rate-limit` ESLint rule passes (no warnings) on both `"ip"` and `"account"` call sites.
- `assertFeatureConformance` boot assertion passes (signIn is `RateLimited` branded).
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.

## In scope

- `feature.manifest.ts` — `rateLimit` declaration for `signIn`.
- `sign-in.use-case.ts` — `rateLimit: IRateLimit` in deps, dual `consume` + `TooManyRequestsError` throw.
- `TooManyRequestsError` class (if not already present in `auth/entities/errors/`) — add alongside other auth errors.
- `bind-production.ts` + `bind-dev-seed.ts` — wire `ctx.rateLimit ?? new NoopRateLimit()` into signIn.
- Unit tests: `RecordingRateLimit` dual-consume assertion; `InMemoryRateLimit` budget-1 rejection.

## Out of scope

- `auth.signUp` rate-limit — not declared in the PRD; consumer adds when needed.
- Rate-limit for any other auth use case — beyond the canonical example scope.
- Redis-backed wiring — consumer adds via ADR-022 library evaluation.

## Tasks

- [x] Add `rateLimit: [{ name: "ip", window: "1m", budget: 5 }, { name: "account", window: "1h", budget: 10 }]` to the `signIn` entry in `packages/auth/src/feature.manifest.ts`; add `rateLimit: IRateLimit` to `signInUseCase` deps; add `TooManyRequestsError` to auth error types if absent; insert dual `rateLimit.consume` calls with `TooManyRequestsError` throws in the use-case body; update `bind-production.ts` + `bind-dev-seed.ts` to pass `ctx.rateLimit ?? new NoopRateLimit()` into signIn's `wireUseCase`; extend signIn unit tests with `RecordingRateLimit` dual-consume assertion and `InMemoryRateLimit` budget-1 rejection; all gates pass.
