Files
support_backend/specs/002-saas-integration/tasks.md
T
saqib mirandClaude Sonnet 5 55253287b3 feat: per-integration and per-user rate limiting (US3) + polish
Implements tasks T026-T032 from specs/002-saas-integration/tasks.md
(User Story 3, P3 - the final piece of this feature) plus Polish.

- New bespoke Redis fixed-window counter (checkRateLimit,
  src/infrastructure/cache/rate-limiter.ts) rather than
  @fastify/rate-limit's default onRequest-stage hook -- that hook
  runs before this feature's preHandler-based auth resolves the
  integration/user identity the limit needs to key on. A second
  preHandler (checkIntegrationRateLimit) runs after
  authenticateProductIntegration on the inbound route, checking the
  integration-level limit then the per-user limit independently,
  each throwing the existing RateLimitError (429
  RATE_LIMIT_EXCEEDED) on breach.
- New integration test (inbound-rate-limit.test.ts) verifies both
  limits are enforced independently against a real Postgres/Redis:
  a throttled user doesn't affect others, and the integration cap
  throttles even when no individual user has hit their own limit.
- Docs: contracts/quickstart updated from the placeholder
  "RATE_LIMITED" code to the actual reused RATE_LIMIT_EXCEEDED code;
  cleaned up a duplicated paragraph in the admin endpoints section;
  added a "SaaS Integration" section to README.md documenting the
  inbound contract, admin routes (and their known auth-stub
  limitation), and how rate limits are configured.

