From 018a56bf7e92884cf04b13bd06d13cf886c71ca9 Mon Sep 17 00:00:00 2001 From: asus-backup Date: Mon, 5 Oct 2026 23:15:29 +0200 Subject: [PATCH] WIP snapshot asus-20261005 (Documents/0pt) --- .env.example | 204 ------------------ .worktrees/feat-import-policy | 1 + .../api/tests/test_internal_token_gate.py | 70 ------ src/auth/internal-token.ts | 73 ------- tests/auth/internal-token.test.ts | 53 ----- tests/auth/middleware-internal-token.test.ts | 75 ------- 6 files changed, 1 insertion(+), 475 deletions(-) delete mode 100644 .env.example create mode 160000 .worktrees/feat-import-policy delete mode 100644 services/api/tests/test_internal_token_gate.py delete mode 100644 src/auth/internal-token.ts delete mode 100644 tests/auth/internal-token.test.ts delete mode 100644 tests/auth/middleware-internal-token.test.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index 59ad76b..0000000 --- a/.env.example +++ /dev/null @@ -1,204 +0,0 @@ -# ============================================================================= -# 0pt (edge plane) -- environment inventory / example environment -# ============================================================================= -# Copy to `.env` (gitignored) and fill in real values. Never commit real values. -# Vercel env for the edge app; VPS .env (git-ignored) for infra/docker-compose.yml. -# See docs/08-infrastructure.md for the canonical description of each integration. -# -# Two provenance markers are used below and they mean different things: -# * "wired in code today" -- read via a `process.env.*` somewhere in -# this repo right now (grep-verified). -# * "documented target, not yet read" -- named in docs/08-infrastructure.md / -# docs/02-architecture.md but with no -# `process.env.*` read yet. Filling it in -# unblocks the integration; it does not by -# itself change behaviour. -# -# The app is designed to build and boot with all of 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 -# --------------------------------------------------------------------------- -# Public base URL of this app (used for absolute links / callbacks). -NEXT_PUBLIC_APP_URL=http://localhost:3000 -# Documented target (docs/08); no call site found reading it in this pass. -# NOTE: api.0pt.app is a dead hostname (falls through to wildcard Vercel -> -# DEPLOYMENT_NOT_FOUND). The app serves its own /api routes on the site -# origin, so the honest base URL is the live origin itself. -NEXT_PUBLIC_API_URL=https://0pt.app - -# --------------------------------------------------------------------------- -# Database (Postgres) -- wired in code today: lib/db.ts, app/api/health -# --------------------------------------------------------------------------- -# Vercel Postgres sets POSTGRES_URL / POSTGRES_PRISMA_URL automatically when the -# integration is attached. First one set wins, in this order (lib/db.ts): -# DATABASE_URL -> POSTGRES_URL -> POSTGRES_PRISMA_URL. -DATABASE_URL=postgres://user:password@localhost:5432/opt -POSTGRES_URL= -POSTGRES_PRISMA_URL= - -# --------------------------------------------------------------------------- -# Cache / queue (Redis) -- app/api/health only checks presence; no other call -# site found reading it in this pass (worker queue usage, if any, lives in -# services/api, outside this repo's scope). -# --------------------------------------------------------------------------- -REDIS_URL=redis://localhost:6379 - -# --------------------------------------------------------------------------- -# Auth -- session / dev-login / service-token secrets (wired in code today) -# --------------------------------------------------------------------------- -# Session-signing secret (generate a long random string). -AUTH_SECRET=change-me-long-random-string -# AUTH_URL is documented (docs/08) but not yet read by any route in this pass. -AUTH_URL=https://0pt.app -# Optional dev-only login shortcut key (leave UNSET in production). -DEV_LOGIN_KEY= -# Shared token for internal/service-to-service calls; sent as X-Internal-Token -# on every engineFetch() call (lib/engine.ts). -INTERNAL_TOKEN=change-me-internal-token - -# --------------------------------------------------------------------------- -# Zitadel / OIDC Bearer verification (Wave 2 hardening) -- wired in code today -# --------------------------------------------------------------------------- -# 0pt verifies inbound Bearer tokens against the fleet Zitadel IdP -# (src/auth/zitadel.ts, wired via middleware.ts). JWKS are discovered from -# /.well-known/openid-configuration, falling back to /oauth/v2/keys. -# -# Fleet Zitadel issuer (OIDC). Default when unset: https://id-customers.siax.io -ZITADEL_ISSUER=https://id-customers.siax.io -# Enforcement is FAIL-CLOSED. Leave this UNSET (or set it to "true") and a -# valid, verified Bearer token is REQUIRED on /api/0pt/* -- 401 on -# missing/invalid, 403 when X-Tenant-Id disagrees with the token tenant. -# ONLY the literal value "false" disables authentication, and that value is -# REFUSED when NODE_ENV=production: the app throws at startup instead of -# serving openly. Use "false" for local development only. -AUTH_ENFORCE=true -# Optional override for the claim key that carries the SIAX tenant id. When -# unset, tenant derivation uses the documented precedence: -# urn:zitadel:iam:org:id -> urn:zitadel:iam:user:resourceowner:id -> tenant_id -> tenant. -ZITADEL_TENANT_CLAIM= - -# --------------------------------------------------------------------------- -# Data engine bridge -- wired in code today: lib/engine.ts -# --------------------------------------------------------------------------- -# When set, edge route handlers defer to the FastAPI engine (services/api) as the -# single source of truth (orders/estimates/catalog/ai-parse). When unset, the edge -# computes locally / falls back to an honest heuristic -- see lib/engine.ts and -# app/api/ai/parse. -ENGINE_URL=http://localhost:8000 - -# --------------------------------------------------------------------------- -# SIAX platform clients -# --------------------------------------------------------------------------- -# WIRED FOR REAL since 2026-08-31: every factory in src/clients/* reads its -# *_URL from process.env when no constructor baseUrl is given. Under -# NODE_ENV=production an UNSET url is a hard boot failure -# (SiaxError DEPENDENCY_UNAVAILABLE) — production never silently falls back -# to the in-memory mocks (Master-DoD §3/§10). In dev/test, unset = documented -# local mock/no-op behaviour. -CL0UD_URL= -N0D_URL= -AUD0_URL= -# Optional service bearer tokens for the platform clients (machine identity). -# Values come from Infisical/secret refs — NEVER commit real values here. -CL0UD_SERVICE_TOKEN= -N0D_SERVICE_TOKEN= -AUD0_SERVICE_TOKEN= -ST0RE_SERVICE_TOKEN= -# INF0 (AI OS) -- documented target, not yet read by any code in this repo. -# Route AI-parsning / model calls through the gateway instead of raw provider keys. -INF0_URL= -# ST0RE_URL is the convention name for src/clients/st0re-client.ts / -# st0re-lakehouse-client.ts and is read by the real-infra test suite -# (tests/p0/*.realinfra.test.ts). The lakehouse write path was live-verified -# against ST0RE on 2026-07-13 with this set; unset, the clients fall back to -# their deterministic in-memory mocks. -ST0RE_URL= - -# --------------------------------------------------------------------------- -# P0 Data-OS backends (src/search, src/analytics, src/metadata, src/features) -# --------------------------------------------------------------------------- -# OpenSearch — search projections ONLY, never canonical truth (§42). Unset in -# dev/test = search capability unavailable (typed error), never a fake. -OPENSEARCH_URL= -# ClickHouse — 0pt capability analytics (tenant-isolated metric events). -# HAS NO CONNECTION to b0ts pipelines or quarantined market data (legal). -CLICKHOUSE_URL= -# "user:password" for ClickHouse basic auth; from secret refs only. -CLICKHOUSE_AUTH= -# OpenMetadata — metadata PROJECTION target (canonical stays in 0pt). -OPENMETADATA_URL= -OPENMETADATA_TOKEN= -# Feast feature server (online features path). -FEAST_SERVER_URL= - -# --------------------------------------------------------------------------- -# Payments (Stripe) -- wired in code today: app/api/payments/*, -# app/api/webhooks/stripe -# --------------------------------------------------------------------------- -STRIPE_SECRET_KEY=sk_test_xxx -STRIPE_WEBHOOK_SECRET=whsec_xxx -# Documented target (docs/08); not yet read by any route in this pass. -NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= - -# --------------------------------------------------------------------------- -# Object storage -- Cloudflare R2 / S3-compatible -# --------------------------------------------------------------------------- -# R2_ENDPOINT / R2_BUCKET / R2_ACCESS_KEY_ID are read in code today; all three -# are required to enable /api/uploads, otherwise it returns 501. -# R2_ACCOUNT_ID and R2_SECRET_ACCESS_KEY are documented in docs/08 but were NOT -# found via a process.env.* read in this pass -- verify before assuming the R2 -# client is fully wired. -R2_ENDPOINT=https://.r2.cloudflarestorage.com -R2_BUCKET=opt-uploads -R2_ACCESS_KEY_ID=change-me -R2_ACCOUNT_ID= -R2_SECRET_ACCESS_KEY= -# Fallback local artifact store used when no object storage is configured -# (src/adapters/local-blob-store.ts). Leave unset in production. -OPT_LOCAL_BLOB_DIR=./.opt-blobs - -# --------------------------------------------------------------------------- -# Document intelligence / OCR (Azure) -- wired in code today: -# src/services/document-extraction-service.ts. Unset => extraction -# "not configured". -# --------------------------------------------------------------------------- -DOC_INTELLIGENCE_ENDPOINT=https://.cognitiveservices.azure.com -DOC_INTELLIGENCE_KEY=change-me -DOC_INTELLIGENCE_API_VERSION=2024-11-30 - -# --------------------------------------------------------------------------- -# Customer DB-grant encryption -- wired in code today: -# /api/connections/db-grant -# --------------------------------------------------------------------------- -# 32-byte key, AES-GCM. Encrypts customer-supplied read-only connection strings -# at rest. -GRANT_ENC_KEY=change-me-32-byte-key - -# --------------------------------------------------------------------------- -# External write-back targets -# --------------------------------------------------------------------------- -# Odoo base URL for the bills/write-back adapter. -ODOO_BASE_URL=https:// - -# --------------------------------------------------------------------------- -# Email -- Resend (documented target, docs/08; no Resend call site found in -# this pass -- treat as not-yet-implemented, not just unconfigured) -# --------------------------------------------------------------------------- -RESEND_API_KEY= - -# --------------------------------------------------------------------------- -# Embedded analytics (documented target, docs/08; no call site found in this -# pass) -# --------------------------------------------------------------------------- -METABASE_SITE_URL= -METABASE_SECRET_KEY= -NOCODB_BASE_URL= -NOCODB_API_TOKEN= - -# --------------------------------------------------------------------------- -# Test-only (tests/p0 real-infra suite; not needed to run the app) -# --------------------------------------------------------------------------- -# ST0RE_TEST_TENANT_ID= diff --git a/.worktrees/feat-import-policy b/.worktrees/feat-import-policy new file mode 160000 index 0000000..c134527 --- /dev/null +++ b/.worktrees/feat-import-policy @@ -0,0 +1 @@ +Subproject commit c134527509a88b51d6197e54664f5ee118c7678e diff --git a/services/api/tests/test_internal_token_gate.py b/services/api/tests/test_internal_token_gate.py deleted file mode 100644 index e6d031a..0000000 --- a/services/api/tests/test_internal_token_gate.py +++ /dev/null @@ -1,70 +0,0 @@ -"""The engine must not accept anonymous traffic when INTERNAL_TOKEN is set. - -Background (2026-09-15): lib/engine.ts on the edge has always SENT -`X-Internal-Token`, but the engine never verified it — a deployed engine would -have accepted anyone who could reach the port. The gate is fail-closed: - - * INTERNAL_TOKEN set -> every route except /health requires the EXACT - token; wrong/missing = 401. - * INTERNAL_TOKEN unset -> gate inert (local dev); production compose always - sets it. - -`/health` stays public on purpose: it is the liveness probe that -`lib/health-checks.ts` calls from the edge, and it leaks nothing but booleans. - -The token is read at module import (the same value the running process serves -with), so each test reloads the module with the env it wants — and the -teardown reloads it one more time with the env CLEARED so later test files in -the same session never inherit a baked-in token. -""" - -from __future__ import annotations - -import importlib -import os - -import pytest -from fastapi.testclient import TestClient - - -@pytest.fixture() -def engine(monkeypatch): - monkeypatch.setenv("INTERNAL_TOKEN", "s3cret") - import orchestrator.main as main - - importlib.reload(main) - yield TestClient(main.app) - os.environ.pop("INTERNAL_TOKEN", None) - importlib.reload(main) - - -@pytest.fixture() -def engine_no_token(monkeypatch): - monkeypatch.delenv("INTERNAL_TOKEN", raising=False) - import orchestrator.main as main - - importlib.reload(main) - yield TestClient(main.app) - os.environ.pop("INTERNAL_TOKEN", None) - importlib.reload(main) - - -def test_gate_off_when_internal_token_unset(engine_no_token): - assert engine_no_token.get("/health").status_code == 200 - assert engine_no_token.get("/sources").status_code == 200 - - -def test_health_stays_public_but_sources_require_token(engine): - # liveness probe is public by design - assert engine.get("/health").status_code == 200 - # anonymous call to a data route -> 401 (was 200 before 2026-09-15) - r = engine.get("/sources") - assert r.status_code == 401 - assert r.json()["error"]["code"] == "unauthorized" - - -def test_wrong_token_is_401_and_correct_token_passes(engine): - wrong = engine.get("/estimates?markets=SE", headers={"X-Internal-Token": "nope"}) - assert wrong.status_code == 401 - right = engine.get("/sources", headers={"X-Internal-Token": "s3cret"}) - assert right.status_code == 200 diff --git a/src/auth/internal-token.ts b/src/auth/internal-token.ts deleted file mode 100644 index 852d5b9..0000000 --- a/src/auth/internal-token.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * `src/auth/internal-token.ts` -- verification for the ONE machine-to-machine - * credential 0pt already uses for engine (Python) -> edge (Next.js) calls: - * the `X-Internal-Token` header, checked against the `INTERNAL_TOKEN` env var - * (the same shared secret `lib/engine.ts` and `lib/health-checks.ts` already - * send on the opposite-direction edge -> engine calls). - * - * P014 FIX: `services/api/workers/quality_bridge.py` has always called - * `POST /api/0pt/quality/bridge-run` with `X-Internal-Token`, never an - * `Authorization: Bearer` header. But this route sits under - * `/api/0pt/:path*`, which `middleware.ts` guards unconditionally with real - * Zitadel Bearer verification -- and nothing anywhere checked - * `X-Internal-Token` as an alternative credential. The result: in any - * environment with the default `AUTH_ENFORCE` posture (enforced), the - * Python engine's ONLY call into this bridge was rejected with 401 by - * middleware before the route handler ever ran. `OPT_QUALITY_BRIDGE_ENABLED=true` - * therefore could never actually succeed against a real deployment -- - * quality_bridge.py's fail-open design (see its own docstring) silently - * absorbed the 401 as `status: "unavailable"`, so nothing outwardly broke, - * but the whole point of building the bridge (an advisory second opinion - * from the TS QualityService) was unreachable. Neither test suite caught - * this: the TS test (tests/p0/quality-bridge.test.ts) calls the route's - * POST function directly, bypassing middleware.ts entirely; the Python test - * (services/api/tests/test_quality_bridge.py) mocks the HTTP layer and never - * exercises the edge's real auth gate. - * - * This module verifies `X-Internal-Token` in constant time. middleware.ts - * accepts it ONLY for the one route the engine actually calls - * (`/api/0pt/quality/bridge-run`) -- not a blanket bypass for the rest of - * `/api/0pt/*`. Fails closed: an unset/empty `INTERNAL_TOKEN` env var never - * authenticates, even against an empty or missing header (no accidental - * "unset equals unset, so let it through"). - * - * Uses only Web APIs (`TextEncoder`), not `node:crypto` -- Next.js - * middleware runs on the Edge runtime, which does not expose Node's - * `crypto.timingSafeEqual`. - */ - -/** Minimal header accessor -- Web `Headers` and `NextRequest.headers` satisfy this. */ -export interface HeaderLookup { - get(name: string): string | null | undefined; -} - -export const INTERNAL_TOKEN_HEADER = "x-internal-token"; - -/** Constant-time-ish byte comparison (Edge-runtime safe: no node:crypto). */ -function timingSafeEqualStr(a: string, b: string): boolean { - const enc = new TextEncoder(); - const aBytes = enc.encode(a); - const bBytes = enc.encode(b); - const len = Math.max(aBytes.length, bBytes.length); - let diff = aBytes.length ^ bBytes.length; - for (let i = 0; i < len; i++) { - diff |= (aBytes[i] ?? 0) ^ (bBytes[i] ?? 0); - } - return diff === 0; -} - -/** - * True only when `X-Internal-Token` is present AND matches a non-empty - * configured `INTERNAL_TOKEN`. Fails closed on every other combination - * (missing header, missing/empty env var, mismatch). - */ -export function verifyInternalServiceToken( - headers: HeaderLookup, - env: { INTERNAL_TOKEN?: string } = process.env, -): boolean { - const expected = env.INTERNAL_TOKEN?.trim(); - if (!expected) return false; - const provided = headers.get(INTERNAL_TOKEN_HEADER); - if (!provided) return false; - return timingSafeEqualStr(provided, expected); -} diff --git a/tests/auth/internal-token.test.ts b/tests/auth/internal-token.test.ts deleted file mode 100644 index 43c9860..0000000 --- a/tests/auth/internal-token.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { verifyInternalServiceToken, INTERNAL_TOKEN_HEADER } from "@/src/auth/internal-token"; - -function headers(map: Record = {}): { get(name: string): string | null } { - const lower = new Map(Object.entries(map).map(([k, v]) => [k.toLowerCase(), v])); - return { get: (name: string) => lower.get(name.toLowerCase()) ?? null }; -} - -describe("verifyInternalServiceToken", () => { - it("accepts a header that matches the configured INTERNAL_TOKEN exactly", () => { - const ok = verifyInternalServiceToken(headers({ [INTERNAL_TOKEN_HEADER]: "s3cr3t" }), { - INTERNAL_TOKEN: "s3cr3t", - }); - expect(ok).toBe(true); - }); - - it("rejects a mismatched token", () => { - const ok = verifyInternalServiceToken(headers({ [INTERNAL_TOKEN_HEADER]: "wrong" }), { - INTERNAL_TOKEN: "s3cr3t", - }); - expect(ok).toBe(false); - }); - - it("rejects a missing header even when INTERNAL_TOKEN is configured", () => { - const ok = verifyInternalServiceToken(headers({}), { INTERNAL_TOKEN: "s3cr3t" }); - expect(ok).toBe(false); - }); - - it("FAILS CLOSED: an unset INTERNAL_TOKEN never authenticates, even against an empty header value", () => { - // Regression guard: quality_bridge.py sends `os.getenv("INTERNAL_TOKEN", "")` - // -- if the edge's own INTERNAL_TOKEN were also unset, a naive `provided - // === expected` comparison of two empty strings would wrongly authenticate. - const ok = verifyInternalServiceToken(headers({ [INTERNAL_TOKEN_HEADER]: "" }), { INTERNAL_TOKEN: undefined }); - expect(ok).toBe(false); - }); - - it("FAILS CLOSED: an unset INTERNAL_TOKEN never authenticates even a non-empty header", () => { - const ok = verifyInternalServiceToken(headers({ [INTERNAL_TOKEN_HEADER]: "anything" }), { - INTERNAL_TOKEN: undefined, - }); - expect(ok).toBe(false); - }); - - it("rejects a whitespace-only configured token (trimmed to empty)", () => { - const ok = verifyInternalServiceToken(headers({ [INTERNAL_TOKEN_HEADER]: " " }), { INTERNAL_TOKEN: " " }); - expect(ok).toBe(false); - }); - - it("is case-sensitive on the header name lookup contract but tolerant of header-name casing via the HeaderLookup adapter", () => { - const ok = verifyInternalServiceToken(headers({ "X-Internal-Token": "s3cr3t" }), { INTERNAL_TOKEN: "s3cr3t" }); - expect(ok).toBe(true); - }); -}); diff --git a/tests/auth/middleware-internal-token.test.ts b/tests/auth/middleware-internal-token.test.ts deleted file mode 100644 index ec57c4f..0000000 --- a/tests/auth/middleware-internal-token.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * P014 regression test: before this pass, POST /api/0pt/quality/bridge-run - * -- the ONE route the Python engine's quality_bridge.py actually calls, - * using X-Internal-Token, never a Bearer token -- was rejected 401 by - * middleware.ts in any environment enforcing auth (the default), because - * middleware required a verified Zitadel Bearer token unconditionally on - * every /api/0pt/* path. See src/auth/internal-token.ts's doc comment for - * the full story. Neither existing test suite caught it: the TS bridge test - * calls the route's POST function directly (bypassing middleware.ts - * entirely) and the Python test mocks the HTTP layer. - * - * This test exercises the actual `middleware` function, not the route - * handler, so it proves the fix at the layer where the bug lived. - */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { NextRequest } from 'next/server'; -import { middleware } from '../../middleware'; - -function req(path: string, headers: Record = {}): NextRequest { - return new NextRequest(new URL(path, 'http://localhost'), { headers }); -} - -describe('middleware.ts: internal-token path for /api/0pt/quality/bridge-run', () => { - const saved = { auth: process.env.AUTH_ENFORCE, internal: process.env.INTERNAL_TOKEN, node: process.env.NODE_ENV }; - - beforeEach(() => { - delete process.env.AUTH_ENFORCE; // default posture: enforced - process.env.INTERNAL_TOKEN = 'engine-shared-secret'; - }); - - afterEach(() => { - for (const [k, v] of Object.entries(saved)) { - const envKey = k === 'auth' ? 'AUTH_ENFORCE' : k === 'internal' ? 'INTERNAL_TOKEN' : 'NODE_ENV'; - if (v === undefined) delete process.env[envKey]; - else process.env[envKey] = v; - } - }); - - it('REGRESSION: a valid X-Internal-Token lets the engine through with NO Bearer token (was: 401)', async () => { - const res = await middleware( - req('/api/0pt/quality/bridge-run', { 'x-internal-token': 'engine-shared-secret' }), - ); - // NextResponse.next() carries no body/status override -- it is not an - // error JSON response the way a rejection is. - expect(res.status).toBe(200); - }); - - it('an invalid X-Internal-Token still falls through to the ordinary Bearer requirement (401, not a bypass)', async () => { - const res = await middleware(req('/api/0pt/quality/bridge-run', { 'x-internal-token': 'wrong-secret' })); - expect(res.status).toBe(401); - const body = await res.json(); - expect(body.error).toBe('OPT_UNAUTHENTICATED'); - }); - - it('a valid X-Internal-Token on a DIFFERENT /api/0pt/* route does NOT bypass auth (not a blanket exemption)', async () => { - const res = await middleware(req('/api/0pt/sources', { 'x-internal-token': 'engine-shared-secret' })); - expect(res.status).toBe(401); - }); - - it('no INTERNAL_TOKEN configured on the edge -- the internal-token path never activates, even with the right-looking header', async () => { - delete process.env.INTERNAL_TOKEN; - const res = await middleware( - req('/api/0pt/quality/bridge-run', { 'x-internal-token': 'engine-shared-secret' }), - ); - expect(res.status).toBe(401); - }); - - it('a real Bearer-authenticated caller can still reach bridge-run directly (not internal-token-only)', async () => { - // No X-Internal-Token at all, no Authorization header either -> falls to - // the normal Bearer requirement -> 401, proving this path is additive, - // not a replacement for the existing Zitadel flow. - const res = await middleware(req('/api/0pt/quality/bridge-run')); - expect(res.status).toBe(401); - }); -}); -- 2.54.0