Implemented transparent encryption/decryption of LLM provider API keys stored in llm_provider_instances.config JSON field using OpenBao Transit encryption. Implementation: - Created llm-encryption.middleware.ts with encryption/decryption logic - Auto-detects format (vault:v1: vs plaintext) for backward compatibility - Idempotent encryption prevents double-encryption - Registered middleware in PrismaService - Created data migration script for active encryption - Added migrate:encrypt-llm-keys command to package.json Tests: - 14 comprehensive unit tests - 90.76% code coverage (exceeds 85% requirement) - Tests create, read, update, upsert operations - Tests error handling and backward compatibility Migration: - Lazy migration: New keys encrypted, old keys work until re-saved - Active migration: pnpm --filter @mosaic/api migrate:encrypt-llm-keys - No schema changes required - Zero downtime Security: - Uses TransitKey.LLM_CONFIG from OpenBao Transit - Keys never touch disk in plaintext (in-memory only) - Transparent to LlmManagerService and providers - Follows proven pattern from account-encryption.middleware.ts Files: - apps/api/src/prisma/llm-encryption.middleware.ts (new) - apps/api/src/prisma/llm-encryption.middleware.spec.ts (new) - apps/api/scripts/encrypt-llm-keys.ts (new) - apps/api/prisma/migrations/20260207_encrypt_llm_api_keys/ (new) - apps/api/src/prisma/prisma.service.ts (modified) - apps/api/package.json (modified) Note: The migration script (encrypt-llm-keys.ts) is not included in tsconfig.json to avoid rootDir conflicts. It's executed via tsx which handles TypeScript directly. Refs #359 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
3.2 KiB
JSON
101 lines
3.2 KiB
JSON
{
|
|
"name": "@mosaic/api",
|
|
"version": "0.0.1",
|
|
"private": true,
|
|
"scripts": {
|
|
"build": "nest build",
|
|
"dev": "nest start --watch",
|
|
"start": "node dist/main",
|
|
"start:debug": "nest start --debug --watch",
|
|
"start:prod": "node dist/main",
|
|
"lint": "eslint \"src/**/*.ts\"",
|
|
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
"typecheck": "tsc --noEmit",
|
|
"clean": "rm -rf dist",
|
|
"test": "vitest run",
|
|
"test:watch": "vitest",
|
|
"test:coverage": "vitest run --coverage",
|
|
"test:e2e": "vitest run --config ./vitest.e2e.config.ts",
|
|
"prisma:generate": "prisma generate",
|
|
"prisma:migrate": "prisma migrate dev",
|
|
"prisma:migrate:prod": "prisma migrate deploy",
|
|
"prisma:studio": "prisma studio",
|
|
"prisma:seed": "prisma db seed",
|
|
"prisma:reset": "prisma migrate reset",
|
|
"migrate:encrypt-llm-keys": "tsx scripts/encrypt-llm-keys.ts"
|
|
},
|
|
"dependencies": {
|
|
"@anthropic-ai/sdk": "^0.72.1",
|
|
"@mosaic/shared": "workspace:*",
|
|
"@nestjs/axios": "^4.0.1",
|
|
"@nestjs/bullmq": "^11.0.4",
|
|
"@nestjs/common": "^11.1.12",
|
|
"@nestjs/config": "^4.0.2",
|
|
"@nestjs/core": "^11.1.12",
|
|
"@nestjs/mapped-types": "^2.1.0",
|
|
"@nestjs/platform-express": "^11.1.12",
|
|
"@nestjs/platform-socket.io": "^11.1.12",
|
|
"@nestjs/throttler": "^6.5.0",
|
|
"@nestjs/websockets": "^11.1.12",
|
|
"@opentelemetry/api": "^1.9.0",
|
|
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
|
|
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
|
|
"@opentelemetry/instrumentation-nestjs-core": "^0.44.0",
|
|
"@opentelemetry/resources": "^1.30.1",
|
|
"@opentelemetry/sdk-node": "^0.56.0",
|
|
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
|
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
"@prisma/client": "^6.19.2",
|
|
"@types/marked": "^6.0.0",
|
|
"@types/multer": "^2.0.0",
|
|
"adm-zip": "^0.5.16",
|
|
"archiver": "^7.0.1",
|
|
"axios": "^1.13.4",
|
|
"better-auth": "^1.4.17",
|
|
"bullmq": "^5.67.2",
|
|
"class-transformer": "^0.5.1",
|
|
"class-validator": "^0.14.3",
|
|
"cookie-parser": "^1.4.7",
|
|
"discord.js": "^14.25.1",
|
|
"gray-matter": "^4.0.3",
|
|
"highlight.js": "^11.11.1",
|
|
"ioredis": "^5.9.2",
|
|
"jose": "^6.1.3",
|
|
"marked": "^17.0.1",
|
|
"marked-gfm-heading-id": "^4.1.3",
|
|
"marked-highlight": "^2.2.3",
|
|
"ollama": "^0.6.3",
|
|
"openai": "^6.17.0",
|
|
"reflect-metadata": "^0.2.2",
|
|
"rxjs": "^7.8.1",
|
|
"sanitize-html": "^2.17.0",
|
|
"slugify": "^1.6.6",
|
|
"socket.io": "^4.8.3"
|
|
},
|
|
"devDependencies": {
|
|
"@better-auth/cli": "^1.4.17",
|
|
"@mosaic/config": "workspace:*",
|
|
"@nestjs/cli": "^11.0.6",
|
|
"@nestjs/schematics": "^11.0.1",
|
|
"@nestjs/testing": "^11.1.12",
|
|
"@opentelemetry/context-async-hooks": "^2.5.0",
|
|
"@swc/core": "^1.10.18",
|
|
"@types/adm-zip": "^0.5.7",
|
|
"@types/archiver": "^7.0.0",
|
|
"@types/cookie-parser": "^1.4.10",
|
|
"@types/express": "^5.0.1",
|
|
"@types/highlight.js": "^10.1.0",
|
|
"@types/node": "^22.13.4",
|
|
"@types/sanitize-html": "^2.16.0",
|
|
"@types/supertest": "^6.0.3",
|
|
"@vitest/coverage-v8": "^4.0.18",
|
|
"express": "^5.2.1",
|
|
"prisma": "^6.19.2",
|
|
"supertest": "^7.2.2",
|
|
"tsx": "^4.21.0",
|
|
"typescript": "^5.8.2",
|
|
"unplugin-swc": "^1.5.2",
|
|
"vitest": "^4.0.18"
|
|
}
|
|
}
|