All 32 tasks in tasks.md are now complete -- all three user stories
(P1 trust boundary, P2 admin lifecycle, P3 rate limiting) are
implemented and covered by integration tests verified against a
live database, in addition to unit tests for the crypto/token
primitives. Full quality gate (typecheck/lint/format/architecture/
unit tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 14:42:20 +05:30

13 KiB

description
description
Task list for 002-saas-integration

Tasks: SaaS Product Integration & Inbound Request Trust

Input: Design documents from specs/002-saas-integration/

Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/inbound-request-contract.md, quickstart.md

Tests: Not explicitly requested as TDD in spec.md, but this feature is a security boundary — unit tests for the token/scope/replay logic and integration tests for the full preHandler are included as first-class tasks (not optional), since "MUST reject" requirements are exactly what regressions silently break.

Organization: Tasks are grouped by user story (US1 = P1 authenticate/validate, US2 = P2 admin lifecycle, US3 = P3 rate limiting).

Format: [ID] [P?] [Story] Description

All file paths are relative to supporthub-api/ (repo root).

Path Conventions

Single project. Prisma schema at prisma/schema.prisma; new plugin under src/plugins/; extended module under src/modules/catalog/products/; tests under tests/unit/ and tests/integration/.


Phase 1: Setup

  • T001 Add INTEGRATION_CREDENTIAL_ENCRYPTION_KEY (32-byte, required) to the Zod schema in src/config/env.ts, and add it to .env.example, .env.development, .env.test
  • T002 [P] Add IdempotencyKey-shaped Zod primitive and shared integration-error codes (INVALID_INTEGRATION_CREDENTIAL, PRODUCT_INTEGRATION_SUSPENDED, REQUEST_OUT_OF_SCOPE) to src/common/constants/app.constants.ts (or a new src/common/constants/integration.constants.ts) for reuse by both the plugin and the module

Checkpoint: Config and shared constants exist for everything below to reference.


Phase 2: Foundational (Blocking Prerequisites)

Purpose: The schema and low-level crypto/verification primitives every user story depends on.

⚠️ CRITICAL: No user-story stage work can begin until this phase is complete.

  • T003 Update prisma/schema.prisma: replace the placeholder Product model with the doc-06-aligned shape (externalProductId, supportEnabled, status: String, integration ProductIntegration? relation) per data-model.md; keep Category, User, AuditLog untouched (research.md "Reconciling the placeholder Prisma schema")
  • T004 Add ProductIntegration model to prisma/schema.prisma per data-model.md (depends on T003)
  • T005 [P] Add CustomerReference model to prisma/schema.prisma per data-model.md (depends on T003)
  • T006 Update src/modules/catalog/products/schema/products.schema.ts's productQuerySchema to query by externalProductId instead of the now-removed code field (depends on T003)
  • T007 Run npm run prisma:generate and create the migration (npm run prisma:migrate) for T003-T005 (depends on T004, T005, T006)
  • T008 [P] Implement the AES-256-GCM encrypt/decrypt helpers (research.md "Credential storage") in src/modules/catalog/products/mapper/credential.crypto.ts — pure functions, no Prisma/Fastify dependency, so they're independently unit-testable
  • T009 [P] Implement signed-token issue/verify helpers (HMAC-SHA256 JWT: productId, tenantId, userId, iat, exp, jti) in src/modules/catalog/products/mapper/integration-token.ts (depends on T008 for how the per-integration secret is obtained, but the signing/verification logic itself has no Prisma dependency)
  • T010 [P] Implement the replay-check helper (hasSeenJti / markJtiSeen, TTL-bound) in src/infrastructure/cache/ using the existing cacheService abstraction (src/infrastructure/cache/cache.service.ts)

Checkpoint: Schema migrated; crypto/token/replay primitives exist and are independently unit tested (see Phase 3). User Story 1's plugin can now be wired up.


Phase 3: User Story 1 - Every inbound request is authenticated and trusted before anything happens (Priority: P1) 🎯 MVP

Goal: A product-integration-auth Fastify plugin validates every inbound request through the 9-step order in contracts/inbound-request-contract.md, populating request.reqContext only on full success, and audit-logging every attempt.

Independent Test: Quickstart Scenarios 1-4 (valid request accepted; invalid/unregistered credential rejected identically; suspended product distinguishably rejected; unknown field rejects the whole request).

Tests for User Story 1

  • T011 [P] [US1] Unit tests for credential.crypto.ts (encrypt/decrypt round-trip, wrong key fails) in tests/unit/products/credential-crypto.test.ts
  • T012 [P] [US1] Unit tests for integration-token.ts (valid token verifies; tampered signature rejected; expired token rejected; clock-skew tolerance boundary) in tests/unit/products/integration-token.test.ts
  • T013 [P] [US1] Integration test for the full preHandler covering Quickstart Scenarios 1-4 against a real Postgres/Redis in tests/integration/product-integration-auth.test.ts

Implementation for User Story 1

  • T014 [US1] Add ProductIntegrationsRepository (Prisma-backed: find by product, find active by internal id, update rotation/revocation fields) in src/modules/catalog/products/repository/product-integrations.repository.ts (depends on T007)
  • T015 [US1] Add CustomerReferencesRepository (find-or-create by externalUserId+externalTenantId) in src/modules/catalog/products/repository/customer-references.repository.ts (depends on T007)
  • T016 [US1] Define the strict inbound request Zod schema (.strict(), per data-model.md's Inbound Request Contract table, including the reserved idempotencyKey) in src/modules/catalog/products/schema/inbound-request.schema.ts (depends on T002)
  • T017 [US1] Implement product-integration-auth.plugin.ts in src/plugins/: runs the 9-step validation order from contracts/inbound-request-contract.md, using T009/T010/T014 /T015/T016, populating request.reqContext (productId, customerId, tenantId, actorType: CUSTOMER, actorId) only after every step passes (depends on T014, T015, T016)
  • T018 [US1] Write one AuditLog row per attempt (success or every failure reason) inside the plugin, per the AuditLog field mapping in data-model.md — never including the raw token/credential (depends on T017)
  • T019 [US1] Register product-integration-auth.plugin.ts in src/bootstrap/plugins.bootstrap.ts, scoped only to the inbound SaaS-facing route (not global) (depends on T017)
  • T020 [US1] Run Quickstart Scenarios 1-4 locally against a seeded integration and confirm all four pass

Checkpoint: User Story 1 is fully functional and independently testable — the trust boundary exists and correctly accepts/rejects/distinguishes every case in scope.


Phase 4: User Story 2 - An admin can onboard, rotate, and revoke a product's integration credential (Priority: P2)

Goal: Admin endpoints to register/rotate/revoke a ProductIntegration and retrieve its audit trail, reusing T008/T014 from Phase 2/3.

Independent Test: Quickstart Scenarios 5-7 (rotation is zero-downtime, revocation is immediate, audit trail is retrievable).

Tests for User Story 2

  • T021 [P] [US2] Integration tests for register/rotate/revoke/get-audit-trail endpoints in tests/integration/product-integrations-admin.test.ts, covering Quickstart Scenarios 5-7

Implementation for User Story 2

  • T022 [US2] Add ProductIntegrationsService methods (register, rotate, revoke, updateStatus, getAuditTrail) in src/modules/catalog/products/service/product-integrations.service.tsregister/ rotate generate a new secret, encrypt it (T008) before persisting, and return the plaintext secret in the response exactly once (depends on T014)
  • T023 [US2] Add ProductIntegrationsController with admin-authenticated handlers (register/rotate/revoke/updateStatus/getAuditTrail) in src/modules/catalog/products/controller/product-integrations.controller.ts, gated by the existing fastify.authenticate (human/admin JWT, auth.plugin.ts) — not the product-integration plugin from Phase 3 (depends on T022)
  • T024 [US2] Add routes (POST /admin/products/:id/integration, POST /admin/products/:id/integration/rotate, POST /admin/products/:id/integration/revoke, PATCH /admin/products/:id/integration/status, GET /admin/products/:id/integration/audit-trail) in src/modules/catalog/products/routes/product-integrations.routes.ts, registered from src/modules/catalog/products/routes/index.ts (depends on T023)
  • T025 [US2] Run Quickstart Scenarios 5-7 locally and confirm all three pass

Checkpoint: Both Stories 1 and 2 work together — an admin can onboard an integration and User Story 1's plugin correctly validates against whatever the admin configured.


Phase 5: User Story 3 - No single product integration or end user can overwhelm the system (Priority: P3)

Goal: Per-integration and per-user rate limiting on the inbound route, using ProductIntegration.rateLimitPerMinute/rateLimitPerUserPerMinute.

Independent Test: Quickstart Scenario 8.

Implementation for User Story 3

  • T026 [US3] Add a per-route @fastify/rate-limit registration (integration-level keyGenerator) plus a second, stricter one (user-level keyGenerator) on the inbound route, reading limits from request.reqContext-resolved ProductIntegration fields, in src/plugins/rate-limit.plugin.ts (keep the existing global registration untouched) — depends on T017 populating reqContext before the rate-limit check runs
  • T027 [P] [US3] Integration test covering Quickstart Scenario 8 (integration-level and user-level throttling, unrelated integration/user unaffected) in tests/integration/inbound-rate-limit.test.ts
  • T028 [US3] Run Quickstart Scenario 8 locally and confirm it passes

Checkpoint: All three user stories work independently and together.


Phase 6: Polish & Cross-Cutting Concerns

  • T029 [P] Add a short "Implemented in 002-saas-integration" note to specs/002-saas-integration/checklists/requirements.md Notes once all scenarios pass (docs/06-database-schema.md itself is the source spec and is intentionally not edited)
  • T030 [P] Add a "SaaS Integration" section to README.md describing the inbound contract at a high level and linking to specs/002-saas-integration/quickstart.md
  • T031 Run npx tsx scripts/check-architecture.ts and npm run lint/npm run typecheck to confirm the new module/plugin code respects existing module-boundary rules
  • T032 Full regression: npm run test:unit (which currently runs the whole suite — see specs/001-ci-pipeline/checklists/requirements.md implementation notes) to confirm nothing in catalog/products or the plugin chain broke existing tests

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): No dependencies
  • Foundational (Phase 2): Depends on Setup — BLOCKS all user stories (schema + crypto/token/ replay primitives are shared by every story)
  • User Story 1 (Phase 3): Depends on Foundational — no dependency on US2/US3
  • User Story 2 (Phase 4): Depends on Foundational (T014) — independent of US1's plugin, but practically sequenced after US1 so there's something to validate against when testing rotation
  • User Story 3 (Phase 5): Depends on US1's reqContext population (T017) — genuinely not implementable before US1, since rate-limit keys need the validated integration/user identity
  • Polish (Phase 6): Depends on all three user stories

Parallel Opportunities

  • T001/T002 (Setup)
  • T005 alongside T004 (different models, same file — coordinate to avoid edit conflicts even though marked [P])
  • T008/T009/T010 (independent primitives)
  • T011/T012/T013 (independent test files) once their subjects exist
  • T021 can be written in parallel with Phase 3's later tasks once T014 exists
  • T029/T030 in Polish

Implementation Strategy

MVP First (User Story 1 Only)

  1. Setup + Foundational (T001-T010)
  2. User Story 1 (T011-T020)
  3. STOP and VALIDATE: Quickstart Scenarios 1-4 pass — the trust boundary itself is complete and demoable even before admin tooling or rate limiting exist (a seeded integration is enough)

Incremental Delivery

  1. Setup + Foundational → schema migrated, primitives tested
  2. Add User Story 1 → inbound requests are authenticated (MVP)
  3. Add User Story 2 → integrations can be onboarded/rotated/revoked without touching the DB by hand
  4. Add User Story 3 → abuse-resistant
  5. Polish → docs and full regression