diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1e0aac9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Dependency directories +node_modules/ + +# Build outputs +dist/ +coverage/ + +# Environment files and credentials +.env +.env.* +*.local +*.pem + +# Git files +.git/ +.github/ + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Docker files +Dockerfile +docker-compose.yml +.dockerignore + +# OS metadata +.DS_Store +Thumbs.db + +# Documentation and configs not needed in production container +README.md +nest-cli.json +tsconfig.json +tsconfig.build.json +eslint.config.mjs +.prettierrc +test/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6a4460..67f9344 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,100 @@ -name: CI +name: CI Pipeline on: - push: - branches: [main] pull_request: - branches: [main] + branches: + - main jobs: - build-lint-test: + ci: runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mergefi + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - - uses: actions/checkout@v4 + - name: Checkout Code + uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Setup Node.js v24 + uses: actions/setup-node@v4 with: - node-version: 20 - cache: npm - - - run: npm ci - - run: npm run build - - run: npm run lint - - run: npm test - - run: npm audit --audit-level=high + node-version: 24 + cache: 'npm' + + - name: Scan for Hardcoded Secrets + run: | + echo "Scanning codebase for potential hardcoded secrets..." + + # Detect Stellar Secret Keys (starts with S, 56 characters, uppercase A-Z, digits 2-7) + if grep -r -E "\bS[A-D][A-Z2-7]{54}\b" --exclude-dir={node_modules,dist,coverage,.git} .; then + echo "::error::Potential Stellar Secret Key detected in codebase!" + exit 1 + fi + + # Detect PEM Private Keys + if grep -r -E "(-+BEGIN [A-Z ]+ PRIVATE KEY-+)" --exclude-dir={node_modules,dist,coverage,.git} .; then + echo "::error::Potential PEM Private Key detected in codebase!" + exit 1 + fi + + # Detect GitHub Personal Access Tokens (Legacy and Fine-grained) + if grep -r -E "(ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{82})" --exclude-dir={node_modules,dist,coverage,.git} .; then + echo "::error::Potential GitHub Token detected in codebase!" + exit 1 + fi + + echo "Secret scan completed. No secrets detected." + shell: bash + + - name: Install Dependencies + run: npm ci + + - name: Lint Code + run: npm run lint + + - name: Build Application + run: npm run build + + - name: Run Unit & Integration Tests + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mergefi + NODE_ENV: test + run: npm run test + + - name: Run Coverage Tests + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mergefi + NODE_ENV: test + run: npm run test:cov + + - name: Run E2E Tests (Conditional) + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mergefi + NODE_ENV: test + # Map GitHub Secrets if configured in the repository + GITHUB_CLIENT_ID: ${{ secrets.GITHUB_CLIENT_ID }} + GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }} + run: | + if [ -z "$GITHUB_CLIENT_ID" ] || [ -z "$GITHUB_CLIENT_SECRET" ]; then + echo "==========================================================================" + echo "WARNING: Skipping E2E tests because external GitHub OAuth secrets are not" + echo "configured in the repository settings (GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET)." + echo "To enable E2E tests in CI, please set these secrets in GitHub." + echo "==========================================================================" + else + npm run test:e2e + fi + shell: bash diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f4da0a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# ========================================== +# Stage 1: Base image +# ========================================== +FROM node:24-alpine AS base + +# Install libc6-compat for compatibility with native modules on Alpine +RUN apk add --no-cache libc6-compat + +WORKDIR /usr/src/app + +# ========================================== +# Stage 2: Development dependencies and environment +# ========================================== +FROM base AS development + +# Copy configuration files for dependency installation +COPY package*.json ./ + +# Install ALL dependencies (including devDependencies) +RUN npm ci + +# Copy the rest of the application files +COPY . . + +# Set default port and run watch mode for development +EXPOSE 3000 +CMD ["npm", "run", "start:dev"] + +# ========================================== +# Stage 3: Build the application +# ========================================== +FROM base AS builder + +COPY package*.json ./ + +# Copy all node_modules from development stage to compile typescript +COPY --from=development /usr/src/app/node_modules ./node_modules +COPY . . + +# Build the production files (calls nest build) +RUN npm run build + +# ========================================== +# Stage 4: Production dependencies +# ========================================== +FROM base AS prod-deps + +COPY package*.json ./ + +# Clean installation of production-only dependencies +RUN npm ci --omit=dev + +# ========================================== +# Stage 5: Production runner +# ========================================== +FROM base AS runner + +# Configure environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Set ownership of the work directory to the non-root 'node' user +RUN chown -R node:node /usr/src/app + +# Use the non-root node user for security compliance +USER node + +# Copy package files and production node_modules +COPY --chown=node:node package*.json ./ +COPY --chown=node:node --from=prod-deps /usr/src/app/node_modules ./node_modules + +# Copy the built main application bundle +COPY --chown=node:node --from=builder /usr/src/app/dist ./dist + +# Expose the default NestJS port +EXPOSE 3000 + +# Run the production bundle +CMD ["node", "dist/main.js"] diff --git a/README.md b/README.md index 80e3b5e..bac44a0 100644 --- a/README.md +++ b/README.md @@ -184,39 +184,86 @@ Route groups: `/api/auth`, `/api/users`, `/api/github`, ## Setup, run, test +You can run this project either natively on your host machine or completely containerized using Docker. + +### 1. Local Development via Docker Compose (Recommended) + +This is the easiest way to get up and running with a pre-configured, isolated PostgreSQL database. + +```bash +# A. Configure Environment Variables +cp .env.example .env + +# B. Start App and Database services +docker compose up --build +``` + +- **Hot Reloading**: The codebase is mounted into the container using a bind mount. File changes on the host will automatically trigger application restarts. +- **Node Modules Isolation**: The container uses an anonymous volume for `/usr/src/app/node_modules`. This prevents Windows/Host compiled node packages from contaminating the Linux-native container. +- **Services**: + - The API is served at `http://localhost:3000/api` (Swagger docs at `http://localhost:3000/api/docs`). + - PostgreSQL is mapped to port `5432` on your localhost with credentials `postgres:postgres` and database name `mergefi`. + +### 2. Local Development Natively on Host + +If you prefer to run NestJS directly on your host machine: + ```bash -# 1. install +# A. Install Dependencies npm install -# 2. configure +# B. Spin up only the Database service in Docker +docker compose up -d db + +# C. Configure Environment Variables cp .env.example .env -# fill in DATABASE_URL at minimum to run against a real Postgres instance +# Set DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mergefi + +# D. Start NestJS in development mode +npm run start:dev +``` -# 3. run -npm run start:dev # watch mode -npm run start:prod # after `npm run build` +### 3. Production Builds & Security -# 4. build -npm run build +The Dockerfile is structured as a secure, multi-stage build running on Alpine Linux. -# 5. test -npm test # unit tests (Jest, src/**/*.spec.ts) -npm run test:cov # with coverage -npm run test:e2e # e2e (requires a running Postgres per DATABASE_URL) +#### Build the production image: +```bash +docker build --target runner -t mergefi-backend:latest . ``` -Unit tests cover, among other things: +#### Production Guardrails (Important): +- **Least Privilege**: The container runs under the non-root `node` user (`USER node`). +- **Production Mode**: The final image forces `NODE_ENV=production`. +- **JWT Secret Enforcer**: If the application boots in production with the default `JWT_SECRET=insecure-dev-secret` (or is missing entirely), the startup hook in `src/main.ts` will crash the container. You **must** provide a secure custom `JWT_SECRET` when running the production container: + ```bash + docker run -p 3000:3000 -e JWT_SECRET="your-highly-secure-random-jwt-key" mergefi-backend:latest + ``` + +### 4. Running Tests + +Unit tests and end-to-end tests are fully supported: + +```bash +# Run unit tests natively +npm test + +# Run unit tests with code coverage +npm run test:cov + +# Run End-to-End (E2E) tests (requires PostgreSQL to be running on DATABASE_URL) +npm run test:e2e +``` +Unit tests cover critical domains including: - `src/bounties/bounty-state-machine.spec.ts` — the bounty lifecycle state machine. - `src/teams/team-split.util.spec.ts` — team payout split percentage math. -- `src/escrow/escrow.service.spec.ts` — escrow fund/release/split-release/refund orchestration, with the Soroban client mocked. +- `src/escrow/escrow.service.spec.ts` — escrow fund/release/split-release/refund orchestration (Soroban client mocked). - `src/github/webhook-signature.util.spec.ts` — GitHub webhook HMAC-SHA256 signature verification. -- `src/github/github-webhooks.service.spec.ts` — end-to-end webhook → bounty release wiring. -- `src/bounties/bounties.service.spec.ts` — bounty service against mocked repositories/escrow. +- `src/github/github-webhooks.service.spec.ts` — webhook-to-escrow release logic. +- `src/bounties/bounties.service.spec.ts` — bounty core management. -`DATABASE_SYNCHRONIZE=true` will auto-create tables from entities for quick -local iteration; a real migration workflow (`typeorm migration:generate`) is -recommended before this touches any shared/staging database — see Roadmap. +`DATABASE_SYNCHRONIZE=true` (set in development) will auto-create tables from entities for fast local iteration. A real migration workflow (`typeorm migration:generate`) is recommended before deploying to production (see Roadmap). ## Roadmap diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1f9a6c2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +version: '3.8' + +services: + app: + build: + context: . + target: development + ports: + - "3000:3000" + volumes: + # Bind mount host files for hot-reloading (development watch mode) + - .:/usr/src/app + # Anonymous volume prevents host's node_modules from overriding container's + - /usr/src/app/node_modules + # Load all environment variables from local .env if it exists + env_file: + - .env + environment: + # Override DATABASE_URL to target the postgres container inside the docker network + - DATABASE_URL=postgresql://postgres:postgres@db:5432/mergefi + - PORT=3000 + - NODE_ENV=development + depends_on: + db: + condition: service_healthy + + db: + image: postgres:16-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=mergefi + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d mergefi"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: