From 561c7bf42d1cb8d3d290f17e335c3c32ecd16e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sun, 12 Apr 2026 16:25:19 +0200 Subject: [PATCH] ci: fix port 5432 collision and include read-only-prisma helper - Remove host port mappings from postgres/redis services in ci.yml; QNAP runner already occupies 5432. Use service DNS names (postgres/redis) instead of localhost for DB/Redis URLs. - Track packages/api/src/lib/read-only-prisma.ts which was imported by assistant-tools.ts but never committed, breaking check:imports. --- .github/workflows/ci.yml | 16 ++----- packages/api/src/lib/read-only-prisma.ts | 57 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 packages/api/src/lib/read-only-prisma.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81ab5b6..318faa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,8 +161,6 @@ jobs: POSTGRES_DB: capakraken_test POSTGRES_USER: capakraken POSTGRES_PASSWORD: capakraken_test - ports: - - 5432:5432 options: >- --health-cmd="pg_isready -U capakraken -d capakraken_test" --health-interval=10s @@ -170,16 +168,14 @@ jobs: --health-retries=5 redis: image: redis:7 - ports: - - 6379:6379 options: >- --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=5 env: - DATABASE_URL: postgresql://capakraken:capakraken_test@localhost:5432/capakraken_test - REDIS_URL: redis://localhost:6379 + DATABASE_URL: postgresql://capakraken:capakraken_test@postgres:5432/capakraken_test + REDIS_URL: redis://redis:6379 NEXTAUTH_URL: ${{ env.CI_AUTH_URL }} AUTH_URL: ${{ env.CI_AUTH_URL }} NEXTAUTH_SECRET: ${{ env.CI_AUTH_SECRET }} @@ -286,8 +282,6 @@ jobs: POSTGRES_DB: capakraken_test POSTGRES_USER: capakraken POSTGRES_PASSWORD: capakraken_test - ports: - - 5432:5432 options: >- --health-cmd="pg_isready -U capakraken -d capakraken_test" --health-interval=10s @@ -295,18 +289,16 @@ jobs: --health-retries=5 redis: image: redis:7 - ports: - - 6379:6379 options: >- --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=5 env: - DATABASE_URL: postgresql://capakraken:capakraken_test@localhost:5432/capakraken_test + DATABASE_URL: postgresql://capakraken:capakraken_test@postgres:5432/capakraken_test ALLOW_DESTRUCTIVE_DB_TOOLS: "true" CONFIRM_DESTRUCTIVE_DB_NAME: capakraken_test - REDIS_URL: redis://localhost:6379 + REDIS_URL: redis://redis:6379 PORT: 3100 NEXTAUTH_URL: ${{ env.CI_AUTH_URL }} AUTH_URL: ${{ env.CI_AUTH_URL }} diff --git a/packages/api/src/lib/read-only-prisma.ts b/packages/api/src/lib/read-only-prisma.ts new file mode 100644 index 0000000..1be408b --- /dev/null +++ b/packages/api/src/lib/read-only-prisma.ts @@ -0,0 +1,57 @@ +/** + * Read-only Prisma proxy. + * + * Wraps a PrismaClient and blocks write operations at the application level. + * Used to enforce read-only access for AI read-tools (EGAI 4.1.1.2 / IAAI 3.6.22). + */ + +import type { prisma } from "@capakraken/db"; + +type PrismaClient = typeof prisma; + +const WRITE_METHODS = new Set([ + "create", + "createMany", + "createManyAndReturn", + "update", + "updateMany", + "upsert", + "delete", + "deleteMany", +]); + +function readOnlyModelProxy(model: Record, modelName: string): unknown { + return new Proxy(model, { + get(target, prop) { + if (typeof prop === "string" && WRITE_METHODS.has(prop)) { + return () => { + throw new Error( + `Write operation "${prop}" on "${modelName}" not permitted on read-only context`, + ); + }; + } + return Reflect.get(target, prop); + }, + }); +} + +export function createReadOnlyProxy(client: PrismaClient): PrismaClient { + return new Proxy(client, { + get(target, prop) { + const value = Reflect.get(target, prop); + // If accessing a model delegate (object with findMany, etc.), wrap it + if (value && typeof value === "object" && "findMany" in (value as Record)) { + return readOnlyModelProxy(value as Record, String(prop)); + } + // Block $executeRaw and $executeRawUnsafe at the client level + if (prop === "$executeRaw" || prop === "$executeRawUnsafe") { + return () => { + throw new Error( + `Raw write operation "${String(prop)}" not permitted on read-only context`, + ); + }; + } + return value; + }, + }) as PrismaClient; +}