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>
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 insrc/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) tosrc/common/constants/app.constants.ts(or a newsrc/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 placeholderProductmodel with the doc-06-aligned shape (externalProductId,supportEnabled,status: String,integration ProductIntegration?relation) perdata-model.md; keepCategory,User,AuditLoguntouched (research.md "Reconciling the placeholder Prisma schema") - T004 Add
ProductIntegrationmodel toprisma/schema.prismaperdata-model.md(depends on T003) - T005 [P] Add
CustomerReferencemodel toprisma/schema.prismaperdata-model.md(depends on T003) - T006 Update
src/modules/catalog/products/schema/products.schema.ts'sproductQuerySchemato query byexternalProductIdinstead of the now-removedcodefield (depends on T003) - T007 Run
npm run prisma:generateand 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) insrc/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) insrc/infrastructure/cache/using the existingcacheServiceabstraction (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) intests/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) intests/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) insrc/modules/catalog/products/repository/product-integrations.repository.ts(depends on T007) - T015 [US1] Add
CustomerReferencesRepository(find-or-create byexternalUserId+externalTenantId) insrc/modules/catalog/products/repository/customer-references.repository.ts(depends on T007) - T016 [US1] Define the strict inbound request Zod schema (
.strict(), perdata-model.md's Inbound Request Contract table, including the reservedidempotencyKey) insrc/modules/catalog/products/schema/inbound-request.schema.ts(depends on T002) - T017 [US1] Implement
product-integration-auth.plugin.tsinsrc/plugins/: runs the 9-step validation order fromcontracts/inbound-request-contract.md, using T009/T010/T014 /T015/T016, populatingrequest.reqContext(productId,customerId,tenantId,actorType: CUSTOMER,actorId) only after every step passes (depends on T014, T015, T016) - T018 [US1] Write one
AuditLogrow per attempt (success or every failure reason) inside the plugin, per theAuditLogfield mapping indata-model.md— never including the raw token/credential (depends on T017) - T019 [US1] Register
product-integration-auth.plugin.tsinsrc/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
ProductIntegrationsServicemethods (register,rotate,revoke,updateStatus,getAuditTrail) insrc/modules/catalog/products/service/product-integrations.service.ts—register/rotategenerate a new secret, encrypt it (T008) before persisting, and return the plaintext secret in the response exactly once (depends on T014) - T023 [US2] Add
ProductIntegrationsControllerwith admin-authenticated handlers (register/rotate/revoke/updateStatus/getAuditTrail) insrc/modules/catalog/products/controller/product-integrations.controller.ts, gated by the existingfastify.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) insrc/modules/catalog/products/routes/product-integrations.routes.ts, registered fromsrc/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-limitregistration (integration-levelkeyGenerator) plus a second, stricter one (user-levelkeyGenerator) on the inbound route, reading limits fromrequest.reqContext-resolvedProductIntegrationfields, insrc/plugins/rate-limit.plugin.ts(keep the existing global registration untouched) — depends on T017 populatingreqContextbefore 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.mdNotes once all scenarios pass (docs/06-database-schema.mditself is the source spec and is intentionally not edited) - T030 [P] Add a "SaaS Integration" section to
README.mddescribing the inbound contract at a high level and linking tospecs/002-saas-integration/quickstart.md - T031 Run
npx tsx scripts/check-architecture.tsandnpm run lint/npm run typecheckto confirm the new module/plugin code respects existing module-boundary rules - T032 Full regression:
npm run test:unit(which currently runs the whole suite — seespecs/001-ci-pipeline/checklists/requirements.mdimplementation 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
reqContextpopulation (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)
- Setup + Foundational (T001-T010)
- User Story 1 (T011-T020)
- 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
- Setup + Foundational → schema migrated, primitives tested
- Add User Story 1 → inbound requests are authenticated (MVP)
- Add User Story 2 → integrations can be onboarded/rotated/revoked without touching the DB by hand
- Add User Story 3 → abuse-resistant
- Polish → docs and full regression