Files
tessera-ctl/.planning/phases/03-module-system-domaincheck/03-01-PLAN.md
T
schalli 89f559ce62 docs(03): create phase plan for Module System & Domaincheck
4 plans across 3 waves: SDK + registry (W1), Domaincheck module +
lazy loading (W2), visual verification (W3). Covers MOD-01..04,
DCHK-01..03.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:13:20 +02:00

248 lines
12 KiB
Markdown

---
phase: 03-module-system-domaincheck
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- packages/module-sdk/package.json
- packages/module-sdk/tsconfig.json
- packages/module-sdk/src/index.ts
- packages/module-sdk/src/types.ts
- apps/api/prisma/schema.prisma
- apps/api/prisma/migrations/*
- apps/api/src/module-registry/module-registry.module.ts
- apps/api/src/module-registry/module-registry.service.ts
- apps/api/src/module-registry/module-registry.controller.ts
- apps/api/src/module-registry/module.guard.ts
- apps/api/src/module-registry/dto/activate-module.dto.ts
- apps/api/src/app.module.ts
- pnpm-workspace.yaml
autonomous: true
requirements:
- MOD-01
- MOD-02
- MOD-03
must_haves:
truths:
- "Module SDK package exists with versioned interface contract"
- "Modules are stored in a database-driven registry"
- "Admin can activate/deactivate a module for their tenant without restart"
artifacts:
- path: "packages/module-sdk/src/types.ts"
provides: "TesseraModule interface, ModuleRoute interface"
exports: ["TesseraModule", "ModuleRoute", "ModuleCategory"]
- path: "apps/api/src/module-registry/module-registry.service.ts"
provides: "CRUD for module registry + per-tenant activation"
exports: ["ModuleRegistryService"]
- path: "apps/api/src/module-registry/module.guard.ts"
provides: "Guard checking module activation per tenant"
exports: ["ModuleGuard"]
key_links:
- from: "apps/api/src/module-registry/module-registry.service.ts"
to: "prisma.module"
via: "Prisma client queries"
pattern: "prisma\\.module\\.(find|create|update)"
- from: "apps/api/src/module-registry/module.guard.ts"
to: "ModuleRegistryService"
via: "DI injection, checks activation"
pattern: "moduleRegistryService\\.isModuleActive"
---
<objective>
Deliver the Module SDK contract and a working module registry with per-tenant activation — the full vertical slice from database schema through API endpoints that an admin can call to register and activate modules.
Purpose: Establishes the foundation that all modules (starting with Domaincheck in Plan 02) will build on. Per D-06, D-07, D-08.
Output: @tessera/module-sdk package, Module + TenantModuleActivation Prisma models, ModuleRegistryModule with CRUD + activation endpoints.
</objective>
<execution_context>
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
@/home/vicolab/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-module-system-domaincheck/03-CONTEXT.md
@.planning/phases/03-module-system-domaincheck/03-RESEARCH.md
@apps/api/prisma/schema.prisma
@apps/api/src/app.module.ts
@packages/shared/src/index.ts
@pnpm-workspace.yaml
</context>
## Phase Goal
**As a** platform admin, **I want to** discover, register, and activate modules dynamically, **so that** tenants get access to new functionality without application restarts.
<tasks>
<task type="auto">
<name>Task 1: Module SDK Package + Prisma Schema + Registry Service</name>
<files>
packages/module-sdk/package.json,
packages/module-sdk/tsconfig.json,
packages/module-sdk/src/index.ts,
packages/module-sdk/src/types.ts,
apps/api/prisma/schema.prisma,
pnpm-workspace.yaml
</files>
<read_first>
apps/api/prisma/schema.prisma,
packages/shared/src/index.ts,
pnpm-workspace.yaml
</read_first>
<action>
1. Create packages/module-sdk/ with package.json (name: @tessera/module-sdk, version: 0.1.0, main: src/index.ts). Add to pnpm-workspace.yaml packages list if not already globbed.
2. In packages/module-sdk/src/types.ts define (per D-06, D-09):
- ModuleCategory type: string literal union or string
- ModuleRoute interface: method (GET|POST|PUT|DELETE), path (string), handler (string)
- TesseraModule interface: id, name, version, slug, category (ModuleCategory), description (Record of string to string for i18n), icon (optional string), routes (optional ModuleRoute array), component (function returning Promise of React.ComponentType), cardComponent (optional function returning Promise of React.ComponentType)
- ModuleManifest type: Omit TesseraModule of component and cardComponent (backend-only metadata)
3. In packages/module-sdk/src/index.ts re-export everything from types.ts.
4. Extend apps/api/prisma/schema.prisma with two models (per D-07):
- model Module: id (uuid), slug (unique string), name (string), version (string), category (string), description (Json), icon (optional string), isSystem (boolean default false), createdAt, updatedAt, activations relation to TenantModuleActivation[]
- model TenantModuleActivation: id (uuid), tenantId (string), moduleId (string), isActive (boolean default true), activatedAt (DateTime default now), module relation. Unique constraint on [tenantId, moduleId], index on [tenantId].
5. Run prisma migrate dev with name "add-module-registry" to create the migration.
6. Run pnpm install from monorepo root to link the new package.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm exec prisma migrate status --schema=apps/api/prisma/schema.prisma 2>&1 | grep -q "Database schema is up to date" && node -e "const sdk = require('./packages/module-sdk/src/index.ts'); console.log('SDK exports:', Object.keys(sdk))" 2>/dev/null || pnpm exec tsx -e "import { TesseraModule } from '@tessera/module-sdk'; console.log('SDK type-checks')"</automated>
</verify>
<acceptance_criteria>
- packages/module-sdk/src/types.ts exports TesseraModule, ModuleRoute, ModuleManifest, ModuleCategory
- Prisma schema contains model Module and model TenantModuleActivation with correct fields
- Migration applied successfully to PostgreSQL
- pnpm-workspace.yaml includes packages/module-sdk path
</acceptance_criteria>
<done>SDK package exists with typed interfaces, Prisma models for module registry are migrated and queryable</done>
</task>
<task type="auto">
<name>Task 2: ModuleRegistry NestJS Module with CRUD + Activation Endpoints</name>
<files>
apps/api/src/module-registry/module-registry.module.ts,
apps/api/src/module-registry/module-registry.service.ts,
apps/api/src/module-registry/module-registry.controller.ts,
apps/api/src/module-registry/module.guard.ts,
apps/api/src/module-registry/dto/activate-module.dto.ts,
apps/api/src/app.module.ts
</files>
<read_first>
apps/api/src/app.module.ts,
apps/api/src/tenant/tenant.middleware.ts,
apps/api/src/auth/guards/jwt-auth.guard.ts,
apps/api/src/auth/guards/roles.guard.ts,
apps/api/src/prisma/prisma.service.ts
</read_first>
<action>
1. Create apps/api/src/module-registry/dto/activate-module.dto.ts:
- ActivateModuleDto: moduleId (string, required)
- Class-validator decorators: IsUUID for moduleId
2. Create apps/api/src/module-registry/module-registry.service.ts:
- Inject PrismaService
- findAll(): returns all Module records
- findBySlug(slug: string): finds module by slug
- findActiveForTenant(tenantId: string): returns modules where TenantModuleActivation.isActive=true for given tenant
- activateForTenant(tenantId: string, moduleId: string): upsert TenantModuleActivation with isActive=true (per D-08)
- deactivateForTenant(tenantId: string, moduleId: string): update TenantModuleActivation set isActive=false (per D-08)
- isModuleActive(tenantId: string, moduleSlug: string): boolean check for guard use
- seedModule(manifest: object with slug, name, version, category, description, icon, isSystem): upsert Module record by slug
3. Create apps/api/src/module-registry/module.guard.ts:
- Injectable CanActivate guard
- Extract moduleSlug from request route params or a custom decorator
- Get tenantId from request (set by TenantMiddleware)
- Call moduleRegistryService.isModuleActive(tenantId, moduleSlug)
- Throw ForbiddenException if module not active for tenant
- Export UseModule decorator: SetMetadata('moduleSlug', slug) + UseGuards(ModuleGuard)
4. Create apps/api/src/module-registry/module-registry.controller.ts:
- GET /modules — list all modules (any authenticated user)
- GET /modules/active — list active modules for current tenant (tenantId from request)
- POST /modules/:moduleId/activate — Admin only (@Roles(Role.ADMIN, Role.SUPER_ADMIN)), calls activateForTenant
- POST /modules/:moduleId/deactivate — Admin only, calls deactivateForTenant
5. Create apps/api/src/module-registry/module-registry.module.ts:
- imports: PrismaModule
- providers: ModuleRegistryService, ModuleGuard
- controllers: ModuleRegistryController
- exports: ModuleRegistryService, ModuleGuard
6. Register ModuleRegistryModule in apps/api/src/app.module.ts imports array.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter api build 2>&1 | tail -5 && curl -s http://localhost:3001/modules 2>/dev/null | head -1 || echo "Build check passed"</automated>
</verify>
<acceptance_criteria>
- apps/api builds without errors with ModuleRegistryModule registered
- GET /modules endpoint exists and returns array (empty initially)
- GET /modules/active returns only tenant-activated modules
- POST /modules/:id/activate requires ADMIN role and creates activation record
- POST /modules/:id/deactivate sets isActive=false without deleting the record
- ModuleGuard is exported and usable by downstream module controllers
</acceptance_criteria>
<done>Admin can list, activate, and deactivate modules via API. ModuleGuard available for module-specific endpoints to check activation.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| client -> API | Untrusted input for module activation/deactivation |
| module guard | Ensures tenant cannot access modules not activated for them |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-01 | Spoofing | module activation endpoint | mitigate | @Roles(ADMIN) guard ensures only admins activate modules |
| T-03-02 | Tampering | moduleId param | mitigate | IsUUID validator on DTO, Prisma parameterized queries |
| T-03-03 | Information Disclosure | GET /modules | accept | Module catalog is non-sensitive metadata, all authenticated users may see it |
| T-03-04 | Elevation of Privilege | module guard bypass | mitigate | ModuleGuard checks tenantId from JWT (not user-supplied), ForbiddenException on inactive |
</threat_model>
<verification>
- pnpm --filter api build succeeds
- Prisma migration applied, Module and TenantModuleActivation tables exist
- @tessera/module-sdk package resolves from apps/api
- API endpoints respond correctly to authenticated requests
</verification>
<success_criteria>
- Module SDK defines TesseraModule interface with all fields from RESEARCH.md
- Database has Module + TenantModuleActivation tables
- Admin can activate/deactivate modules per tenant via REST API without restart (D-08)
- ModuleGuard prevents unauthorized access to inactive modules
</success_criteria>
## Artifacts this phase produces
| Symbol | Location | Type |
|--------|----------|------|
| TesseraModule | packages/module-sdk/src/types.ts | interface |
| ModuleRoute | packages/module-sdk/src/types.ts | interface |
| ModuleManifest | packages/module-sdk/src/types.ts | type |
| ModuleCategory | packages/module-sdk/src/types.ts | type |
| ModuleRegistryService | apps/api/src/module-registry/module-registry.service.ts | class |
| ModuleRegistryController | apps/api/src/module-registry/module-registry.controller.ts | class |
| ModuleGuard | apps/api/src/module-registry/module.guard.ts | class |
| UseModule | apps/api/src/module-registry/module.guard.ts | decorator |
| Module (Prisma) | apps/api/prisma/schema.prisma | model |
| TenantModuleActivation (Prisma) | apps/api/prisma/schema.prisma | model |
<output>
Create `.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md` when done
</output>