diff --git a/.dockerignore b/.dockerignore index 55f2490..2aedf07 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,7 @@ -# Dependencies +# Dependencies — never bake into image node_modules/ -# Compiled output (built inside Docker) +# Compiled output — built inside Docker dist/ # Test artifacts @@ -10,7 +10,18 @@ tests/ # Environment and secrets — never bake into image .env +.env.* *.pem +*.key +*.cert + +# Docker files — not needed inside the image +compose.yaml +compose.*.yaml +docker-compose.yml +docker-compose*.yml +Dockerfile* +.dockerignore # Development workspace .cto-workspace/ @@ -21,11 +32,23 @@ next_steps.md # Git .git/ .gitignore +.gitattributes # Editor .vscode/ .idea/ +*.swp +*.swo + +# OS artifacts +.DS_Store +Thumbs.db # Logs *.log npm-debug.log* +logs/ + +# Temporary directories +tmp/ +temp/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7889d8d --- /dev/null +++ b/.env.example @@ -0,0 +1,79 @@ +# SentryAgent.ai AgentIdP — Environment Variables +# Copy this file to .env and fill in the values for your environment. + +# ── Server ────────────────────────────────────────────────────────────────── +NODE_ENV=development +PORT=3000 +CORS_ORIGIN=* + +# ── Database ───────────────────────────────────────────────────────────────── +# Individual credentials — used by compose.yaml to construct DATABASE_URL +POSTGRES_USER=sentryagent +POSTGRES_PASSWORD=change-me-in-production +POSTGRES_DB=sentryagent_idp + +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB} + +# PostgreSQL connection pool tuning (task 2.1) +DB_POOL_MAX=20 +DB_POOL_MIN=2 +DB_POOL_IDLE_TIMEOUT_MS=30000 +DB_POOL_CONNECTION_TIMEOUT_MS=5000 + +# ── Redis ──────────────────────────────────────────────────────────────────── +REDIS_URL=redis://localhost:6379 + +# Rate limiting (task 1.2 / 1.3) +# Set REDIS_RATE_LIMIT_ENABLED=true to use Redis-backed sliding-window rate limiting. +# When false (or not set) the rate limiter operates in-process (RateLimiterMemory). +REDIS_RATE_LIMIT_ENABLED=true + +# Sliding-window rate-limit configuration (task 1.3) +RATE_LIMIT_WINDOW_MS=60000 +RATE_LIMIT_MAX_REQUESTS=100 + +# ── JWT ────────────────────────────────────────────────────────────────────── +# RS256 key pair — generate with: +# openssl genrsa -out private.pem 2048 +# openssl rsa -in private.pem -pubout -out public.pem +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" + +# ── HashiCorp Vault (optional) ──────────────────────────────────────────────── +# When set, new agent credentials are stored in Vault KV v2 instead of bcrypt. +# VAULT_ADDR=http://127.0.0.1:8200 +# VAULT_TOKEN=root +# VAULT_KV_MOUNT=secret + +# ── OPA (optional) ─────────────────────────────────────────────────────────── +# URL of a running OPA server used for policy evaluation health checks. +# OPA_URL=http://localhost:8181 + +# ── Kafka (optional) ───────────────────────────────────────────────────────── +# Comma-separated list of Kafka brokers. Leave unset to disable Kafka. +# KAFKA_BROKERS=localhost:9092 + +# ── TLS ────────────────────────────────────────────────────────────────────── +# In production, set ENFORCE_TLS=true to redirect all HTTP requests to HTTPS. +# ENFORCE_TLS=false + +# ── Billing (Stripe) ───────────────────────────────────────────────────────── +# Set BILLING_ENABLED=false to disable free-tier enforcement (useful in dev/test). +BILLING_ENABLED=false +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +STRIPE_PRICE_ID=price_... + +# ── Monitoring (Grafana) ───────────────────────────────────────────────────── +# Used by compose.monitoring.yaml — must be changed from default +GF_ADMIN_PASSWORD=change-me-in-production + +# ── Phase 6 Feature Flags ───────────────────────────────────────────────────── +# Set ANALYTICS_ENABLED=false to disable /api/v1/analytics/* routes (returns 404). +ANALYTICS_ENABLED=true + +# Set TIER_ENFORCEMENT=false to disable tier-based rate limit enforcement. +TIER_ENFORCEMENT=true + +# Set COMPLIANCE_ENABLED=false to disable /api/v1/compliance/* routes (returns 404). +COMPLIANCE_ENABLED=true diff --git a/.gitignore b/.gitignore index 988f23f..0023ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ coverage/ .env .env.* +!.env.example *.log .DS_Store diff --git a/Dockerfile b/Dockerfile index 6a9636a..153a7b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # ───────────────────────────────────────────────────────────── -# Stage 1: builder — compile TypeScript to dist/ +# Stage 1: build — compile TypeScript to dist/ # ───────────────────────────────────────────────────────────── -FROM node:18-alpine AS builder +FROM node:20.11-bookworm-slim AS build WORKDIR /app @@ -16,25 +16,32 @@ COPY scripts/ ./scripts/ RUN npm run build # ───────────────────────────────────────────────────────────── -# Stage 2: production — minimal runtime image +# Stage 2: final — minimal, non-root runtime image # ───────────────────────────────────────────────────────────── -FROM node:18-alpine AS production +FROM node:20.11-bookworm-slim AS final WORKDIR /app +# Install curl for healthcheck probe — then clean up apt cache in same layer +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl && \ + rm -rf /var/lib/apt/lists/* + +# Create dedicated non-root system user/group — containers must never run as root +RUN groupadd --system --gid 1001 nodejs && \ + useradd --system --uid 1001 --gid nodejs nodeapp + # Copy package files and install production dependencies only COPY package.json package-lock.json ./ RUN npm ci --omit=dev -# Copy compiled output from builder stage -COPY --from=builder /app/dist ./dist +# Copy compiled artifacts and runtime-required files from build stage only +COPY --from=build /app/dist ./dist +COPY --from=build /app/scripts ./scripts +COPY --from=build /app/src/db/migrations ./src/db/migrations -# Copy migration scripts (needed for db:migrate at deploy time) -COPY --from=builder /app/scripts ./scripts -COPY src/db/migrations ./src/db/migrations - -# Run as non-root user (built into node:alpine) -USER node +# Drop root — all subsequent instructions and the running container use nodeapp +USER nodeapp EXPOSE 3000 diff --git a/compose.monitoring.yaml b/compose.monitoring.yaml new file mode 100644 index 0000000..06dcca1 --- /dev/null +++ b/compose.monitoring.yaml @@ -0,0 +1,69 @@ +# SentryAgent.ai AgentIdP — Monitoring Overlay +# Compose Specification (no version header — deprecated per modern Compose Spec) +# Usage: docker compose -f compose.yaml -f compose.monitoring.yaml up + +services: + prometheus: + image: prom/prometheus:v2.53.0 + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.enable-lifecycle' + ports: + - '9090:9090' + networks: + - app-tier + restart: unless-stopped + deploy: + resources: + limits: + memory: 256m + cpus: '0.5' + healthcheck: + test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:9090/-/healthy'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + grafana: + image: grafana/grafana:11.2.0 + volumes: + - grafana-data:/var/lib/grafana + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro + environment: + GF_SECURITY_ADMIN_PASSWORD: ${GF_ADMIN_PASSWORD} + GF_USERS_ALLOW_SIGN_UP: 'false' + GF_AUTH_ANONYMOUS_ENABLED: 'false' + ports: + - '3001:3000' + networks: + - app-tier + depends_on: + - prometheus + restart: unless-stopped + deploy: + resources: + limits: + memory: 256m + cpus: '0.5' + healthcheck: + test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000/api/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +volumes: + prometheus-data: + grafana-data: + +networks: + app-tier: + external: true diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..eed4066 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,95 @@ +# SentryAgent.ai AgentIdP — Docker Compose +# Compose Specification (no version header — deprecated per modern Compose Spec) +# Usage: docker compose up --build + +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - '3000:3000' + environment: + NODE_ENV: ${NODE_ENV:-development} + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + REDIS_URL: redis://redis:6379 + PORT: '3000' + env_file: + - path: .env + required: false + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - app-tier + restart: unless-stopped + deploy: + resources: + limits: + memory: 512m + cpus: '1.0' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + # Bind mount for local development source-sync only + volumes: + - ./src:/app/src:ro + + postgres: + image: postgres:14.12-alpine3.19 + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - '5432:5432' + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - app-tier + restart: unless-stopped + deploy: + resources: + limits: + memory: 256m + cpus: '0.5' + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $POSTGRES_USER -d $POSTGRES_DB'] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s + + redis: + image: redis:7.2-alpine3.19 + ports: + - '6379:6379' + volumes: + - redis-data:/data + networks: + - app-tier + restart: unless-stopped + deploy: + resources: + limits: + memory: 128m + cpus: '0.5' + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +networks: + app-tier: + driver: bridge + +volumes: + postgres-data: + redis-data: diff --git a/docker-compose.monitoring.yml b/docker-compose.monitoring.yml deleted file mode 100644 index 96bc560..0000000 --- a/docker-compose.monitoring.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: '3.8' - -# Monitoring overlay — extend the base docker-compose.yml -# Usage: docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up - -services: - prometheus: - image: prom/prometheus:v2.53.0 - container_name: agentidp_prometheus - volumes: - - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - - prometheus_data:/prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - '--web.console.libraries=/etc/prometheus/console_libraries' - - '--web.console.templates=/etc/prometheus/consoles' - - '--web.enable-lifecycle' - ports: - - '9090:9090' - networks: - - agentidp_network - restart: unless-stopped - - grafana: - image: grafana/grafana:11.2.0 - container_name: agentidp_grafana - volumes: - - grafana_data:/var/lib/grafana - - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro - - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro - environment: - - GF_SECURITY_ADMIN_PASSWORD=agentidp - - GF_USERS_ALLOW_SIGN_UP=false - - GF_AUTH_ANONYMOUS_ENABLED=false - ports: - - '3001:3000' - networks: - - agentidp_network - depends_on: - - prometheus - restart: unless-stopped - -volumes: - prometheus_data: - grafana_data: - -networks: - agentidp_network: - external: true diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 9213d09..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,54 +0,0 @@ -version: '3.9' - -services: - app: - build: - context: . - dockerfile: Dockerfile - ports: - - '3000:3000' - environment: - - DATABASE_URL=postgresql://sentryagent:sentryagent@postgres:5432/sentryagent_idp - - REDIS_URL=redis://redis:6379 - - PORT=3000 - env_file: - - .env - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - volumes: - - ./src:/app/src:ro - - postgres: - image: postgres:14-alpine - environment: - POSTGRES_USER: sentryagent - POSTGRES_PASSWORD: sentryagent - POSTGRES_DB: sentryagent_idp - ports: - - '5432:5432' - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U sentryagent -d sentryagent_idp'] - interval: 5s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - ports: - - '6379:6379' - volumes: - - redis_data:/data - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - redis_data: