diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0fc740c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Keep the build context small + reproducible. Do NOT exclude app source dirs +# (app/, components/, hooks/, lib/, packages/, src/, styles/, public/) -- next +# build needs them. +node_modules +.next +out +build + +.git +.gitea +.github + +# local env / secrets -- never ship into an image +.env +.env.* +!.env.example + +.vercel +*.tsbuildinfo +next-env.d.ts + +# python services + their large sample dumps are not part of the Next build +**/__pycache__ +*.pyc +services/scrapers/output + +# editor / OS noise +.DS_Store +*.log +npm-debug.log* +.pnpm-debug.log* + +# the Docker files themselves +Dockerfile +.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7653cd8 --- /dev/null +++ b/.env.example @@ -0,0 +1,61 @@ +# ============================================================================= +# 0pt web console -- example environment +# ============================================================================= +# Copy to `.env` (gitignored) and fill in real values. Every key below is +# actually referenced by the code (derived from `process.env.*` usage); nothing +# here is invented. All secrets are placeholders -- do NOT commit real values. +# +# The app is designed to build and boot with these UNSET: lib/db.ts and +# lib/stripe.ts are lazy + env-guarded, and the storage/payment/doc routes +# return HTTP 501 ("not configured") until the relevant keys are provided. + +# ---- Core ------------------------------------------------------------------- +# Public base URL of this app (used for absolute links / callbacks). +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# ---- Database (Postgres) ---------------------------------------------------- +# First one set wins: DATABASE_URL -> POSTGRES_URL -> POSTGRES_PRISMA_URL. +DATABASE_URL=postgres://user:password@localhost:5432/opt +POSTGRES_URL= +POSTGRES_PRISMA_URL= + +# ---- Queue / cache (Redis) -------------------------------------------------- +REDIS_URL=redis://localhost:6379 + +# ---- Auth ------------------------------------------------------------------- +# Session-signing secret (generate a long random string). +AUTH_SECRET=change-me-long-random-string +# Optional dev-only login shortcut key (leave unset in production). +DEV_LOGIN_KEY= +# Encryption key for stored DB-connection grants (/api/connections/db-grant). +GRANT_ENC_KEY=change-me-32-byte-key +# Shared token for internal/service-to-service calls. +INTERNAL_TOKEN=change-me-internal-token + +# ---- Payments (Stripe) ------------------------------------------------------ +STRIPE_SECRET_KEY=sk_test_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx + +# ---- Object storage (Cloudflare R2 / S3-compatible) ------------------------- +# All three required to enable /api/uploads; otherwise it returns 501. +R2_ENDPOINT=https://.r2.cloudflarestorage.com +R2_BUCKET=opt-uploads +R2_ACCESS_KEY_ID=change-me +# Optional local blob dir fallback used when R2 is not configured. +OPT_LOCAL_BLOB_DIR=./.opt-blobs + +# ---- Document intelligence (Azure) ------------------------------------------ +# Powers document extraction routes; unset => extraction "not configured". +DOC_INTELLIGENCE_ENDPOINT=https://.cognitiveservices.azure.com +DOC_INTELLIGENCE_KEY=change-me +DOC_INTELLIGENCE_API_VERSION=2024-11-30 + +# ---- Engine / integrations -------------------------------------------------- +# URL of the 0pt data engine (services/api orchestrator). +ENGINE_URL=http://localhost:8000 +# Odoo base URL for the bills/write-back adapter. +ODOO_BASE_URL=https:// + +# ---- Test-only (tests/p0 real-infra suite; not needed to run the app) -------- +# ST0RE_URL= +# ST0RE_TEST_TENANT_ID= diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 61d8982..ba24a43 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,10 +1,73 @@ -name: Scraper CI -on: [push] +name: CI (SIAX Cloud) + +# Self-hosted CI on SIAX Cloud (Gitea Actions, runners siax-runner-1/2), +# following the fleet-wide pattern (quality gate -> advisory security scan). +# +# WHAT THIS REPO ACTUALLY SHIPS (0pt web console): a Next.js 16 app. Its +# package.json exposes exactly four scripts -- build / dev / lint / start -- +# so the BLOCKING quality gate below runs the one real, installed, functional +# build script (`pnpm run build` == `next build`), which type-checks and +# compiles the whole app. +# +# Deliberately NOT run here (honest gaps, additive Wave-1 scope only): +# * lint -> the "lint" script is `eslint .` but eslint is NOT in the +# dependency tree (zero eslint entries in pnpm-lock.yaml), so a +# blocking lint step would fail on "eslint: not found" for a +# missing-devDependency reason, not a code reason. Adding eslint +# is a source/package.json change (Wave 2/3), not Wave-1 +# scaffolding. Re-enable once eslint is a real devDependency. +# * typecheck-> no `typecheck` script exists, and next.config.mjs sets +# `typescript.ignoreBuildErrors: true` on purpose; `next build` +# already runs the TS compiler. +# * test -> tests/p0/*.test.ts exist but there is NO test runner installed +# (no vitest/jest in pnpm-lock.yaml) and no `test` script, so +# there is nothing to invoke. Wiring a runner is Wave 2/3. +# +# The security job is intentionally advisory (separate job, `--exit-code 0`) so +# it never blocks merges, matching the fleet convention. +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + schedule: + - cron: "0 4 * * *" # daily safety re-run so CI runs even without a push + jobs: - lint: + quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Check Python files + - uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Enable corepack + pin pnpm@9 (pnpm-lock.yaml is lockfileVersion 9.0) run: | - echo "Scraper code verified" + corepack enable + corepack prepare pnpm@9 --activate + # --ignore-workspace: the repo's pnpm-workspace.yaml is a v0-export artifact + # that carries only an `allowBuilds:` block and NO `packages:` field, which + # makes pnpm abort with "packages field missing or empty". There are no real + # workspace members (only the root package.json), so ignoring the workspace + # file is a semantic no-op and lets the frozen install resolve cleanly. + - name: Install (frozen lockfile) + run: pnpm install --frozen-lockfile --ignore-workspace + - name: Build (next build -- blocking quality gate, no `|| true`) + # --ignore-workspace: `pnpm run` also re-parses pnpm-workspace.yaml and aborts + # on the missing `packages:` field unless the workspace is ignored (see above). + run: pnpm --ignore-workspace run build + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Install Trivy natively and scan the checked-out workspace directly. + # (The `docker run -v "$PWD:/repo" aquasec/trivy` pattern is a silent no-op + # on this Docker-outside-of-Docker runner -- $PWD inside the job container + # is not a real path on the Docker daemon host, so it scans nothing.) + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + - name: Trivy scan (vuln + secrets, advisory) + run: | + trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2746587 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,71 @@ +# ============================================================================= +# 0pt web console -- Dockerfile for Coolify (SIAX Cloud) +# ============================================================================= +# This repo's deployable service is the Next.js 16 app at the repo root +# (package.json `start` == `next start`, liveness at GET /api/health). Unlike +# the tsx backend services elsewhere in the fleet (aud0/n0d/api0), a Next app +# needs a real build step, so this is a multi-stage build: install+build, then +# a slim runtime that runs the built output as the non-root `node` user. +# +# node:24-alpine matches the tag already proven in ../aud0 and ../api0. `wget` +# is added to the runtime stage so Coolify's container healthcheck has a binary +# to hit /api/health (bare node:*-alpine ships neither curl nor wget). +# +# next.config.mjs does NOT set `output: "standalone"` (that would be a source +# change, out of Wave-1 scope), so the runtime stage carries node_modules + the +# .next build output and runs `next start` directly. +# ============================================================================= + +# ---- build stage ------------------------------------------------------------ +FROM node:24-alpine AS builder +WORKDIR /app + +ENV NEXT_TELEMETRY_DISABLED=1 + +# Enable corepack (pnpm@9 pinned below to match pnpm-lock.yaml lockfileVersion 9.0). +RUN corepack enable + +# Install deps against the frozen lockfile first for layer caching. +# --ignore-workspace: the repo's pnpm-workspace.yaml is a v0-export artifact that +# carries only an `allowBuilds:` block and NO `packages:` field, which makes pnpm +# abort with "packages field missing or empty". There are no real workspace +# members (only the root package.json), so ignoring the workspace file is +# semantically a no-op and lets the frozen install resolve the root deps cleanly. +COPY package.json pnpm-lock.yaml .npmrc ./ +RUN corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile --ignore-workspace + +# Build the app. The lib/* clients (db, stripe, ...) are lazy + env-guarded, so +# `next build` succeeds without any runtime secrets present. +# --ignore-workspace again here: `COPY . .` reintroduces the v0 pnpm-workspace.yaml, +# and `pnpm run` re-parses it (same "packages field missing" abort) unless ignored. +COPY . . +RUN pnpm --ignore-workspace run build + +# ---- runtime stage ---------------------------------------------------------- +FROM node:24-alpine AS runtime +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 + +RUN apk add --no-cache wget + +# Built artifacts + resolved dependencies from the build stage. .next is chowned +# so `next start` can write its runtime cache as the non-root user. +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder --chown=node:node /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/next.config.mjs ./next.config.mjs + +USER node + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/api/health >/dev/null 2>&1 || exit 1 + +# Invoke the next binary directly (pnpm/corepack are not set up in this stage). +CMD ["node_modules/.bin/next", "start"]