Files
support_backend/specs/002-saas-integration/tasks.md
T

253 lines
13 KiB
Markdown
Raw Normal View History

---
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