Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
76bb341
feat: harden resilience, database constraints, trace auditing and cop…
mhikel66 Oct 17, 2026
2e1b725
Merge pull request #160 from mhikel66/fix/issues-148-149-150-151
Levi-Ojukwu Oct 17, 2026
444edfa
feat: add seeded snapshots, backups, secret rotation and CD
thegreatfeez Oct 17, 2026
16f713b
test: integration-tier infrastructure (Testcontainers, per-file DBs, …
choniszn1 Oct 17, 2026
026c1fe
test: auth, payments and isolation integration suites (#135)
choniszn1 Oct 17, 2026
3c4ace0
ci, docs: run and document the integration tier (#135)
choniszn1 Oct 17, 2026
c1db856
feat(db): add trace_publications queue table and status enum (#131)
abayomiwav Oct 17, 2026
d443df1
feat(traces): queue trace publication until the market is on-chain an…
abayomiwav Oct 17, 2026
c046882
test+docs(traces): cover the publication queue; document lifecycle an…
abayomiwav Oct 17, 2026
958f7cf
feat(db): add idempotency_keys table
chonilius Oct 17, 2026
1fadf5b
feat(api): Idempotency-Key middleware on state-changing routes
chonilius Oct 17, 2026
8017c6f
fix(payments): resolve a concurrent duplicate txHash instead of a 500
chonilius Oct 17, 2026
7941d5c
test,docs: cover Idempotency-Key behaviour and document it
chonilius Oct 17, 2026
1cc8d3e
Merge pull request #167 from chonilius/feat/123-idempotency-key
Levi-Ojukwu Oct 17, 2026
1d655fe
Merge pull request #165 from choniszn1/test/135-integration-tier
Levi-Ojukwu Oct 17, 2026
fcbd340
Merge branch 'develop' into feat/131-trace-publication-queue
abayomiwav Oct 17, 2026
673e0e9
Merge pull request #166 from abayomiwav/feat/131-trace-publication-queue
Levi-Ojukwu Oct 17, 2026
f7e05ab
Merge branch 'develop' into feat/oracledesk-147-146-145-144
thegreatfeez Oct 17, 2026
3f5ed86
Merge pull request #164 from thegreatfeez/feat/oracledesk-147-146-145…
Levi-Ojukwu Oct 17, 2026
7926059
ci: lint new migrations for lock safety with allowlist
harystyleseze Oct 17, 2026
e94c73e
feat(health): add livez, dependency-aware readyz and admin status
vjuliaife Oct 17, 2026
a2a221b
feat: propagate request and job correlation ids through logs, agent l…
hardcordev Oct 17, 2026
fc5dfd7
Merge pull request #171 from vjuliaife/feat/health-probes-111
Levi-Ojukwu Oct 17, 2026
8cb2009
Merge pull request #169 from harystyleseze/feat/migration-lint-gate
Levi-Ojukwu Oct 17, 2026
7c61687
Merge branch 'develop' into feat/correlation-ids-110
hardcordev Oct 17, 2026
ced038e
Merge pull request #170 from hardcordev/feat/correlation-ids-110
Levi-Ojukwu Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ PORT=8000
# Postgres and Redis. These defaults match docker-compose.yml (local only,
# not secrets). Point them at your own servers for anything shared.
DATABASE_URL=postgresql://oracledesk:oracledesk@127.0.0.1:5433/oracledesk
# Alternatively, mount the URL from a file: DATABASE_URL_FILE=/run/secrets/database_url
# Every secret below also accepts <NAME>_FILE (env/file provider) — see #145.

# ── Secret loading (#145) ──────────────────────────────────────────────────
# env: process environment (default); file: <NAME>_FILE mounts; vault: Vault KV v2.
SECRETS_PROVIDER=env
# Refresh interval for file/vault providers (minimum 30000 ms).
SECRETS_REFRESH_MS=300000
# How long a rotated JWT_SECRET remains valid for already-issued tokens.
JWT_PREVIOUS_SECRET_GRACE_SECS=86400
# Required only when SECRETS_PROVIDER=vault.
VAULT_ADDR=
VAULT_TOKEN_FILE=
VAULT_KV_PATH=
REDIS_URL=redis://127.0.0.1:6379
# Comma-separated browser origins allowed to call the API.
CORS_ORIGINS=http://localhost:3000
Expand Down Expand Up @@ -60,10 +74,24 @@ RESOLUTION_SIGNERS=
RESOLUTION_THRESHOLD=1
RESOLUTION_DISPUTE_WINDOW_SECS=86400

# Accept a well-formed inbound X-Request-Id; set false without a trusted proxy.
TRUST_INBOUND_REQUEST_ID=true

# Event indexer (Stellar RPC getEvents poller).
INDEXER_ENABLED=true
INDEXER_POLL_MS=5000

# Health probes (#111): /livez (process only) and /readyz (dependencies).
# HEALTH_CRITICAL: comma list of database,redis,rpc,indexer that fail readiness.
HEALTH_CRITICAL=database,redis
HEALTH_PROBE_TIMEOUT_MS=1500
HEALTH_CACHE_TTL_MS=2000
INDEXER_MAX_LAG_LEDGERS=120
INDEXER_MAX_STALENESS_MS=60000
LIVENESS_MAX_LOOP_DELAY_MS=2000
# Drain + close must fit the 15 s forced-exit window.
SHUTDOWN_DRAIN_MS=5000

# ── LLM providers (market and trace generation) ────────────────────────────
ANTHROPIC_API_KEY=
GEMINI_API_KEY=
Expand All @@ -72,7 +100,64 @@ GEMINI_API_KEY=
PINATA_API_KEY=
PINATA_SECRET_API_KEY=
IPFS_GATEWAY_URL=https://gateway.pinata.cloud/ipfs
# pinata (default) or local (development only; stores bytes in .ipfs-local/).
IPFS_TRANSPORT=pinata
IPFS_LOCAL_DIR=.ipfs-local

# ── Data sources ───────────────────────────────────────────────────────────
NEWSAPI_KEY=
FRED_API_KEY=

# ── Resilience & Timeouts ──────────────────────────────────────────────────
TIMEOUT_RPC_READ_MS=8000
TIMEOUT_RPC_WRITE_MS=45000
TIMEOUT_RPC_EVENTS_MS=15000
TIMEOUT_LLM_MS=60000
TIMEOUT_IPFS_PIN_MS=20000
TIMEOUT_IPFS_FETCH_MS=15000
TIMEOUT_DATASOURCE_MS=10000

# Concurrency bulkheads
BULKHEAD_LLM=2
BULKHEAD_IPFS=4
BULKHEAD_RPC_READ=16
BULKHEAD_RPC_WRITE=4
BULKHEAD_RPC_EVENTS=4
BULKHEAD_DATASOURCE=8
BULKHEAD_QUEUE=32

# Deadlines & caps
HTTP_REQUEST_TIMEOUT_MS=30000
CYCLE_DEADLINE_MS=600000
IPFS_MAX_BYTES=2097152

# ── Trace Audit (Registry Verification) ────────────────────────────────────
TRACE_AUDIT_ENABLED=true
TRACE_AUDIT_CRON=40 */2 * * *
TRACE_AUDIT_RECHECK_HOURS=168
TRACE_AUDIT_MAX_RPC=500
TRACE_AUDIT_MAX_FETCHES=100
TRACE_AUDIT_MAX_BYTES=2097152

# Trace publication queue (#131): pins traces, waits for the market to be
# on-chain, publishes once, and confirms via the indexer's TracePublished event.
TRACE_PUBLISH_ENABLED=true
TRACE_PUBLISH_CRON=*/30 * * * * *
TRACE_PUBLISH_MAX_ATTEMPTS=8
TRACE_CONFIRM_TIMEOUT_MS=600000

# ── Copy Trading ───────────────────────────────────────────────────────────
COPY_TRADE_TTL_MINUTES=30
COPY_TRADE_MAX_PENDING=5

# ── Seed / snapshot (#147) ─────────────────────────────────────────────────
# The seed refuses production and non-local databases. Set to 1 only when you
# intentionally seed a remote development database.
SEED_ALLOW_REMOTE=0
# Base id for seeded on-chain market/trace rows, and the starting ledger.
SEED_ONCHAIN_BASE=1000000
SEED_BASE_LEDGER=6000000
# Optional comma-separated real testnet ids to link (read-only).
SEED_TESTNET_MARKET_IDS=
# Required by scripts/snapshot-export.ts; never commit the value.
SNAPSHOT_SALT=
135 changes: 135 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: CD

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
packages: write
id-token: write
pull-requests: read

concurrency:
group: cd-main
cancel-in-progress: false

jobs:
verify:
name: Verify release commit
uses: ./.github/workflows/ci.yml

image:
name: Build, sign and attest image
needs: verify
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
run: echo "tag=ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
- id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tag }}
provenance: true
sbom: true
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign image
run: cosign sign --yes "${{ steps.meta.outputs.tag }}@${{ steps.build.outputs.digest }}"
- name: SBOM (SPDX)
uses: anchore/sbom-action@v0
with:
image: ${{ steps.meta.outputs.tag }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json

staging:
name: Deploy staging
needs: image
uses: ./.github/workflows/deploy-env.yml
with:
environment: staging
namespace: oracledesk-staging
image: ghcr.io/${{ github.repository }}@${{ needs.image.outputs.digest }}
digest: ${{ needs.image.outputs.digest }}
values_file: values-staging.yaml
secrets: inherit

staging-checks:
name: Staging post-deploy checks
needs: [image, staging]
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- name: Checks
env:
API_URL: ${{ vars.API_URL }}
READY_CHECKS: "3"
INDEXER_STATUS_URL: ${{ vars.INDEXER_STATUS_URL }}
MAX_INDEXER_LAG_LEDGERS: "120"
PROMETHEUS_URL: ${{ vars.PROMETHEUS_URL }}
KUBE_NAMESPACE: oracledesk-staging
DEPLOYMENTS: api indexer worker
DIGEST: ${{ needs.image.outputs.digest }}
KUBECONFIG_DATA: ${{ secrets.KUBECONFIG }}
run: |
echo "$KUBECONFIG_DATA" > "$RUNNER_TEMP/kubeconfig"
export KUBECONFIG="$RUNNER_TEMP/kubeconfig"
./scripts/post-deploy-checks.sh

production:
name: Deploy production
needs: [image, staging-checks]
uses: ./.github/workflows/deploy-env.yml
with:
environment: production
namespace: oracledesk
image: ghcr.io/${{ github.repository }}@${{ needs.image.outputs.digest }}
digest: ${{ needs.image.outputs.digest }}
values_file: values-production.yaml
secrets: inherit

production-checks:
name: Production post-deploy checks
needs: [image, production]
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- name: Checks
env:
API_URL: ${{ vars.API_URL }}
READY_CHECKS: "5"
INDEXER_STATUS_URL: ${{ vars.INDEXER_STATUS_URL }}
MAX_INDEXER_LAG_LEDGERS: "120"
PROMETHEUS_URL: ${{ vars.PROMETHEUS_URL }}
KUBE_NAMESPACE: oracledesk
DEPLOYMENTS: api indexer worker
DIGEST: ${{ needs.image.outputs.digest }}
KUBECONFIG_DATA: ${{ secrets.KUBECONFIG }}
run: |
echo "$KUBECONFIG_DATA" > "$RUNNER_TEMP/kubeconfig"
export KUBECONFIG="$RUNNER_TEMP/kubeconfig"
./scripts/post-deploy-checks.sh
31 changes: 30 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ name: CI

on:
push:
branches: [main]
branches: [main, develop]
pull_request:
workflow_call:

jobs:
backend:
Expand Down Expand Up @@ -53,5 +54,33 @@ jobs:

- run: npx prisma generate
- run: npx prisma migrate deploy
- name: Verify schema datamodel matches migrations
run: npx prisma migrate diff --from-migrations prisma/migrations --to-schema-datamodel prisma/schema.prisma --exit-code
- name: Audit database constraints
run: npx tsx scripts/audit-constraints.ts --json audit.json
- run: npm run build
- run: npm test
# Integration tier (#135): real Postgres 16 + Redis 7 from the service
# containers above. TEST_DATABASE_URL is the admin DB, used to clone a
# migrated template database per test file.
- name: Integration tests
run: npm run test:integration
env:
TEST_DATABASE_URL: postgresql://oracledesk:oracledesk@localhost:5432/postgres
TEST_REDIS_URL: redis://localhost:6379

migrations:
name: Migration lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc

- name: Lint new migrations
run: npm run lint:migrations
80 changes: 80 additions & 0 deletions .github/workflows/deploy-env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Deploy environment

on:
workflow_call:
inputs:
environment:
required: true
type: string
namespace:
required: true
type: string
image:
required: true
type: string
digest:
required: true
type: string
values_file:
required: true
type: string
secrets:
KUBECONFIG:
required: true
DATABASE_URL:
required: true
HELM_VALUES:
required: false

permissions:
contents: read
id-token: write

jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- uses: azure/setup-helm@v4
- name: kubeconfig
run: |
echo "${{ secrets.KUBECONFIG }}" > "$RUNNER_TEMP/kubeconfig"
echo "KUBECONFIG=$RUNNER_TEMP/kubeconfig" >> "$GITHUB_ENV"
- name: Verify image signature
uses: sigstore/cosign-installer@v3
- run: cosign verify "${{ inputs.image }}" --certificate-identity-regexp "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/cd.yml@refs/heads/main" --certificate-oidc-issuer https://token.actions.githubusercontent.com
- name: Migration lint (changed migrations only)
run: |
changed="$(git diff --name-only HEAD^ HEAD -- prisma/migrations || true)"
if [[ -n "$changed" ]]; then
printf '%s\n' "$changed" | xargs node scripts/check-migration-compat.mjs
else
echo "No migration files changed in this release"
fi
- name: Apply migrations
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npx prisma migrate deploy
- name: Helm upgrade
env:
HELM_VALUES: ${{ secrets.HELM_VALUES }}
NAMESPACE: ${{ inputs.namespace }}
IMAGE: ${{ inputs.image }}
VALUES_FILE: ${{ inputs.values_file }}
run: |
test -d deploy/helm/oracledesk-backend || { echo "Helm chart missing (deploy/helm/oracledesk-backend)"; exit 1; }
extra=()
if [[ -n "$HELM_VALUES" ]]; then printf '%s' "$HELM_VALUES" > "$RUNNER_TEMP/values-secret.yaml"; extra=(-f "$RUNNER_TEMP/values-secret.yaml"); fi
helm upgrade --install oracledesk deploy/helm/oracledesk-backend \
--namespace "$NAMESPACE" --create-namespace \
-f "deploy/helm/oracledesk-backend/$VALUES_FILE" "${extra[@]}" \
--set image.repository="${IMAGE%@*}" --set image.digest="${IMAGE#*@}" \
--atomic --timeout 10m
Loading
Loading