89f559ce62
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>
12 KiB
12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-module-system-domaincheck | 01 | execute | 1 |
|
true |
|
|
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.
<execution_context> @/home/vicolab/.claude/gsd-core/workflows/execute-plan.md @/home/vicolab/.claude/gsd-core/templates/summary.md </execution_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.yamlPhase 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.
Task 1: Module SDK Package + Prisma Schema + Registry Service 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 apps/api/prisma/schema.prisma, packages/shared/src/index.ts, pnpm-workspace.yaml 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.
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')"
- 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
SDK package exists with typed interfaces, Prisma models for module registry are migrated and queryable
Task 2: ModuleRegistry NestJS Module with CRUD + Activation Endpoints
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
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
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.
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"
- 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
Admin can list, activate, and deactivate modules via API. ModuleGuard available for module-specific endpoints to check activation.
<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> |
<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 |