WIP snapshot asus-20261005 (Documents/0pt) #358

Closed
admin wants to merge 1 commits from wip/asus-20261005/Documents_0pt-9cad5f9 into main
6 changed files with 1 additions and 475 deletions
-204
View File
@@ -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
# <issuer>/.well-known/openid-configuration, falling back to <issuer>/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://<accountid>.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://<resource>.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://<your-odoo-host>
# ---------------------------------------------------------------------------
# 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=
Submodule .worktrees/feat-import-policy added at c134527509
@@ -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
-73
View File
@@ -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);
}
-53
View File
@@ -1,53 +0,0 @@
import { describe, it, expect } from "vitest";
import { verifyInternalServiceToken, INTERNAL_TOKEN_HEADER } from "@/src/auth/internal-token";
function headers(map: Record<string, string> = {}): { 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);
});
});
@@ -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<string, string> = {}): 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);
});
});