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>
253 lines
13 KiB
Markdown
253 lines
13 KiB
Markdown
---
|
|
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](./plan.md), [spec.md](./spec.md), [research.md](./research.md),
|
|
[data-model.md](./data-model.md), [contracts/inbound-request-contract.md](./contracts/inbound-request-contract.md),
|
|
[quickstart.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
|
|
|
|
- [X] 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`
|
|
- [X] 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.
|
|
|
|
- [X] 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")
|
|
- [X] T004 Add `ProductIntegration` model to `prisma/schema.prisma` per `data-model.md`
|
|
(depends on T003)
|
|
- [X] T005 [P] Add `CustomerReference` model to `prisma/schema.prisma` per `data-model.md`
|
|
(depends on T003)
|
|
- [X] 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)
|
|
- [X] T007 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`) for
|
|
T003-T005 (depends on T004, T005, T006)
|
|
- [X] 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
|
|
- [X] 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)
|
|
- [X] 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
|
|
|
|
- [X] T011 [P] [US1] Unit tests for `credential.crypto.ts` (encrypt/decrypt round-trip, wrong key
|
|
fails) in `tests/unit/products/credential-crypto.test.ts`
|
|
- [X] 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`
|
|
- [X] 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
|
|
|
|
- [X] 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)
|
|
- [X] T015 [US1] Add `CustomerReferencesRepository` (find-or-create by
|
|
`externalUserId`+`externalTenantId`) in
|
|
`src/modules/catalog/products/repository/customer-references.repository.ts` (depends on
|
|
T007)
|
|
- [X] 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)
|
|
- [X] 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)
|
|
- [X] 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)
|
|
- [X] 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)
|
|
- [X] 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
|
|
|
|
- [X] 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
|
|
|
|
- [X] T022 [US2] Add `ProductIntegrationsService` methods (`register`, `rotate`, `revoke`,
|
|
`updateStatus`, `getAuditTrail`) in
|
|
`src/modules/catalog/products/service/product-integrations.service.ts` — `register`/
|
|
`rotate` generate a new secret, encrypt it (T008) before persisting, and return the
|
|
plaintext secret in the response exactly once (depends on T014)
|
|
- [X] 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)
|
|
- [X] 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)
|
|
- [X] 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
|
|
|
|
- [X] 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
|
|
- [X] 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`
|
|
- [X] 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
|
|
|
|
- [X] 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)
|
|
- [X] 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`
|
|
- [X] 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
|
|
- [X] 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
|