diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5a2d430..636dccf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,12 +1,41 @@ -# Default owners for everything in the repository +# CODEOWNERS — https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +# +# Each line is a file pattern followed by one or more owners. +# The last matching rule takes precedence. +# +# @SynapsVault/core must approve every PR. The more specific rules below +# ensure the right sub-team is also notified for their area. + +# Default: all files require core team approval * @SynapsVault/core -# Kubernetes manifests, Dockerfiles, and CI/CD workflows -/k8s/ @SynapsVault/core -/backend/ @SynapsVault/core -/frontend/ @SynapsVault/core -/.github/ @SynapsVault/core +# Kubernetes manifests and Kustomize overlays — infra team +/k8s/ @SynapsVault/core @SynapsVault/infra + +# Dockerfiles — infra team +/backend/Dockerfile @SynapsVault/core @SynapsVault/infra +/frontend/Dockerfile @SynapsVault/core @SynapsVault/infra + +# CI / CD workflows and Actions config — infra team +/.github/workflows/ @SynapsVault/core @SynapsVault/infra +/.github/dependabot.yml @SynapsVault/core @SynapsVault/infra +/.github/app-versions.yml @SynapsVault/core @SynapsVault/infra + +# Issue / PR templates — any core member +/.github/ISSUE_TEMPLATE/ @SynapsVault/core +/.github/PULL_REQUEST_TEMPLATE.md @SynapsVault/core -# Operational scripts and backup/restore runbooks +# Deployment and operational scripts /scripts/ @SynapsVault/core + +# Monitoring configuration (alerts, dashboards, recording rules) +/monitoring/ @SynapsVault/core + +# Runbooks and guides /docs/ @SynapsVault/core + +# Security-sensitive files — always require a security-aware core review +/SECURITY.md @SynapsVault/core +/k8s/secrets.yaml @SynapsVault/core @SynapsVault/infra +/.github/workflows/deploy-k8s.yml @SynapsVault/core @SynapsVault/infra +/.github/workflows/backup.yml @SynapsVault/core @SynapsVault/infra diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 913dcdf..edf69dd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,12 +8,27 @@ assignees: "" ## Affected Component -- [ ] `scripts/` (contract deploy/rollback, seed, smoke test, backup/restore/db-rollback) -- [ ] `k8s/` (Kubernetes / Kustomize manifests) + + +- [ ] `scripts/` — contract deploy/rollback, seed, smoke test, backup/restore/db-rollback +- [ ] `scripts/check-env.js` — environment variable validation +- [ ] `k8s/` — Kubernetes / Kustomize manifests - [ ] `backend/Dockerfile` or `frontend/Dockerfile` -- [ ] `monitoring/` (Prometheus, Alertmanager, Grafana) -- [ ] `.github/workflows/` (CI, K8s deploy, contract deploy, backups) -- [ ] `docs/` (runbooks and guides) +- [ ] `monitoring/` — Prometheus, Alertmanager, Grafana +- [ ] `.github/workflows/` — CI, K8s deploy, contract deploy, backups +- [ ] `docs/` — runbooks and guides +- [ ] `docker-compose.yml` / `.env.example` + +## Environment + +| Field | Value | +|-------|-------| +| OS / Shell | | +| Node.js version (`node --version`) | | +| npm version (`npm --version`) | | +| Target network | `testnet` / `mainnet` / `futurenet` / `local` | +| Deploy target | `compose` / `k8s` / `railway` | +| Relevant tool versions (kubectl, docker, etc.) | | ## Steps to Reproduce @@ -21,16 +36,17 @@ assignees: "" 2. 3. -## Expected Behavior +## Expected Behaviour + + -## Actual Behavior / Logs +## Actual Behaviour / Error Output ```text -Paste relevant error output or logs here (redact any secrets or keys). +Paste relevant error output or logs here. +IMPORTANT: redact any secrets, keys, or connection strings before pasting. ``` -## Environment +## Additional Context -- OS / Shell: -- Node.js version: -- Target network (`testnet` / `mainnet` / `futurenet`): + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 9c93d05..9a93cac 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,6 @@ --- name: Feature / Operational Enhancement -about: Propose an improvement to deployment automation, observability, or runbooks +about: Propose an improvement to deployment automation, observability, scripts, or runbooks title: "[feat] " labels: ["enhancement"] assignees: "" @@ -8,13 +8,38 @@ assignees: "" ## Problem Statement - + ## Proposed Solution - + + +## Alternatives Considered + + + +## Affected Components + + + +- [ ] `scripts/` — deployment or operational scripts +- [ ] `k8s/` — Kubernetes manifests / Kustomize overlays +- [ ] `backend/Dockerfile` or `frontend/Dockerfile` +- [ ] `.github/workflows/` — CI or deploy workflows +- [ ] `monitoring/` — alerting or dashboards +- [ ] `docs/` — runbooks or guides +- [ ] `docker-compose.yml` / `.env.example` ## Acceptance Criteria + + +- [ ] - [ ] - [ ] + +## Additional Context + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 70c47b9..5a48495 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,9 +7,53 @@ Closes # +## Type of change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature or enhancement +- [ ] Documentation update +- [ ] Dependency upgrade +- [ ] Refactor / code quality improvement + +## Changes made + + + +- +- + +## Testing + + + ## Verification Checklist +**Environment** +- [ ] `node scripts/check-env.js --target compose` passes (no placeholder values) + +**Scripts** - [ ] Shell scripts pass `shellcheck scripts/*.sh` and `bash -n scripts/*.sh` - [ ] Node scripts and unit tests pass (`npm test`) -- [ ] Kubernetes manifests render and validate cleanly (`kubectl kustomize k8s/` / `kubeconform -strict`) -- [ ] Documentation in `docs/` or `README.md` has been updated if operational behavior changed +- [ ] ESLint passes (`npm run lint`) +- [ ] Prettier check passes (`npm run format:check`) + +**Kubernetes** _(skip if no `k8s/` changes)_ +- [ ] Manifests render cleanly (`kubectl kustomize k8s/ > /dev/null`) +- [ ] `kubeconform -strict` reports no errors +- [ ] `kube-linter lint` reports no errors + +**Monitoring** _(skip if no `monitoring/` changes)_ +- [ ] `promtool check rules monitoring/alert-rules.yml` passes +- [ ] `amtool check-config monitoring/alertmanager.yml` passes + +**Docker Compose** _(skip if no compose/Dockerfile changes)_ +- [ ] `docker compose config --quiet` passes with `.env.example` + +**Documentation** +- [ ] `docs/` or `README.md` updated if operational behaviour changed +- [ ] `docs/ENVIRONMENT-VARIABLES.md` updated if any env var was added/changed +- [ ] Cross-links point to the right canonical guides (no duplicated Railway/k8s content) + +**Security** +- [ ] No secrets, keys, or real credentials introduced in any file +- [ ] `k8s/secrets.yaml` not added to `k8s/kustomization.yaml` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74b639a..2f2da14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,15 @@ jobs: run: npm ci - name: Syntax-check scripts run: for f in scripts/*.js scripts/*.cjs; do node --check "$f"; done + - name: Check env placeholders (compose target) + run: | + cp .env.example .env + node scripts/check-env.js --target compose --env-file .env || true + # In CI the .env.example intentionally contains placeholders. + # This step validates that check-env.js itself runs without crashing; + # the actual placeholder check is enforced in deploy workflows where + # real secrets are available via GitHub Actions secrets. + node scripts/check-env.js --help > /dev/null - name: Unit tests run: npm test - name: CLI smoke tests @@ -197,6 +206,11 @@ jobs: run: npm ci - name: Prepare env run: cp .env.example .env + - name: Check env (compose) + # .env.example contains placeholder values; this is a best-effort check + # to ensure check-env.js runs. Failures are informational only in CI + # because secrets are not available in this job. + run: node scripts/check-env.js --target compose --env-file .env || true - name: Start stack run: docker compose up -d --wait - name: Run smoke test diff --git a/.github/workflows/deploy-k8s.yml b/.github/workflows/deploy-k8s.yml index 94910c2..4b6c25d 100644 --- a/.github/workflows/deploy-k8s.yml +++ b/.github/workflows/deploy-k8s.yml @@ -236,6 +236,41 @@ jobs: echo "ready=true" >> "$GITHUB_OUTPUT" fi + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: npm + - name: Install dependencies + run: npm ci + - name: Validate environment variables (k8s target) + # Fail fast if any required variable is missing or contains a placeholder. + # Secrets are mapped from GitHub Actions secrets into the environment so + # the script can verify they have been filled in. + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }} + SUPABASE_URL: ${{ vars.SUPABASE_URL }} + SUPABASE_STORAGE_BUCKET: ${{ vars.SUPABASE_STORAGE_BUCKET }} + STELLAR_NETWORK: ${{ vars.STELLAR_NETWORK }} + SOROBAN_RPC_URL: ${{ vars.SOROBAN_RPC_URL }} + USDC_CONTRACT_ID: ${{ vars.USDC_CONTRACT_ID }} + PAY_TO: ${{ vars.PAY_TO }} + AGENT_SECRET_KEY: ${{ secrets.AGENT_SECRET_KEY }} + REGISTRY_SECRET_KEY: ${{ secrets.REGISTRY_SECRET_KEY }} + STELLAR_CONTRACT_VAULT_REGISTRY: ${{ vars.STELLAR_CONTRACT_VAULT_REGISTRY }} + STELLAR_CONTRACT_ACCESS_LEASE: ${{ vars.STELLAR_CONTRACT_ACCESS_LEASE }} + STELLAR_CONTRACT_SUBSCRIPTION: ${{ vars.STELLAR_CONTRACT_SUBSCRIPTION }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + ADMIN_API_KEY: ${{ secrets.ADMIN_API_KEY }} + STELLAR_SECRET_KEY: ${{ secrets.STELLAR_SECRET_KEY }} + STELLAR_PUBLIC_KEY: ${{ vars.STELLAR_PUBLIC_KEY }} + POSTGRES_USER: ${{ vars.POSTGRES_USER || 'postgres' }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD || 'postgres' }} + POSTGRES_DB: ${{ vars.POSTGRES_DB || 'app' }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + run: node scripts/check-env.js --target k8s + - name: Set up kubectl if: steps.config.outputs.ready == 'true' uses: azure/setup-kubectl@v5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 514fb25..f800ac4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,98 +1,250 @@ # Contributing to SynapsVault Deploy -Thank you for contributing to `SynapsVault/deploy`! This repository contains the deployment scripts, Dockerfiles, Kubernetes Kustomize manifests, monitoring configurations, and operational runbooks for the SynapsVault platform. +Thank you for contributing to `SynapsVault/deploy`! This repository contains +the deployment scripts, Dockerfiles, Kubernetes Kustomize manifests, monitoring +configurations, and operational runbooks for the SynapsVault platform. + +--- + +## Table of Contents + +1. [Prerequisites & Setup](#1-prerequisites--setup) +2. [Running Tests & Quality Checks](#2-running-tests--quality-checks) +3. [Repository Conventions](#3-repository-conventions) +4. [Submitting a Pull Request](#4-submitting-a-pull-request) +5. [Reporting Issues](#5-reporting-issues) +6. [Security Vulnerabilities](#6-security-vulnerabilities) --- ## 1. Prerequisites & Setup -Ensure the following tools are installed on your workstation: +### Required tools + +| Tool | Version | Purpose | +|------|---------|---------| +| Node.js | `>= 22.12.0` | Run and test scripts (`@stellar/stellar-sdk` v17+ requirement) | +| npm | bundled with Node | Dependency management | +| Bash | any | Shell scripts in `scripts/*.sh` | +| ShellCheck | any | Lint shell scripts | + +Install Node.js via nvm (version is read from `.nvmrc`): + +```bash +nvm use +node --version # should print v22.x.x or newer +npm ci +``` -- **Node.js `>= 22.12.0`** (required by `@stellar/stellar-sdk` v17+; see `.nvmrc`): - ```bash - nvm use - node --version - npm ci - ``` -- **Bash & ShellCheck** (for validating operational scripts under `scripts/*.sh`): - ```bash - shellcheck --version - ``` -- **kubectl & kubeconform** (optional locally; used to validate `k8s/` manifests): - ```bash - kubectl kustomize k8s/ > /dev/null - ``` -- **Docker & Docker Compose** (optional locally; used to validate `docker-compose.yml` and build container images). +### Optional tools (needed only for full local validation) -### Local Stack Setup +| Tool | Purpose | +|------|---------| +| Docker & Docker Compose | Validate and smoke-test `docker-compose.yml` | +| kubectl | Validate Kubernetes manifests (`k8s/`) | +| kubeconform | Schema-validate rendered manifests | +| kube-linter | Policy-lint rendered manifests | +| promtool / amtool | Validate `monitoring/` config | -To spin up the full local development stack alongside sibling checkouts of `SynapsVault/backend` and `SynapsVault/frontend`: +### Local stack setup + +Clone the sibling application repos, then start the full stack from this repo's +root — Compose builds images from those checkouts: ```bash git clone https://github.com/SynapsVault/backend ../backend git clone https://github.com/SynapsVault/frontend ../frontend cp .env.example .env +# Optional: edit .env to set real secrets if you need live Stellar/Supabase +node scripts/check-env.js --target compose # flag missing/placeholder values docker compose up --build ``` +Service URLs (configurable via `.env`): + +| Service | URL | +|------------|-----------------------| +| Frontend | http://localhost:8080 | +| Backend | http://localhost:3000 | +| Postgres | localhost:5432 | +| Prometheus | http://localhost:9090 | +| Grafana | http://localhost:3001 | + --- ## 2. Running Tests & Quality Checks -All pull requests are verified by `.github/workflows/ci.yml`. Run the following checks locally before opening a pull request: +All pull requests are verified by `.github/workflows/ci.yml`. Run the same +checks locally before opening a PR. + +### Environment variable check + +Before starting the stack or running integration tests, verify that no +placeholder values remain in your `.env`: + +```bash +node scripts/check-env.js --target compose # local compose stack +node scripts/check-env.js --target k8s # Kubernetes deploy +``` + +### Unit tests -### Unit Tests -Run the Node.js built-in test runner against `test/**/*.test.cjs`: ```bash npm test ``` -### Linting & Formatting (Node.js Scripts) -Check JavaScript/CommonJS syntax, ESLint rules, and Prettier formatting: +Runs the Node.js built-in test runner against `test/**/*.test.cjs`. + +### Linting & formatting + ```bash +# ESLint npm run lint + +# Prettier (check only) npm run format:check -``` -To automatically format files with Prettier: -```bash + +# Prettier (auto-fix) npm run format ``` -### Shell Script Linting -Every shell script in `scripts/*.sh` must pass both `shellcheck` and `bash -n`: +### Shell script linting + ```bash shellcheck scripts/*.sh for f in scripts/*.sh; do bash -n "$f"; done ``` -### Kubernetes & Monitoring Validation +### Kubernetes validation + ```bash kubectl kustomize k8s/ > rendered.yaml kubeconform -strict -summary rendered.yaml -kubeconform -strict -summary k8s/secrets.yaml +kube-linter lint rendered.yaml +``` + +### Monitoring validation + +```bash +promtool check rules monitoring/alert-rules.yml +promtool test rules monitoring/tests/*.yml +amtool check-config monitoring/alertmanager.yml python3 -m json.tool monitoring/grafana-dashboard.json > /dev/null ``` +### Full pre-push checklist + +```bash +npm ci +npm test +npm run lint +npm run format:check +node scripts/check-env.js --target compose +shellcheck scripts/*.sh +kubectl kustomize k8s/ > /dev/null +cp .env.example .env && docker compose config --quiet +``` + --- ## 3. Repository Conventions -### Node.js Scripts (`scripts/*.js`, `scripts/*.cjs`) -- **Module format**:The package defaults to ES Modules (`"type": "module"`). Step scripts (`01`–`06`) use ESM (`.js`), while the standalone contract deployment/rollback CLI tools (`deploy-contracts.cjs`, `rollback-contracts.cjs`) use CommonJS (`.cjs`) so their pure helpers can be required directly by `test/*.test.cjs`. -- **Exit codes**: Scripts must exit non-zero (`process.exit(1)`) whenever required environment variables are missing, validation fails, or an API/RPC step fails. Never silently swallow errors. -- **No hard-coded secrets or mainnet defaults**: Default to `testnet` and require explicit confirmation (`--yes` in CI) before executing destructive or mainnet operations. -- **Offline unit tests**: Keep CLI argument parsing, environment validation, and file helpers exported so they can be unit-tested offline in `test/` without live RPC or Friendbot calls. - -### Shell Scripts (`scripts/*.sh`) -- Start with `#!/usr/bin/env bash` and strict mode (`set -Eeuo pipefail` or `set -euo pipefail`). -- Quote all variable expansions (`"${VAR}"`) and keep scripts clean under `shellcheck`. -- Document supported flags, environment variables, and exit codes in the header comment block so `--help` stays accurate. - -### Kubernetes Manifests (`k8s/`) -- Keep real secrets out of version control. `k8s/secrets.yaml` is a placeholder template and must **not** be added to `k8s/kustomization.yaml`. -- All workloads must run as non-root with `allowPrivilegeEscalation: false` and explicit resource requests/limits. - -### Commit Messages & Pull Requests -- Use Conventional Commits (`feat(k8s): ...`, `fix(scripts): ...`, `docs: ...`, `test: ...`, `chore: ...`). -- Reference the issue(s) resolved in the commit body and PR description (e.g., `Closes #38`). -- Keep changes focused and ensure all CI jobs pass. +### Node.js scripts (`scripts/*.js`, `scripts/*.cjs`) + +- **Module format**: The package uses ES Modules (`"type": "module"`). Step + scripts (`01`–`06`) and utilities (`.js`) use ESM. The standalone contract + CLI tools (`deploy-contracts.cjs`, `rollback-contracts.cjs`) use CommonJS + (`.cjs`) so their helpers can be `require()`-d by `test/*.test.cjs`. +- **Exit codes**: Scripts **must** exit non-zero (`process.exit(1)`) whenever + required environment variables are missing, validation fails, or an RPC step + fails. Never swallow errors silently. +- **No hard-coded secrets or mainnet defaults**: Default to `testnet`; require + explicit `--yes` before executing destructive or mainnet operations. +- **Offline unit tests**: Export CLI argument parsers, environment validators, + and file helpers so they can be unit-tested in `test/` without live network + calls. + +### Shell scripts (`scripts/*.sh`) + +- Start with `#!/usr/bin/env bash` and `set -Eeuo pipefail`. +- Quote all variable expansions: `"${VAR}"`. +- Keep scripts clean under `shellcheck`. +- Document supported flags, environment variables, and exit codes in the header + comment block so `--help` stays accurate. + +### Kubernetes manifests (`k8s/`) + +- **No real secrets in version control.** `k8s/secrets.yaml` is a placeholder + template and must **not** be added to `k8s/kustomization.yaml`. +- All workloads must run as non-root with `allowPrivilegeEscalation: false` and + explicit resource requests/limits. +- Use Kustomize overlays (`k8s/overlays/staging`, `k8s/overlays/production`) to + separate environment-specific values from the base. + +### Documentation (`docs/`, `README.md`) + +- Each doc must have a clear purpose and cross-link to related docs rather than + duplicating content. +- Railway deployment is documented once in + [`docs/railway-deploy.md`](./docs/railway-deploy.md); all other mentions + link there. +- Update `docs/ENVIRONMENT-VARIABLES.md` whenever a variable is added or + renamed — that is the single source of truth. + +### Commit messages + +Use [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat(k8s): add HorizontalPodAutoscaler for backend +fix(scripts): handle missing DEPLOYER_SECRET gracefully +docs: update Railway guide with Supabase pooler URL +test(contract): cover rollback with unknown contract ID +chore(deps): update @stellar/stellar-sdk to 17.3.0 +``` + +Reference resolved issues in the commit body: `Closes #42`. + +--- + +## 4. Submitting a Pull Request + +1. **Fork** the repository and create a branch from `main`: + ```bash + git checkout -b feat/your-feature-name + ``` +2. Make your changes, following the conventions above. +3. Run the full pre-push checklist (see §2). +4. Open a PR against `main`. Fill in the PR template — link the issue, describe + what changed and how you tested it. +5. At least one member of `@SynapsVault/core` must approve before merging. +6. Merge by **squash-and-merge** so every commit on `main` corresponds to one + PR and references one issue. + +### PR scope + +Keep PRs focused. A PR that touches `k8s/`, `scripts/`, and `docs/` for the +same feature is fine; a PR that mixes unrelated changes is not. If you are +fixing a bug you found while working on a feature, open a separate PR for the +fix. + +--- + +## 5. Reporting Issues + +Use the GitHub issue templates: + +- **[Bug report](.github/ISSUE_TEMPLATE/bug_report.md)** — for failures in + scripts, manifests, Dockerfiles, CI workflows, or runbooks. +- **[Feature / Enhancement](.github/ISSUE_TEMPLATE/feature_request.md)** — for + improvements to deployment automation, observability, or documentation. + +Redact any secrets, keys, or connection strings before pasting logs. + +--- + +## 6. Security Vulnerabilities + +**Do not report security vulnerabilities through public issues.** + +See [SECURITY.md](./SECURITY.md) for the responsible disclosure process. diff --git a/README.md b/README.md index da923bc..1805bc0 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,111 @@ This repo holds deployment tooling only; the application code lives in --- +## Architecture + +### Docker Compose (local development) + +```mermaid +graph TD + Browser["Browser / Client"] + + subgraph compose["docker-compose stack (this repo)"] + FE["frontend\n(Vite · :8080)"] + BE["backend\n(Node.js · :3000)"] + MIG["migrate\n(one-shot job)"] + PG["postgres\n(PostgreSQL 18 · :5432)"] + + subgraph monitoring["monitoring profile"] + PGEXP["postgres-exporter"] + PROM["prometheus\n(:9090)"] + AM["alertmanager\n(:9093)"] + GF["grafana\n(:3001)"] + end + end + + subgraph external["External services"] + SUPABASE["Supabase\n(storage)"] + STELLAR["Stellar / Soroban\n(testnet RPC)"] + OR["OpenRouter\n(LLM)"] + end + + Browser -->|"HTTP :8080"| FE + Browser -->|"HTTP :3000"| BE + FE -->|"VITE_API_URL"| BE + BE -->|"DATABASE_URL"| PG + MIG -->|"DATABASE_URL (migrations)"| PG + BE -->|"SUPABASE_URL"| SUPABASE + BE -->|"SOROBAN_RPC_URL"| STELLAR + BE -->|"OPENROUTER_API_KEY"| OR + + PGEXP -->|"scrape"| PG + PROM -->|"scrape"| BE + PROM -->|"scrape"| PGEXP + AM -->|"alerts"| PROM + GF -->|"datasource"| PROM +``` + +### Kubernetes (production) + +```mermaid +graph TD + Browser["Browser / Client"] + + subgraph cluster["Kubernetes cluster (namespace: synapsvault)"] + ING["Ingress\n(nginx / cert-manager)"] + + subgraph fe_svc["frontend"] + FE_DEP["Deployment\n(2 replicas)"] + end + + subgraph be_svc["backend"] + BE_DEP["Deployment\n(2–5 replicas · HPA)"] + end + + MIG_JOB["backend-migrate\n(Job — runs before rollout)"] + + subgraph obs["observability"] + SM_BE["ServiceMonitor\n(backend)"] + SM_FE["ServiceMonitor\n(frontend)"] + PR["PrometheusRule\n(alert rules)"] + end + + PDB["PodDisruptionBudget"] + CM["ConfigMap\n(non-secret env)"] + SEC["Secrets\n(DATABASE_URL, keys…)"] + end + + subgraph external["External services"] + SUPABASE["Supabase\n(Postgres + storage)"] + STELLAR["Stellar / Soroban\n(mainnet RPC)"] + OR["OpenRouter\n(LLM)"] + PROM_OP["Prometheus Operator\n(cluster-level)"] + GHCR["GHCR\n(container images)"] + end + + Browser -->|"HTTPS"| ING + ING -->|"/api"| BE_DEP + ING -->|"/"| FE_DEP + BE_DEP -->|"DATABASE_URL"| SUPABASE + BE_DEP -->|"SUPABASE_URL"| SUPABASE + BE_DEP -->|"SOROBAN_RPC_URL"| STELLAR + BE_DEP -->|"OPENROUTER_API_KEY"| OR + MIG_JOB -->|"DATABASE_URL"| SUPABASE + CM -->|"env injection"| BE_DEP + CM -->|"env injection"| FE_DEP + SEC -->|"env injection"| BE_DEP + SEC -->|"env injection"| MIG_JOB + PDB -->|"guards"| BE_DEP + SM_BE -->|"scraped by"| PROM_OP + SM_FE -->|"scraped by"| PROM_OP + PR -->|"loaded by"| PROM_OP + GHCR -->|"image pull"| FE_DEP + GHCR -->|"image pull"| BE_DEP + GHCR -->|"image pull"| MIG_JOB +``` + +--- + ## Local Development (Docker Compose) Clone the application repos next to this one, then bring up the full stack @@ -202,18 +307,16 @@ Save the contract IDs from `contract-ids.env` — you need them for the backend. ### Step 5 — Deploy backend to Railway -Follow: [docs/railway-deploy.md](./docs/railway-deploy.md) +Follow the canonical guide: **[docs/railway-deploy.md](./docs/railway-deploy.md)** -Key env vars to set in Railway: -``` -DATABASE_URL = your Supabase connection string -STELLAR_CONTRACT_VAULT_REGISTRY = from contract-ids.env -STELLAR_CONTRACT_ACCESS_LEASE = from contract-ids.env -STELLAR_CONTRACT_SUBSCRIPTION = from contract-ids.env -STELLAR_SECRET_KEY = your BACKEND secret key -ADMIN_API_KEY = openssl rand -hex 32 +Before deploying, verify your `.env` has no unfilled placeholders: +```bash +node scripts/check-env.js --target railway ``` +The guide covers Supabase setup, Railway project creation, all required +environment variables, migrations, verification, and troubleshooting. + ### Step 6 — Run migrations In Railway shell: diff --git a/SECURITY.md b/SECURITY.md index c015232..cb2fd5d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,35 +2,144 @@ ## Supported Versions -Only the latest state of the `main` branch and active production deployment manifests are supported with security updates. +Only the latest state of the `main` branch and the active production deployment +manifests receive security updates. -| Version / Branch | Supported | -| --- | --- | -| `main` | :white_check_mark: | -| Older commits / tags | :x: | +| Branch / State | Supported | +|----------------|-----------| +| `main` (HEAD) | ✅ | +| Older commits / tags | ❌ | + +--- + +## Scope + +This policy covers security issues in: + +| Component | Examples | +|-----------|---------| +| `scripts/` | Contract deploy/rollback, backup, seed, smoke-test scripts | +| `k8s/` | Kubernetes manifests and Kustomize overlays | +| `backend/Dockerfile`, `frontend/Dockerfile` | Image builds | +| `.github/workflows/` | CI, deploy, backup workflows | +| `monitoring/` | Prometheus, Alertmanager, Grafana configuration | +| `docs/` | Runbooks that contain security-relevant instructions | + +Issues in the application code itself (`SynapsVault/backend`, +`SynapsVault/frontend`, `SynapsVault/contracts`) should be reported to those +repositories. + +--- ## Reporting a Vulnerability -**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** +**Please do not use public GitHub issues, discussions, or pull requests to +report security vulnerabilities.** + +### Option 1 — GitHub Private Vulnerability Reporting (preferred) + +1. Go to the **Security** tab of this repository. +2. Click **Report a vulnerability**. +3. Fill in the advisory form with the details below. -If you discover a vulnerability in `SynapsVault/deploy`, `SynapsVault/backend`, `SynapsVault/frontend`, or `SynapsVault/contracts`: +GitHub will notify the maintainers privately and open a draft security advisory. -1. **GitHub Private Vulnerability Reporting (Preferred)**: Submit a report via the repository's [Security Advisories](https://github.com/SynapsVault/deploy/security/advisories/new) tab. -2. **Email**: Send a detailed report to `security@synapsvault.xyz` including: - - Affected component (`k8s/`, `scripts/`, `backend/Dockerfile`, `frontend/Dockerfile`, `.github/workflows/`, or contracts) - - Reproduction steps or proof-of-concept - - Potential impact and suggested remediation +### Option 2 — Email -### Response SLA +Send a report to **security@synapsvault.xyz** with the subject line: -| Milestone | Target SLA | -| --- | --- | +``` +[SECURITY] +``` + +Include: + +- **Affected component**: which directory or file (e.g., `scripts/backup.sh`, + `k8s/secrets.yaml`, `.github/workflows/deploy-k8s.yml`). +- **Reproduction steps**: the minimal sequence of actions to trigger the issue. +- **Proof of concept** (if applicable): a script, manifest diff, or screenshot. +- **Potential impact**: what an attacker could do if they exploited this. +- **Suggested remediation** (optional): your recommended fix. + +Redact any real secrets, keys, or connection strings before sending. + +--- + +## Response SLA + +| Milestone | Target | +|-----------|--------| | Initial acknowledgment | Within **48 hours** | -| Triage & severity assessment | Within **5 business days** | -| Remediation & coordinated disclosure | Within **14–30 days** depending on severity | +| Triage and severity assessment | Within **5 business days** | +| Remediation and coordinated disclosure | Within **14–30 days** (depending on severity and complexity) | + +We follow [coordinated vulnerability disclosure](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html). +If a fix requires more than 30 days we will communicate a revised timeline. + +--- ## Operational Security Guidelines -- **Zero Secrets in Git**: Never commit `.env`, `secrets.env`, `kubeconfig`, Stellar secret keys (`S...`), or unencrypted database dumps. `k8s/secrets.yaml` is strictly a placeholder template and must never be added to `k8s/kustomization.yaml`. -- **Backup Encryption**: Production backups generated by `scripts/backup.sh` must always use `gpg` or `age` encryption (`BACKUP_ENCRYPTION_METHOD` must not be `none` in production). -- **Mainnet Guardrails**: Contract deployments (`scripts/deploy-contracts.cjs`) and database restores (`scripts/restore.sh`) require explicit operator confirmation before modifying live state. +Follow these rules in all contributions to this repository. + +### Secrets + +- **Zero secrets in Git.** Never commit `.env`, `secrets.env`, `kubeconfig`, + Stellar secret keys (`S...`), service-role JWT tokens, or unencrypted + database dumps. +- `k8s/secrets.yaml` is a **placeholder template only** and must never be added + to `k8s/kustomization.yaml` or pushed with real values. +- Use `node scripts/check-env.js` before deploying to catch placeholder values + (e.g. `REPLACE_ME_*`, `change-me-*`, `S[X...]`, `C[X...]`) that were never + replaced. +- The CI `secrets` job runs [gitleaks](https://github.com/gitleaks/gitleaks) + on every push and pull request. A gitleaks finding blocks merging. + +### Backups + +- Production backups (`scripts/backup.sh`) **must** use `gpg` or `age` + encryption. `BACKUP_ENCRYPTION_METHOD=none` is forbidden in production. +- Backup archives are uploaded to S3 with server-side encryption. Never store + unencrypted dumps in a public bucket. + +### Mainnet guardrails + +- Contract deployments (`scripts/deploy-contracts.cjs`) and database restores + (`scripts/restore.sh`) require explicit operator confirmation (interactive + prompt or `--yes` flag) before modifying live state. +- The `STELLAR_NETWORK` environment variable must be set to `mainnet` explicitly + — scripts default to `testnet`. + +### Supply chain + +- Dependencies are pinned to exact versions in `package.json` and + `package-lock.json`. Open-ended ranges (`^`, `~`) require maintainer approval. +- Dependabot is configured to open weekly PRs for dependency updates + (`.github/dependabot.yml`). +- Container images built in CI are scanned with Trivy; any unfixed CRITICAL CVE + fails the build. +- Images pushed to production are signed with cosign (keyless, Sigstore). + +### Workflow permissions + +- All GitHub Actions workflows use the minimum required `permissions` scope. + `contents: read` is the default; write access is granted only where + explicitly needed (e.g., pushing images, uploading SARIF). +- Secrets are never echoed in workflow logs (`set -x` is prohibited in `run:` + blocks that handle secrets). + +--- + +## Security-relevant changes + +Changes to the following areas require a review comment explaining the security +implications: + +- `k8s/secrets.yaml` or any file that touches how secrets are injected +- `.github/workflows/` — any change to how secrets are consumed or exported +- `scripts/backup.sh` or `scripts/restore.sh` +- `scripts/deploy-contracts.cjs` (mainnet guard logic) +- Dockerfile `USER`, `RUN`, or `COPY` instructions that affect the runtime + attack surface + +Tag such PRs with the `security` label so they receive a dedicated review. diff --git a/docs/DEPLOY-BACKEND-GUIDE.md b/docs/DEPLOY-BACKEND-GUIDE.md index 2ffcbd4..bd7160a 100644 --- a/docs/DEPLOY-BACKEND-GUIDE.md +++ b/docs/DEPLOY-BACKEND-GUIDE.md @@ -1,435 +1,19 @@ -# SynapsVault Backend — Full Deployment Guide -## Supabase + Railway (both free tier, ~20 minutes total) - ---- - -## What you'll have at the end - -- A live backend API at `https://your-app.up.railway.app` -- A Postgres database on Supabase (free, 500 MB) -- File storage on Supabase (free, 1 GB) -- Auto-deploy whenever you push to `main` - ---- - -## Part 1 — Supabase (database + storage) -**Time: ~8 minutes** - -### Step 1.1 — Create a Supabase account - -1. Go to **[supabase.com](https://supabase.com)** -2. Click **Start your project** → sign up with GitHub (easiest) -3. You'll land on your Supabase dashboard - ---- - -### Step 1.2 — Create a new project - -1. Click **New project** -2. Fill in: - - **Name:** `synapsvault-testnet` - - **Database Password:** choose a strong password — **write it down, you'll need it** - - **Region:** choose the one closest to you (Lagos → `eu-west-2` London or `us-east-1`) -3. Click **Create new project** -4. Wait ~2 minutes for the project to provision (you'll see a spinning loader) - ---- - -### Step 1.3 — Get your Database URL - -1. In the left sidebar click **Settings** (gear icon at the bottom) -2. Click **Database** -3. Scroll down to **Connection string** -4. Click the **URI** tab -5. Copy the full connection string — it looks like: - ``` - postgres://postgres:[YOUR-DB-PASSWORD]@db.abcdefghijkl.supabase.co:5432/postgres - ``` -6. Replace `[YOUR-PASSWORD]` with the password you set in Step 1.2 -7. **Save this** — this is your `DATABASE_URL` - -> ⚠️ If you forget the password, go to Settings → Database → Reset database password - ---- - -### Step 1.4 — Get your Service Role Key - -1. In the left sidebar click **Settings** → **API** -2. Under **Project API keys** find the row labelled **`service_role`** -3. Click the eye icon to reveal it, then copy it -4. **Save this** — this is your `SUPABASE_SERVICE_KEY` - -It looks like: `` - ---- - -### Step 1.5 — Get your Supabase project URL - -1. Still in **Settings → API** -2. Under **Project URL** copy the URL -3. It looks like: `https://abcdefghijkl.supabase.co` -4. **Save this** — this is your `SUPABASE_URL` - ---- - -### Step 1.6 — Create the file storage bucket - -1. In the left sidebar click **Storage** -2. Click **New bucket** -3. Name it exactly: `resources` -4. Toggle **Public bucket** to **OFF** (private — access controlled by the backend) -5. Click **Save** - ---- - -## Part 2 — Railway (deploy the backend) -**Time: ~10 minutes** - -### Step 2.1 — Create a Railway account - -1. Go to **[railway.app](https://railway.app)** -2. Click **Login** → **Login with GitHub** -3. Authorise Railway to access your GitHub account - ---- - -### Step 2.2 — Create a new project - -1. From the Railway dashboard click **New Project** -2. Select **Deploy from GitHub repo** -3. If this is your first time, click **Configure GitHub App** and give Railway access to your repositories -4. Search for and select **`SynapsVault/backend`** -5. Click **Deploy Now** - -Railway will start building automatically using the `Dockerfile` in the repo. - -> 💡 The first build takes 3–5 minutes. You can watch the build log in real time. - ---- - -### Step 2.3 — Add a Postgres database - -1. While the build runs, click **+ New** in the top right of your project -2. Select **Database** → **Add PostgreSQL** -3. Railway creates a managed Postgres instance and automatically sets `DATABASE_URL` in your service — **but** we'll override it with Supabase's URL in the next step - ---- - -### Step 2.4 — Set environment variables - -This is the most important step. Click on your **SynapsVault-backend** service (the box in the project), then click the **Variables** tab. - -> 📖 For the canonical, complete list of every environment variable the backend reads — including defaults, validation rules, and which ones are required — see **[docs/ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md)**. The tables below cover the values you need for this deployment; the reference guide is the source of truth. - -Add each variable below. Click **+ New Variable** for each one. - ---- - -#### 🔴 Required — must set these or the server won't start - -| Variable | Value | Where to get it | -|---|---|---| -| `DATABASE_URL` | `postgres://postgres:[YOUR-DB-PASSWORD]@db.[REF].supabase.co:5432/postgres` | Supabase → Settings → Database → Connection string (URI) | -| `SUPABASE_URL` | `https://[REF].supabase.co` | Supabase → Settings → API → Project URL | -| `SUPABASE_SERVICE_KEY` | `eyJ...` (long JWT token) | Supabase → Settings → API → service_role | -| `SUPABASE_STORAGE_BUCKET` | `resources` | You created this in Step 1.6 | -| `STELLAR_NETWORK` | `testnet` | Fixed value | -| `NETWORK` | `testnet` | Fixed value | -| `SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | Fixed value | -| `USDC_CONTRACT_ID` | `CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA` | Fixed — testnet USDC | -| `PAY_TO` | `GCK7ALS7EP5DQG4UZMON3XTNUE7SSIW27XW24UOLXDHBOHWEWIGZVPZF` | Your BACKEND wallet public key | -| `AGENT_SECRET_KEY` | `` | Your BACKEND wallet secret key | -| `REGISTRY_SECRET_KEY` | `` | Same as AGENT_SECRET_KEY | -| `STELLAR_CONTRACT_VAULT_REGISTRY` | `CBQEIMSRPSRKJJHGOELZTP3CISZVHZ6WPKZTWJMYZXHFXPGHHWFQBD4H` | Deployed contract (from `contract-ids.env`) | -| `STELLAR_CONTRACT_ACCESS_LEASE` | `CDQZBCEPN6RCJAQLEKB3ZRNLUZXDCCFMVIS6STZNAOEAQLXD3FUBNQGX` | Deployed contract (from `contract-ids.env`) | -| `STELLAR_CONTRACT_SUBSCRIPTION` | `CCCE6Q6WHDICGMQWXXXMM6X7YDGK3BXU4JNQJNSIA6XQRC52MQL42CD6` | Deployed contract (from `contract-ids.env`) | -| `OPENROUTER_API_KEY` | Get a free key at [openrouter.ai](https://openrouter.ai) | Free tier available | -| `ADMIN_API_KEY` | `` | Keep this secret | - ---- - -#### 🟡 Optional but recommended - -| Variable | Value | Notes | -|---|---|---| -| `NODE_ENV` | `production` | Enables production optimisations | -| `PORT` | `3000` | Railway sets this automatically too | -| `ALLOWED_ORIGINS` | `https://your-frontend.vercel.app` | Set after you have your Vercel URL | -| `FACILITATOR_URL` | `https://www.x402.org/facilitator` | Default x402 facilitator | -| `STELLAR_HORIZON_URL` | `https://horizon-testnet.stellar.org` | Already defaulted | - ---- - -#### Copy-paste block (fill in the blanks marked with `← FILL IN`) - -``` -DATABASE_URL=postgres://postgres:[YOUR-DB-PASSWORD]@db.[YOUR-REF].supabase.co:5432/postgres ← FILL IN -SUPABASE_URL=https://[YOUR-REF].supabase.co ← FILL IN -SUPABASE_SERVICE_KEY=eyJ... ← FILL IN (from Supabase API settings) -SUPABASE_STORAGE_BUCKET=resources -STELLAR_NETWORK=testnet -NETWORK=testnet -SOROBAN_RPC_URL=https://soroban-testnet.stellar.org -USDC_CONTRACT_ID=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA -PAY_TO=GCK7ALS7EP5DQG4UZMON3XTNUE7SSIW27XW24UOLXDHBOHWEWIGZVPZF -AGENT_SECRET_KEY= -REGISTRY_SECRET_KEY= -STELLAR_CONTRACT_VAULT_REGISTRY=CBQEIMSRPSRKJJHGOELZTP3CISZVHZ6WPKZTWJMYZXHFXPGHHWFQBD4H -STELLAR_CONTRACT_ACCESS_LEASE=CDQZBCEPN6RCJAQLEKB3ZRNLUZXDCCFMVIS6STZNAOEAQLXD3FUBNQGX -STELLAR_CONTRACT_SUBSCRIPTION=CCCE6Q6WHDICGMQWXXXMM6X7YDGK3BXU4JNQJNSIA6XQRC52MQL42CD6 -OPENROUTER_API_KEY=sk-or-... ← FILL IN (get free at openrouter.ai) -ADMIN_API_KEY= -NODE_ENV=production -PORT=3000 -FACILITATOR_URL=https://www.x402.org/facilitator -``` - -After adding all variables, Railway will **automatically redeploy**. - ---- - -### Step 2.5 — Run database migrations - -The database is empty. You need to apply the schema. - -1. Click on your **SynapsVault-backend** service -2. Click the **Settings** tab -3. Scroll to **Deploy** → find **Custom Start Command** — leave it as is (uses Dockerfile CMD) -4. Now open the **Shell** tab (or click the terminal icon) -5. Run this command: - -```bash -for f in $(ls /app/drizzle/*.sql | sort); do - echo "Applying $f..." - node -e " -const { Pool } = require('pg'); -const fs = require('fs'); -const pool = new Pool({ connectionString: process.env.DATABASE_URL }); -pool.query(fs.readFileSync('$f', 'utf8')) - .then(() => { console.log('OK'); pool.end(); }) - .catch(e => { console.error(e.message); pool.end(); process.exit(0); }); - " -done -echo "All migrations done" -``` - -> 💡 Alternatively, you can connect to the Supabase database directly using **Supabase → SQL Editor** and run each file in `drizzle/` one by one. - ---- - -### Step 2.6 — Get your Railway URL - -1. Click on your service -2. Click the **Settings** tab → **Networking** → **Generate Domain** -3. Railway gives you a URL like: `synapsvault-backend-production.up.railway.app` -4. **Save this** — you'll need it for the frontend - ---- - -### Step 2.7 — Verify the deployment - -Open your browser and visit these URLs (replace with your Railway domain): - -``` -# Health check — should return {"status":"ok",...} -https://your-app.up.railway.app/health - -# Catalog — should return {"resources":[],...} (empty until seeded) -https://your-app.up.railway.app/resources - -# Admin stats — needs header -curl https://your-app.up.railway.app/admin/stats \ - -H "X-Admin-Key: " -``` - -If `/health` returns `{"status":"ok"}` — your backend is live. ✅ - -> 📈 Once the backend is live, set up logging, health-check alerting, and dashboards by following **[docs/MONITORING.md](./MONITORING.md)**. - ---- - -## Part 3 — Seed the catalog - -Now add the 3 real resources to the catalog. - -### Step 3.1 — Create Publisher 1 - -```bash -curl -X POST https://your-app.up.railway.app/publishers \ - -H "Content-Type: application/json" \ - -d '{ - "name": "SynapsVault Research Lab", - "email": "research@synapsvault.demo", - "walletAddress": "GDEJJLXV2CNHF5HEGX34MU4G6L4PRVUAGD4ANF77ZDYTQJKSDSDYVVI6" - }' -``` - -Copy the `apiKey` from the response. It looks like `ak_...`. **Save it.** - ---- - -### Step 3.2 — Create Publisher 2 - -```bash -curl -X POST https://your-app.up.railway.app/publishers \ - -H "Content-Type: application/json" \ - -d '{ - "name": "SynapsVault Data Studio", - "email": "data@synapsvault.demo", - "walletAddress": "GBXL7OEOSVNSESHD7Q2BYVVA2KPQLSSIRLDSY6HFNYPXHCEBKPNCJQKX" - }' -``` - -Copy and save this `apiKey` too. - ---- - -### Step 3.3 — Publish 3 resources - -Replace `PUB1_KEY` and `PUB2_KEY` with the API keys from above. - -```bash -# Resource 1 — Soroban Smart Contract Patterns -curl -X POST https://your-app.up.railway.app/resources \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer PUB1_KEY" \ - -d '{ - "title": "Stellar Soroban Smart Contract Patterns (2025)", - "description": "Production-tested Soroban patterns: access control, time-locks, circuit breakers, multi-sig. Annotated Rust source for each.", - "price": "0.50", - "externalUrl": "https://developers.stellar.org/docs/build/smart-contracts/example-contracts" - }' - -# Resource 2 — Stellar Analytics Dataset -curl -X POST https://your-app.up.railway.app/resources \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer PUB2_KEY" \ - -d '{ - "title": "Stellar Network Analytics — Q1 2025", - "description": "90 days of Stellar on-chain metrics: daily active accounts, payment volume, DEX trades, Soroban invocations. CSV format.", - "price": "1.00", - "externalUrl": "https://dashboard.stellar.org" - }' - -# Resource 3 — x402 Integration Guide -curl -X POST https://your-app.up.railway.app/resources \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer PUB1_KEY" \ - -d '{ - "title": "x402 HTTP Payment Protocol — Integration Guide", - "description": "Step-by-step x402 integration: server middleware, USDC settlement, receipt verification. Node.js + React examples.", - "price": "0.25", - "externalUrl": "https://github.com/x402-org/x402" - }' -``` - ---- - -### Step 3.4 — Verify the catalog - -```bash -curl https://your-app.up.railway.app/resources -``` - -You should see 3 resources in the response. ✅ - ---- - -## Part 4 — Connect the frontend - -### Step 4.1 — Go to your Vercel project - -1. Go to **[vercel.com](https://vercel.com)** → your SynapsVault-frontend project -2. Click **Settings** → **Environment Variables** - -### Step 4.2 — Update VITE_API_URL - -Find the `VITE_API_URL` variable (or add it if missing): - -| Variable | Value | -|---|---| -| `VITE_API_URL` | `https://your-app.up.railway.app` | - -### Step 4.3 — Redeploy - -1. Click **Deployments** tab -2. Click the three dots on the latest deployment → **Redeploy** -3. Wait ~1 minute - -Your frontend now talks to the live backend. Open `https://your-frontend.vercel.app` — you should see 3 resources in the catalog. - ---- - -## Part 5 — Final checks - -Run these to confirm everything works end-to-end: - -```bash -BACKEND="https://your-app.up.railway.app" -ADMIN_KEY="" - -echo "1. Health check" -curl -s $BACKEND/health | python3 -m json.tool - -echo "2. Catalog" -curl -s $BACKEND/resources | python3 -c " -import sys,json -d=json.load(sys.stdin) -items=d.get('resources',d.get('data',[])) -print(f' {len(items)} resources in catalog') -for r in items: print(f' - {r[\"title\"]} @ {r[\"price\"]} USDC') -" - -echo "3. Admin stats" -curl -s $BACKEND/admin/stats -H "X-Admin-Key: $ADMIN_KEY" | python3 -m json.tool - -echo "4. x402 paywall blocks unauthenticated access" -CODE=$(curl -s -o /dev/null -w "%{http_code}" $BACKEND/resources/res_001/access) -echo " Paywall returns HTTP $CODE (expected 402)" -``` - ---- - -## Summary of what you have - -| Component | Status | URL | -|---|---|---| -| Soroban contracts | ✅ Live on testnet | stellar.expert (see below) | -| Backend API | ✅ Live | `https://your-app.up.railway.app` | -| Database | ✅ Supabase Postgres | Managed | -| File storage | ✅ Supabase Storage | `resources` bucket | -| Catalog | ✅ 3 resources | `/resources` | -| Frontend | ✅ Vercel | `https://your-frontend.vercel.app` | - -### Contract IDs (copy these into your SCF submission) - -``` -vault-registry: CBQEIMSRPSRKJJHGOELZTP3CISZVHZ6WPKZTWJMYZXHFXPGHHWFQBD4H -access-lease: CDQZBCEPN6RCJAQLEKB3ZRNLUZXDCCFMVIS6STZNAOEAQLXD3FUBNQGX -subscription: CCCE6Q6WHDICGMQWXXXMM6X7YDGK3BXU4JNQJNSIA6XQRC52MQL42CD6 -``` - -### Stellar Expert links (put these in your SCF submission) - -- [vault-registry](https://stellar.expert/explorer/testnet/contract/CBQEIMSRPSRKJJHGOELZTP3CISZVHZ6WPKZTWJMYZXHFXPGHHWFQBD4H) -- [access-lease](https://stellar.expert/explorer/testnet/contract/CDQZBCEPN6RCJAQLEKB3ZRNLUZXDCCFMVIS6STZNAOEAQLXD3FUBNQGX) -- [subscription](https://stellar.expert/explorer/testnet/contract/CCCE6Q6WHDICGMQWXXXMM6X7YDGK3BXU4JNQJNSIA6XQRC52MQL42CD6) - ---- - -## Troubleshooting - -| Problem | Fix | -|---|---| -| Railway build fails | Check the build log — usually a missing env var or npm install error | -| `config_invalid` in server log | A required env var is missing — check the list in Part 2 and the canonical reference in [docs/ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) | -| `/health` returns 502 | Server crashed on startup — open Railway → your service → Logs | -| Database connection error | Check `DATABASE_URL` — make sure the password is correct and not URL-encoded twice | -| Supabase connection refused | Supabase free tier sleeps after inactivity — visit the dashboard to wake it | -| `curl: (7) Failed to connect` | The service isn't running — check Logs in Railway | -| Publishers 409 conflict | Publisher email already exists — use a different email or skip | -| Resources publish fails | Check that your `Authorization: Bearer` key is correct | - ---- - -*If you get stuck on any step, open an issue at https://github.com/SynapsVault/backend/issues* +# SynapsVault Backend — Deployment Guide + +> **This file has been consolidated into the canonical Railway deployment guide.** +> +> Please read: **[docs/railway-deploy.md](./railway-deploy.md)** +> +> That guide covers everything that was previously in this file: +> - Supabase project setup (database + storage bucket) +> - Railway project creation (dashboard and CLI) +> - Environment variable reference (required and optional) +> - Running database migrations +> - Verifying the deployment +> - Connecting the Vercel frontend +> - Seeding the catalog +> - Costs, troubleshooting, and monitoring links + +Keeping a single canonical guide avoids the differing instructions that +previously existed between this file, `docs/railway-deploy.md`, and +`README.md`. All three now point to `docs/railway-deploy.md`. diff --git a/docs/railway-deploy.md b/docs/railway-deploy.md index 3511dc9..8173f68 100644 --- a/docs/railway-deploy.md +++ b/docs/railway-deploy.md @@ -1,160 +1,267 @@ -# Deploy SynapsVault Backend to Railway (Free Tier) +# Deploy SynapsVault Backend to Railway -Railway gives you a free $5/month credit — enough to run the backend 24/7 for testnet demos. +> **This is the canonical Railway deployment guide.** +> All other mentions of Railway in this repository link here. +> +> Other deployment targets: +> - **Local (Docker Compose):** see [README.md](../README.md#local-development-docker-compose) and [`docker-compose.yml`](../docker-compose.yml). +> - **Production (Kubernetes):** see [`docs/DEPLOY-KUBERNETES.md`](./DEPLOY-KUBERNETES.md). +> - **Database migrations and rollback:** see [`docs/DATABASE-ROLLBACK.md`](./DATABASE-ROLLBACK.md). -> **Other deployment paths** -> - **Local development / self-hosted:** see [docker-compose.yml](../docker-compose.yml) and the local setup notes in [README.md](../README.md). -> - **Production (Kubernetes):** see [docs/DEPLOYMENT.md](./DEPLOYMENT.md) for the Kubernetes manifests, scaling, and production hardening guidance. -> - **Database migrations & rollback:** see [docs/DATABASE-ROLLBACK.md](./DATABASE-ROLLBACK.md) for rollback procedures. This guide only covers applying forward migrations. +--- + +## Overview + +**Total time: ~20 minutes** (mostly waiting for the first build) + +``` +Part 1 Set up Supabase (~8 min) +Part 2 Deploy backend to Railway (~10 min) +Part 3 Run database migrations (~2 min) +Part 4 Verify and connect frontend (~2 min) +``` -## 1. Prerequisites +At the end you will have: -- Railway account at [railway.app](https://railway.app) (GitHub login) -- `SynapsVault-backend` pushed to `github.com/SynapsVault/backend` ✅ (already done) -- Supabase project for the database (free tier at [supabase.com](https://supabase.com)) +- A live backend API at `https://.up.railway.app` +- A Postgres database on Supabase (free tier, 500 MB) +- File storage on Supabase (free, 1 GB) +- Auto-deploy on every push to `main` --- -## 2. Set up Supabase (database) +## Prerequisites -1. Go to [supabase.com](https://supabase.com) → New project -2. Name it `synapsvault-testnet`, choose any region -3. Copy the **Connection string** (Settings → Database → Connection string → URI mode) - - Format: `postgres://postgres:[YOUR-DB-PASSWORD]@db.[ref].supabase.co:5432/postgres` -4. Copy the **Service Role Key** (Settings → API → `service_role`) +- [ ] Railway account at [railway.app](https://railway.app) — sign in with GitHub +- [ ] `SynapsVault/backend` pushed to GitHub ✅ +- [ ] Supabase account at [supabase.com](https://supabase.com) +- [ ] Contract IDs from `contract-ids.env` (output of Step 3 in [README.md](../README.md)) +- [ ] Backend wallet keypair (`STELLAR_SECRET_KEY` / `STELLAR_PUBLIC_KEY`) +- [ ] Environment variables validated: + ```bash + cp .env.example .env # fill in your values + node scripts/check-env.js --target railway + ``` --- -## 3. Deploy to Railway +## Part 1 — Supabase (database + storage) -### Option A — Railway Dashboard (easiest) +### 1.1 Create a project -1. [railway.app/new](https://railway.app/new) → **Deploy from GitHub repo** -2. Choose `SynapsVault/backend` -3. Railway auto-detects the `Dockerfile` and builds it -4. Go to **Settings → Domains** → **Generate Domain** - - You'll get a URL like `synapsvault-backend-production.up.railway.app` +1. [supabase.com](https://supabase.com) → **New project** +2. Name: `synapsvault-testnet`, choose a region close to your users +3. Set a strong database password — **write it down** +4. Wait ~2 minutes for provisioning -### Option B — Railway CLI +### 1.2 Get your Database URL -```bash -npm install -g @railway/cli -railway login -railway init # link to new project -railway up # deploy from current directory -railway domain # get your public URL -``` +1. Left sidebar → **Settings** → **Database** +2. Scroll to **Connection string** → select the **URI** tab +3. Copy the string: + ``` + postgres://postgres:[YOUR-DB-PASSWORD]@db.[ref].supabase.co:5432/postgres + ``` +4. This is your `DATABASE_URL`. ---- +> If you forget the password: Settings → Database → **Reset database password**. -## 4. Set environment variables +### 1.3 Get your API credentials -> See [docs/ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) for the full reference of every variable, defaults, and required/optional status. +1. Left sidebar → **Settings** → **API** +2. Copy **Project URL** → this is `SUPABASE_URL` +3. Reveal and copy `service_role` key → this is `SUPABASE_SERVICE_KEY` -In Railway dashboard → your service → **Variables**, add: +### 1.4 Create the storage bucket -```env -# Database -DATABASE_URL=postgres://postgres:[YOUR-DB-PASSWORD]@db.[ref].supabase.co:5432/postgres +1. Left sidebar → **Storage** → **New bucket** +2. Name: `resources` +3. Toggle **Public bucket** → **OFF** (access controlled by the backend) +4. Click **Save** -# Supabase (for file storage) -SUPABASE_URL=https://[ref].supabase.co -SUPABASE_SERVICE_ROLE_KEY=eyJ...your_service_role_key +--- -# Stellar -STELLAR_NETWORK=testnet -STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org -STELLAR_CONTRACT_VAULT_REGISTRY=[from contract-ids.env] -STELLAR_CONTRACT_ACCESS_LEASE=[from contract-ids.env] -STELLAR_CONTRACT_SUBSCRIPTION=[from contract-ids.env] -STELLAR_SECRET_KEY=[BACKEND wallet secret key] +## Part 2 — Deploy to Railway + +### Option A — Dashboard (easiest) -# Security -ADMIN_API_KEY=[generate: openssl rand -hex 32] +1. [railway.app/new](https://railway.app/new) → **Deploy from GitHub repo** +2. Select `SynapsVault/backend` +3. Railway detects the `Dockerfile` and starts building automatically +4. Go to **Settings → Domains → Generate Domain** once the build finishes + - URL format: `synapsvault-backend-production.up.railway.app` + +### Option B — Railway CLI + +```bash +npm install -g @railway/cli +railway login +railway init # link or create a project +railway up # deploy from the current directory +railway domain # print your public URL +``` -# CORS — allow your Vercel frontend -ALLOWED_ORIGINS=https://[your-app].vercel.app,http://localhost:5173 +### Set environment variables + +In the Railway dashboard → your service → **Variables**, add every variable +from the table below. + +> See [docs/ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) for the +> complete reference including defaults, validation rules, and optional +> variables. The table below covers the variables required for this deployment. + +#### Required — the server will not start without these + +| Variable | Value | Source | +|----------|-------|--------| +| `DATABASE_URL` | `postgres://postgres:[PASSWORD]@db.[ref].supabase.co:5432/postgres` | Supabase → Settings → Database → Connection string | +| `SUPABASE_URL` | `https://[ref].supabase.co` | Supabase → Settings → API → Project URL | +| `SUPABASE_SERVICE_KEY` | `eyJ...` (JWT) | Supabase → Settings → API → `service_role` | +| `SUPABASE_STORAGE_BUCKET` | `resources` | Created in Part 1.4 | +| `STELLAR_NETWORK` | `testnet` | Fixed value | +| `NETWORK` | `testnet` | Fixed value | +| `SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | Fixed value | +| `USDC_CONTRACT_ID` | `CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA` | Fixed — testnet USDC | +| `PAY_TO` | `G...` | Your BACKEND wallet public key | +| `AGENT_SECRET_KEY` | `S...` | Your BACKEND wallet secret key | +| `REGISTRY_SECRET_KEY` | `S...` | Same as `AGENT_SECRET_KEY` | +| `STELLAR_CONTRACT_VAULT_REGISTRY` | `C...` | `contract-ids.env` | +| `STELLAR_CONTRACT_ACCESS_LEASE` | `C...` | `contract-ids.env` | +| `STELLAR_CONTRACT_SUBSCRIPTION` | `C...` | `contract-ids.env` | +| `OPENROUTER_API_KEY` | `sk-or-...` | [openrouter.ai](https://openrouter.ai) (free tier available) | +| `ADMIN_API_KEY` | random hex | `openssl rand -hex 32` | + +#### Optional but recommended + +| Variable | Value | Notes | +|----------|-------|-------| +| `NODE_ENV` | `production` | Enables production optimisations | +| `PORT` | `3000` | Railway sets this automatically | +| `ALLOWED_ORIGINS` | `https://[your-app].vercel.app` | Set after you have your Vercel URL | +| `FACILITATOR_URL` | `https://www.x402.org/facilitator` | Default x402 facilitator | +| `STELLAR_HORIZON_URL` | `https://horizon-testnet.stellar.org` | Already defaulted in code | + +#### Copy-paste block (replace `← FILL IN` values) +```env +DATABASE_URL=postgres://postgres:[YOUR-DB-PASSWORD]@db.[YOUR-REF].supabase.co:5432/postgres +SUPABASE_URL=https://[YOUR-REF].supabase.co +SUPABASE_SERVICE_KEY=eyJ... +SUPABASE_STORAGE_BUCKET=resources +STELLAR_NETWORK=testnet +NETWORK=testnet +SOROBAN_RPC_URL=https://soroban-testnet.stellar.org +USDC_CONTRACT_ID=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA +PAY_TO=G... +AGENT_SECRET_KEY=S... +REGISTRY_SECRET_KEY=S... +STELLAR_CONTRACT_VAULT_REGISTRY=C... +STELLAR_CONTRACT_ACCESS_LEASE=C... +STELLAR_CONTRACT_SUBSCRIPTION=C... +OPENROUTER_API_KEY=sk-or-... +ADMIN_API_KEY=← FILL IN (openssl rand -hex 32) NODE_ENV=production PORT=3000 +FACILITATOR_URL=https://www.x402.org/facilitator ``` ---- +After adding all variables Railway automatically redeploys. -## 5. Run database migrations +--- -> This section covers **forward migrations only**. For rollback procedures (failed migration, bad deploy, data recovery), see [docs/DATABASE-ROLLBACK.md](./DATABASE-ROLLBACK.md). +## Part 3 — Run database migrations -After deploy, open Railway **Shell** tab: +> This section covers **forward migrations only**. For rollback procedures, see +> [docs/DATABASE-ROLLBACK.md](./DATABASE-ROLLBACK.md). -```bash -node -e " -const { drizzle } = require('drizzle-orm/node-postgres'); -const { migrate } = require('drizzle-orm/node-postgres/migrator'); -const { Pool } = require('pg'); -const pool = new Pool({ connectionString: process.env.DATABASE_URL }); -migrate(drizzle(pool), { migrationsFolder: './drizzle' }).then(() => { - console.log('Migrations done'); process.exit(0); -}).catch(err => { console.error(err); process.exit(1); }); -" -``` +1. In the Railway dashboard, click on your service → **Shell** tab +2. Run: + ```bash + pnpm drizzle-kit migrate + ``` -Or via the npm script (if you add one to package.json): -```bash -pnpm drizzle-kit migrate -``` +Alternatively, connect to Supabase directly (Settings → SQL Editor) and apply +each file in the `drizzle/` directory in order. --- -## 6. Verify deployment +## Part 4 — Verify and connect the frontend -> For ongoing health checks, metrics, and alerting, see [docs/MONITORING.md](./MONITORING.md). +### Verify the backend ```bash -# Health check -curl https://[your-app].up.railway.app/health +RAILWAY_URL=https://your-app.up.railway.app -# Should return: -# {"status":"ok","version":"...","uptime":...} +# Health check — should return {"status":"ok",...} +curl "$RAILWAY_URL/health" -# Catalog (empty at first) -curl https://[your-app].up.railway.app/resources +# Catalog — should return {"resources":[]} until seeded +curl "$RAILWAY_URL/resources" # Admin stats -curl https://[your-app].up.railway.app/admin/stats \ - -H "X-Admin-Key: your_admin_key" +curl "$RAILWAY_URL/admin/stats" -H "X-Admin-Key: your_admin_key" ``` ---- +If `/health` returns `{"status":"ok"}` the backend is live. ✅ -## 7. Connect frontend +### Connect the frontend Update your Vercel environment variables: ```env -VITE_API_URL=https://[your-app].up.railway.app +VITE_API_URL=https://your-app.up.railway.app ``` -Redeploy frontend → Vercel dashboard → **Redeploy**. +Redeploy in the Vercel dashboard: **Deployments → latest → Redeploy**. --- -## 8. Seed the catalog +## Seeding the catalog + +Run the seed script from the `deploy` repo: ```bash -BACKEND_URL=https://[your-app].up.railway.app \ +BACKEND_URL=https://your-app.up.railway.app \ +PUBLISHER1_PUBLIC=G[PUBLISHER1] \ +PUBLISHER2_PUBLIC=G[PUBLISHER2] \ node scripts/05-seed-catalog.js ``` +This idempotently publishes the 3 catalog resources and exits non-zero if any +step fails. + --- -## Estimated costs (testnet phase) +## Costs (testnet phase) | Service | Plan | Monthly cost | |---------|------|-------------| -| Railway | Hobby (free $5 credit) | $0 | +| Railway | Hobby ($5 credit) | $0 | | Supabase | Free tier (500 MB DB, 1 GB storage) | $0 | | Vercel | Hobby (free) | $0 | | **Total** | | **$0** | -The free tiers are sufficient for an SCF testnet demo with <100 daily active users. +The free tiers are sufficient for an SCF testnet demo with fewer than 100 daily +active users. + +--- + +## Troubleshooting + +| Symptom | Fix | +|---------|-----| +| Build fails | Check the Railway build log — usually a missing env var or npm install error | +| `config_invalid` in server log | A required env var is missing — check the variables table above and [docs/ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) | +| `/health` returns 502 | Server crashed on startup — Railway → your service → **Logs** | +| Database connection error | Verify `DATABASE_URL` — ensure the password is not URL-encoded twice | +| Supabase connection refused | The Supabase free tier sleeps after inactivity — open the Supabase dashboard to wake it | +| `curl: (7) Failed to connect` | Service not running — check Railway Logs | +| `REPLACE_ME_*` / `change-me-*` errors | Run `node scripts/check-env.js --target railway` to find unfilled placeholders | +| Migrations fail | Confirm `DATABASE_URL` resolves to the correct Supabase project | + +--- + +## Monitoring + +Once the backend is live, set up health-check alerting, dashboards, and +on-call procedures by following [docs/MONITORING.md](./MONITORING.md). diff --git a/package.json b/package.json index e8a857b..6531af7 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,13 @@ "name": "handsoff", "version": "0.1.0", "private": true, + "type": "module", "scripts": { - "check:links": "markdown-link-check -q -p README.md && markdown-link-check -q -r docs" + "check:env": "node scripts/check-env.js", + "check:env:compose": "node scripts/check-env.js --target compose", + "check:env:k8s": "node scripts/check-env.js --target k8s", + "check:env:railway": "node scripts/check-env.js --target railway", + "check:links": "markdown-link-check -q -p README.md && markdown-link-check -q -r docs", "step1": "node scripts/01-generate-keypairs.js", "step2": "node scripts/02-fund-accounts.js", "step3": "bash scripts/03-deploy-contracts.sh", diff --git a/scripts/check-env.js b/scripts/check-env.js new file mode 100644 index 0000000..19fc792 --- /dev/null +++ b/scripts/check-env.js @@ -0,0 +1,354 @@ +#!/usr/bin/env node +/** + * scripts/check-env.js + * + * Validates that all required environment variables for a given deployment + * target are present and have been filled in (no placeholder values). + * + * Usage: + * node scripts/check-env.js [--target ] [--env-file ] + * + * Options: + * --target Deployment target to validate (default: compose) + * compose — local Docker Compose stack + * k8s — Kubernetes / Kustomize deploy + * railway — Railway backend deploy + * --env-file Path to the .env file to check (default: .env) + * --help Print this help message + * + * Exit codes: + * 0 All required variables are set and have non-placeholder values + * 1 One or more variables are missing or contain placeholder values + * + * Placeholder patterns detected: + * - Keys that match S[XXX...] (Stellar secret key template) + * - Keys that match C[XXX...] (Contract ID template) + * - Keys that match G[XXX...] (Stellar public key template) + * - Values starting with REPLACE_ME + * - Values starting with change-me + * - Values starting with your- + * - Values equal to a well-known .env.example default that must be replaced + * (e.g. "your-supabase-anon-key", "your-openrouter-api-key") + * + * This script only reads from the specified --env-file and the current process + * environment (process.env). It never mutates either. + */ + +import { readFileSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// --------------------------------------------------------------------------- +// Placeholder detection +// --------------------------------------------------------------------------- + +/** + * Patterns whose presence in a value (case-insensitive prefix/substring match) + * indicates the value has not been filled in. + * + * @type {Array} + */ +const PLACEHOLDER_PATTERNS = [ + /^REPLACE_ME/i, + /^change-me/i, + /^your-/i, + // Stellar secret key placeholder: S followed by 55 uppercase Xs (or just many Xs) + /^S[X]{10,}/, + // Contract/account ID placeholder: C or G followed by 55 uppercase Xs + /^[CG][X]{10,}/, + // Literal placeholder text embedded in docs + /^\[YOUR-/i, + /^ re.test(value)); +} + +// --------------------------------------------------------------------------- +// Required variable definitions per target +// --------------------------------------------------------------------------- + +/** + * @typedef {Object} VarSpec + * @property {string} name Environment variable name + * @property {string} description Human-readable description (shown in errors) + * @property {boolean} [secret] True if the value is a secret (masked in output) + */ + +/** @type {VarSpec[]} */ +const COMPOSE_VARS = [ + { name: 'POSTGRES_USER', description: 'PostgreSQL username for the compose stack' }, + { name: 'POSTGRES_PASSWORD', description: 'PostgreSQL password for the compose stack' }, + { name: 'POSTGRES_DB', description: 'PostgreSQL database name' }, + { name: 'STELLAR_NETWORK', description: 'Stellar network (testnet/mainnet)' }, + { name: 'SOROBAN_RPC_URL', description: 'Soroban RPC endpoint URL' }, + { name: 'STELLAR_SECRET_KEY', description: 'Stellar secret key for signing transactions', secret: true }, + { name: 'STELLAR_PUBLIC_KEY', description: 'Stellar public key matching STELLAR_SECRET_KEY' }, + { name: 'SUPABASE_URL', description: 'Supabase project URL' }, + { name: 'SUPABASE_ANON_KEY', description: 'Supabase anonymous/public key', secret: true }, + { name: 'SUPABASE_SERVICE_KEY', description: 'Supabase service role key (backend only)', secret: true }, + { name: 'ADMIN_API_KEY', description: 'Admin API key for protected endpoints', secret: true }, + { name: 'OPENROUTER_API_KEY', description: 'OpenRouter API key for LLM access', secret: true }, + { name: 'AGENT_SECRET_KEY', description: 'Stellar secret key for the agent signer', secret: true }, + { name: 'REGISTRY_SECRET_KEY', description: 'Stellar secret key for the registry signer', secret: true }, +]; + +/** @type {VarSpec[]} */ +const K8S_VARS = [ + ...COMPOSE_VARS, + { name: 'STELLAR_CONTRACT_VAULT_REGISTRY', description: 'Deployed vault-registry contract ID' }, + { name: 'STELLAR_CONTRACT_ACCESS_LEASE', description: 'Deployed access-lease contract ID' }, + { name: 'STELLAR_CONTRACT_SUBSCRIPTION', description: 'Deployed subscription contract ID' }, + { name: 'USDC_CONTRACT_ID', description: 'USDC token contract ID' }, +]; + +/** @type {VarSpec[]} */ +const RAILWAY_VARS = [ + { name: 'DATABASE_URL', description: 'Supabase/Railway Postgres connection string', secret: true }, + { name: 'SUPABASE_URL', description: 'Supabase project URL' }, + { name: 'SUPABASE_SERVICE_KEY', description: 'Supabase service role key', secret: true }, + { name: 'SUPABASE_STORAGE_BUCKET', description: 'Supabase storage bucket name (should be "resources")' }, + { name: 'STELLAR_NETWORK', description: 'Stellar network (testnet/mainnet)' }, + { name: 'SOROBAN_RPC_URL', description: 'Soroban RPC endpoint URL' }, + { name: 'USDC_CONTRACT_ID', description: 'USDC token contract ID' }, + { name: 'PAY_TO', description: 'Backend wallet public key (Stellar G...)' }, + { name: 'AGENT_SECRET_KEY', description: 'Backend wallet secret key', secret: true }, + { name: 'REGISTRY_SECRET_KEY', description: 'Registry wallet secret key', secret: true }, + { name: 'STELLAR_CONTRACT_VAULT_REGISTRY', description: 'Deployed vault-registry contract ID' }, + { name: 'STELLAR_CONTRACT_ACCESS_LEASE', description: 'Deployed access-lease contract ID' }, + { name: 'STELLAR_CONTRACT_SUBSCRIPTION', description: 'Deployed subscription contract ID' }, + { name: 'OPENROUTER_API_KEY', description: 'OpenRouter API key', secret: true }, + { name: 'ADMIN_API_KEY', description: 'Admin API key for protected endpoints', secret: true }, +]; + +/** @type {Record} */ +const TARGETS = { + compose: COMPOSE_VARS, + k8s: K8S_VARS, + railway: RAILWAY_VARS, +}; + +// --------------------------------------------------------------------------- +// .env file parser +// --------------------------------------------------------------------------- + +/** + * Parse a .env file into a key→value map. Handles: + * - Comments (#) + * - Quoted values ("..." or '...') + * - KEY=VALUE and KEY= (empty) + * - Blank lines + * + * @param {string} filePath + * @returns {Record} + */ +export function parseEnvFile(filePath) { + if (!existsSync(filePath)) return {}; + const content = readFileSync(filePath, 'utf8'); + /** @type {Record} */ + const env = {}; + for (const rawLine of content.split('\n')) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + const eqIdx = line.indexOf('='); + if (eqIdx === -1) continue; + const key = line.slice(0, eqIdx).trim(); + let value = line.slice(eqIdx + 1).trim(); + // Strip inline comments (e.g. VALUE=foo # comment) + // only if the value is not quoted + if (!value.startsWith('"') && !value.startsWith("'")) { + const commentIdx = value.indexOf(' #'); + if (commentIdx !== -1) value = value.slice(0, commentIdx).trim(); + } + // Strip surrounding quotes + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + env[key] = value; + } + return env; +} + +// --------------------------------------------------------------------------- +// Validation +// --------------------------------------------------------------------------- + +/** + * @typedef {Object} ValidationResult + * @property {boolean} ok + * @property {string[]} missing Variables not set at all + * @property {string[]} placeholder Variables set to a placeholder value + */ + +/** + * Validate a set of variable specs against the merged env (file + process). + * + * @param {VarSpec[]} specs + * @param {Record} fileEnv + * @returns {ValidationResult} + */ +export function validateEnv(specs, fileEnv) { + const missing = []; + const placeholder = []; + + for (const spec of specs) { + // process.env takes precedence over the file (mirrors how dotenv works in apps) + const value = process.env[spec.name] ?? fileEnv[spec.name]; + if (value === undefined || value === null || value === '') { + missing.push(spec.name); + } else if (isPlaceholder(value)) { + placeholder.push(spec.name); + } + } + + return { + ok: missing.length === 0 && placeholder.length === 0, + missing, + placeholder, + }; +} + +// --------------------------------------------------------------------------- +// CLI argument parsing +// --------------------------------------------------------------------------- + +/** + * Parse process.argv into an options object. + * + * @param {string[]} argv process.argv.slice(2) + * @returns {{ target: string, envFile: string, help: boolean }} + */ +export function parseArgs(argv) { + let target = 'compose'; + let envFile = '.env'; + let help = false; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--help' || arg === '-h') { + help = true; + } else if (arg === '--target' || arg === '-t') { + target = argv[++i] ?? target; + } else if (arg === '--env-file') { + envFile = argv[++i] ?? envFile; + } else if (arg.startsWith('--target=')) { + target = arg.split('=')[1]; + } else if (arg.startsWith('--env-file=')) { + envFile = arg.split('=')[1]; + } + } + + return { target, envFile, help }; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +const HELP_TEXT = ` +check-env.js — Validate .env variables for a deployment target + +Usage: + node scripts/check-env.js [--target ] [--env-file ] + +Options: + --target Deployment target (default: compose) + compose — local Docker Compose stack + k8s — Kubernetes / Kustomize deploy + railway — Railway backend deploy + --env-file Path to .env file (default: .env) + --help Print this help message + +Exit codes: + 0 All checks passed + 1 One or more variables are missing or contain placeholder values +`.trim(); + +/** + * Run the CLI. Exported so it can be stubbed in tests. + * + * @param {string[]} argv Argument array (default: process.argv.slice(2)) + * @param {string} [cwd] Working directory for resolving --env-file (default: process.cwd()) + * @returns {number} Exit code + */ +export function run(argv = process.argv.slice(2), cwd = process.cwd()) { + const { target, envFile, help } = parseArgs(argv); + + if (help) { + console.log(HELP_TEXT); + return 0; + } + + const validTargets = Object.keys(TARGETS); + if (!validTargets.includes(target)) { + console.error(`check-env: unknown target "${target}". Valid targets: ${validTargets.join(', ')}`); + return 1; + } + + const resolvedEnvFile = resolve(cwd, envFile); + const fileEnv = parseEnvFile(resolvedEnvFile); + const specs = TARGETS[target]; + + const { ok, missing, placeholder } = validateEnv(specs, fileEnv); + + const targetLabel = target.toUpperCase(); + + if (ok) { + console.log(`✅ check-env [${targetLabel}]: all ${specs.length} required variables are set`); + return 0; + } + + // Report errors + console.error(`\n❌ check-env [${targetLabel}]: environment validation failed\n`); + + if (missing.length > 0) { + console.error(`Missing variables (not set in ${envFile} or process environment):`); + for (const name of missing) { + const spec = specs.find((s) => s.name === name); + console.error(` • ${name}${spec?.description ? ` — ${spec.description}` : ''}`); + } + console.error(''); + } + + if (placeholder.length > 0) { + console.error(`Placeholder values (must be replaced before deploying):`); + for (const name of placeholder) { + const spec = specs.find((s) => s.name === name); + const rawValue = process.env[name] ?? fileEnv[name] ?? ''; + // Mask secrets in output so real values are never echoed in CI logs + const displayValue = spec?.secret + ? rawValue.slice(0, 4) + '****' + : rawValue.length > 50 + ? rawValue.slice(0, 50) + '…' + : rawValue; + console.error(` • ${name}="${displayValue}"${spec?.description ? ` — ${spec.description}` : ''}`); + } + console.error(''); + } + + console.error( + `Fix these values in ${envFile} (copy from .env.example and fill in real values),\n` + + `then re-run: node scripts/check-env.js --target ${target}\n` + ); + + return 1; +} + +// Run when invoked directly (not imported as a module) +if (process.argv[1] && process.argv[1].endsWith('check-env.js')) { + process.exit(run()); +}