--- 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" --- 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. @/home/vicolab/.claude/gsd-core/workflows/execute-plan.md @/home/vicolab/.claude/gsd-core/templates/summary.md @.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 ## 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. 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. ## 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 | - 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 - 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 ## 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 | Create `.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md` when done