docs(standard): add code-verified ARCHITECTURE.md + siax.config.json (S1-S7)
CI (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / quality (push) Successful in 39s

Brings 0pt to the SIAX repo standard. README, LICENSE (proprietary, correct
for this repo), Dockerfile and .gitea/workflows/ci.yml already existed and are
left untouched; no .github/workflows exists.

The architecture document was written by reading the code (entrypoints,
routes, services, migrations, Dockerfile, env, tests) and by probing the live
0pt.app instance -- not from the templates. It therefore contradicts the
existing docs in several places, deliberately:

* /api/0pt/* answers ANONYMOUSLY in production (live-verified 2026-08-07):
  GET https://0pt.app/api/0pt returns 200 with no token, and 200 with a
  deliberately invalid one. The deployment is current (the newer lakehouse
  routes are present), so middleware.ts is simply not running. README lines
  49-52 and merge 6531384 claim the opposite. Root cause UNKNOWN -- flagged,
  not guessed.
* No handler reads x-0pt-verified-tenant. middleware.ts sets it; nothing else
  in the repo reads it. Every /api/0pt/* handler takes tenant_id from the
  request body/query instead, so tenant isolation is not implemented.
* Everything outside /api/0pt/* has no auth at all (matcher scope), including
  GET /api/admin/metrics and GET /api/orders (both live-verified 200 anon) and
  POST /api/payments/{id}/release, which flips an order to "delivered"
  regardless of payment.
* CL0UD policy client is fail-open and AUD0 audit is a silent no-op when their
  URLs are unset, with no production guard -- unlike AUTH_ENFORCE, which has one.
* connections/db-grant documents AES-GCM encryption it does not implement.
* The FastAPI engine (services/api) has zero authentication despite
  lib/engine.ts sending X-Internal-Token.
* services/scrapers/waf_bypass.py (FlareSolverr, Tor, cloudscraper, proxy pool,
  undetected browser, fake identities) plus flaresolverr/tor-proxy in
  services/infra/docker-compose.data-stack.yml directly contradict CLAUDE.md
  §1 and the README's compliance claim. Left in place -- deletion is a Simon
  decision, recorded as open question 2.
* Production has NO database (live /api/health reports db:not_configured) and
  the whole /api/0pt/* capability layer is per-module in-memory Maps, so state
  does not survive a cold start and is not even shared between route modules.

siax.config.json deviates from the template on two fields, on purpose:
deploy.target is "vercel" (live-verified Server: Vercel header), not "coolify",
and designSystem is null because @siax/ui is not a dependency here. Both
deviations are documented in ARCHITECTURE.md §5 and §8.

Docs-only change. No code, CI, LICENSE or workflow files were modified.
This commit is contained in:
Claude (SIAX platform session)
2026-08-07 21:48:35 +02:00
parent 65313842d9
commit bb58617540
3 changed files with 441 additions and 0 deletions
+7
View File
@@ -7,6 +7,13 @@ confidence; nothing is guessed.
**Live:** https://0pt.app · **Contract:** [`CLAUDE.md`](CLAUDE.md) + [`docs/`](docs/)
> **Architecture (verified against code, not aspirational):**
> [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md).
> It documents the real API surface, the real persistence story, and the known
> security gaps — including several places where this README is optimistic. Read
> it before trusting any capability claim below. Repo identity + estate
> classification: [`siax.config.json`](siax.config.json).
## Two planes (see [docs/02-architecture.md](docs/02-architecture.md))
- **Edge (Vercel, this app)** — marketing, catalog, the order lifecycle, customer portal,
+417
View File
@@ -0,0 +1,417 @@
# 0PT — Arkitektur
**Kategori:** PLATTFORM **Underkategori:** Data OS **Status:** under utveckling (deployad frontend i drift, capability-lagret ej produktionsklart)
**Senast verifierad mot kod:** 2026-08-07 (commit `6531384`, main)
> **Läsanvisning.** Detta dokument beskriver vad koden i repot **gör**, inte vad
> `README.md`, `CLAUDE.md` eller `docs/01–11` säger att den borde göra. Där de
> avviker står avvikelsen utskriven — se §7. Påståenden märkta *(live-verifierat
> 2026-08-07)* är kontrollerade mot den körande instansen på `https://0pt.app`.
> Allt annat är läst ur källkoden. Där något inte gick att belägga står **OKÄNT**.
---
## 1. Syfte
0pt är SIAX Clouds **Data OS**: plattformen som skaffar, strukturerar, härleder
och levererar företags-, kontakt- och fordonsdata för Norden/Europa, med
proveniens (källa, färskhet, konfidens) per fält.
Repot innehåller i praktiken **två separata system** i samma träd:
| Plan | Var | Vad koden faktiskt är |
|---|---|---|
| **Webbkonsol + edge-API** | repo-roten (`app/`, `lib/`, `components/`) | Next.js 16.0.10 App Router. 20 sidor (svensk UI-copy), 28 API-routehandlers. Marknadsföring, katalog, orderflöde, kund-/adminportal. |
| **Capability-lager** | `src/` + `app/api/0pt/*` | TypeScript-tjänster för `0pt.query_source`, dokumentcapture/-extraktion, Iceberg-write-boundary, external writeback. **All state ligger i `Map`-objekt i minnet** (`src/services/*.ts`), inte i databas. |
| **Data-engine** | `services/api/` | FastAPI-orkestrator, 12 endpoints, 162 rader. Workers (`adaptive_router`, `pipeline`, `queue`, `resolve`, `run`). |
| **Skrapmotor** | `services/scrapers/` | Python. `engine.py`, `engine_v2.py`, `waf_bypass.py`, ClickHouse-pipeline. |
### Vad 0pt uttryckligen INTE gör
- **Ingen egen IdP.** Bearer-tokens verifieras mot Zitadel (`src/auth/zitadel.ts`),
utfärdas aldrig här. Cookie-sessionen i `lib/auth.ts` är ett HMAC-scaffold med
dev-login bakom `DEV_LOGIN_KEY`, uttryckligen inte produktionsauth.
- **Ingen BigQuery.** Ingen referens till BQ finns i koden (verifierat med grep).
- **Ingen egen policymotor.** Beslut delegeras till CL0UD (`src/clients/cl0ud-client.ts`).
- **Ingen tung batch i webblagret.** Långa jobb ska gå till engine/N0D.
### Uttryckligt förbud som koden BRYTER MOT
`CLAUDE.md` §1 och `README.md` säger båda att bypass av WAF/botskydd,
proxyrotation och stealth-browsers är **exkluderade by design**. Det stämmer inte:
`services/scrapers/waf_bypass.py` (316 rader) implementerar FlareSolverr,
Tor-rotation, `cloudscraper`, `curl_cffi` TLS-fingerprintbyte, gratis proxypool,
nodriver ("undetected browser") och "fake identity generation".
`services/infra/docker-compose.data-stack.yml` startar `flaresolverr` och
`tor-proxy` som tjänster. Commit `4a164c5` (Simon Axelsson, 2026-07-13) heter
ordagrant *"refactor: remove compliance restrictions"*. Se §7 lucka **S9**.
---
## 2. Plats i estatet
### Beroenden uppåt (0pt anropar)
| Beroende | Klient | Env-var | Beteende när env saknas |
|---|---|---|---|
| Zitadel (IdP) | `src/auth/zitadel.ts` | `ZITADEL_ISSUER` (default `https://id-customers.siax.io`) | Använder default-issuern; JWKS hämtas via OIDC-discovery, fallback `<issuer>/oauth/v2/keys` |
| CL0UD (policy) | `src/clients/cl0ud-client.ts` | `CL0UD_URL` | **Fail-open** — returnerar alltid `allow` utan nätanrop (§7 S5) |
| AUD0 (audit) | `src/clients/aud0-client.ts` | `AUD0_URL` | **No-op** — inga auditevent skrivs (§7 S6) |
| ST0RE (artefakter/lakehouse) | `src/clients/st0re-client.ts`, `st0re-lakehouse-client.ts` | `ST0RE_URL` | Returnerar deterministisk mockartefakt |
| N0D (jobb) | `src/clients/n0d-client.ts` | `N0D_URL` | Returnerar deterministiskt mockjobb |
| Egen data-engine | `lib/engine.ts` | `ENGINE_URL`, `INTERNAL_TOKEN` | `engineFetch` returnerar `null`, edge räknar lokalt |
| Stripe | `lib/stripe.ts` | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` | Betalroutes svarar `501` |
| Azure Document Intelligence | `src/adapters/document-intelligence-adapter.ts` | `DOC_INTELLIGENCE_*` | Extraktion svarar "not configured" |
| Cloudflare R2 | `app/api/uploads/route.ts` | `R2_ENDPOINT`, `R2_BUCKET`, `R2_ACCESS_KEY_ID` | `501`, fallback `OPT_LOCAL_BLOB_DIR` |
| OpenSanctions (extern, publik) | `app/api/screen/route.ts` | — | Anropas direkt utan nyckel |
| Odoo | `src/adapters/odoo-bills-adapter.ts` | `ODOO_BASE_URL` | Adapter inaktiv |
Mönstret är genomgående: **saknad env ⇒ tyst degradering, inte fel.** Det är
bekvämt lokalt och farligt i produktion (§7 S5, S6).
### Beroenden nedåt (vem konsumerar 0pt)
Enligt `CLAUDE.md` matar 0pt interna produkter (Protosell/FiveCRM, VehIQ). **Inga
sådana anrop finns i det här repot** — ingen inkommande integration är verifierad
här. Betrakta konsumentlistan som odokumenterad tills motparten visats.
### Vad 0pt inte får bero på
- **BigQuery** — hård estate-regel. Ej överträdd.
- **Hogia** — skrivning förbjuden. Ingen referens finns.
- **Protosell-tenantdata** — tenantisolering, se §7 S2.
- Nya SaaS-beroenden utan dokumenterat skäl (`CLAUDE.md` §4, "one tool per job").
---
## 3. API-yta
Risknivå = SIAX L0–L5-taxonomin, bedömd av mig utifrån vad handlern gör.
"Kräver godkännande" = huruvida koden **faktiskt** kräver auth/godkännande idag.
### 3.1 `/api/0pt/*` — capability-lagret (bakom `middleware.ts`-matchern)
| Operation | Metod/sökväg | Risknivå | Kräver godkännande |
|---|---|---|---|
| Capability-index | `GET /api/0pt` | L0 | Nej — *(live-verifierat: 200 anonymt)* |
| Registrera dataset↔Iceberg-mappning | `POST /api/0pt/lakehouse/mappings` | L3 | Nej i kod; `tenant_id` läses ur body |
| Lista mappningar | `GET /api/0pt/lakehouse/mappings` | L2 | Nej; `tenant_id` ur query |
| Hämta mappning | `GET /api/0pt/lakehouse/mappings/{mappingId}` | L2 | Nej |
| Utför Iceberg-write | `POST /api/0pt/lakehouse/mappings/{mappingId}/writes` | **L4** | Nej i kod (CL0UD fail-open, §7 S5) |
| Registrera rate-policy | `POST /api/0pt/sources/rate-policies` | L2 | Nej |
| Lista/hämta rate-policy | `GET /api/0pt/sources/rate-policies[/{id}]` | L1 | Nej |
| Kör källfråga | `POST /api/0pt/sources/{sourceId}/query` | **L4** | Nej; `connection_config` tas ur body (§7 S10) |
| Ta emot dokument | `POST /api/0pt/documents/capture` | L3 (PII) | Nej |
| Lista dokument | `GET /api/0pt/documents/capture` | L3 (PII) | Nej |
| Extrahera dokument | `POST /api/0pt/documents/{documentId}/extraction` | L3 (PII) | Nej |
### 3.2 Övriga API-routes — **helt utanför middleware-matchern**
`middleware.ts` har `matcher: ['/api/0pt/:path*']`. Allt nedan har därför
**noll autentisering i koden**.
| Operation | Metod/sökväg | Risknivå | Kräver godkännande |
|---|---|---|---|
| Liveness | `GET /api/health` | L0 | Nej (avsiktligt) |
| Prisestimat | `POST /api/estimates` | L1 | Nej |
| Katalog / källor / fordon | `GET /api/{catalog,sources,vehicles}` | L1 | Nej |
| Sanktions-/PEP-screening | `GET /api/screen?q=` | L2 | Nej — proxar mot OpenSanctions |
| AI-parsning av fritext | `POST /api/ai/parse` | L2 | Nej |
| Skapa order | `POST /api/orders` | L2 | Nej |
| **Lista alla ordrar** | `GET /api/orders` | **L3** | **Nej** — ingen org-scoping *(live: 200, `[]`)* |
| Läs/ändra order | `GET,PATCH /api/orders/{id}` | L3 | Nej |
| **Adminmetrik (orderpipeline, belopp)** | `GET /api/admin/metrics` | **L3** | **Nej** *(live-verifierat: 200 anonymt)* |
| Deposition (Stripe checkout) | `POST /api/payments/{order_id}/deposit` | **L4** | Nej |
| **Frigör escrow-slutbetalning** | `POST /api/payments/{order_id}/release` | **L4** | **Nej** — sätter dessutom status `delivered` (§7 S4) |
| Stripe-webhook | `POST /api/webhooks/stripe` | L3 | Signaturkontroll via `STRIPE_WEBHOOK_SECRET` |
| Prenumerationer | `GET,POST /api/subscriptions` | L2 | Nej |
| Filuppladdning | `GET,POST /api/uploads` | L3 | Nej (`501` utan R2) |
| Ta emot kund-DB-grant | `POST /api/connections/db-grant` | **L4** | Nej (`501` utan `GRANT_ENC_KEY`; §7 S7) |
| Dev-login | `POST /api/auth/login` | L2 | Ja — kräver `AUTH_SECRET` + matchande `DEV_LOGIN_KEY` |
| Logout / me | `POST /api/auth/logout`, `GET /api/auth/me` | L1 | Cookiebaserat |
### 3.3 FastAPI-engine (`services/api/orchestrator/main.py`)
12 endpoints: `GET /health`, `/sources`, `/sources/{market}`, `/vehicles/{plate}`,
`/screen`, `POST /estimates`, `POST /orders`, `GET /orders`, `GET /orders/{oid}`,
`PATCH /orders/{oid}`, `POST /orders/{oid}/run`, `GET /orders/{oid}/status`.
**Ingen autentisering alls** — grep efter `Depends`, `auth`, `token`,
`X-Internal-Token` i filen ger noll träffar, trots att `lib/engine.ts` skickar
`X-Internal-Token`. Se §7 S8.
---
## 4. Datamodell
### 4.1 Postgres-schema (DDL finns, används inte av `/api/0pt/*`)
`sql/001_init.sql` definierar 13 tabeller:
- **Dataentiteter:** `dim_source`, `dim_company`, `dim_contact`, `dim_ubo`,
`dim_vehicle`, `fact_financials`
- **Proveniens:** `bridge_provenance` (`entity_type`, `entity_key`, `field_name`,
`source_id`, `freshness`, `confidence`, `level` 0–3, `agreed_sources`) — unikt
index per fält. Detta är produktens kärnsignatur.
- **Plattform:** `orgs`, `users`, `orders`, `jobs`, `deliveries`, `subscriptions`,
`files`, `db_grants`, `payments`, `audit_log`
- Kräver `pgvector` + `pgcrypto`. Belopp i **öre** (integer).
`sql/004_iceberg_write_boundary.sql` + `005_..._gap_closure.sql` lägger till
`opt_dataset_iceberg_mappings` och `opt_source_rate_policies`.
**Migrationskedjan har hål:** `sql/002_*` och `sql/003_*` **saknas i repot** men
refereras av tre filer (`app/api/0pt/sources/[sourceId]/query/route.ts:14`,
`src/services/source-service.ts:8`, `docs/P0_QUERY_SOURCE_FLOW.md:98`) som
"`sql/002_p0_dataset_quality.sql` definierar redan `opt_sources`". Tabellen
`opt_sources` har alltså ingen migration här. Det finns heller ingen
migrationsrunner — SQL:en appliceras manuellt. **OKÄNT** var/om den körts.
### 4.2 Var data faktiskt ligger
| Data | Faktisk lagring i kod |
|---|---|
| Ordrar, jobb, betalningar | `lib/orders-store.ts`: Postgres **om** `getSql()` ger en klient, annars `const mem = new Map()` per instans |
| Sources, datasets, mappningar, rate-policies, dokument, extraktioner, writebacks | `Map` i minnet, en per tjänsteinstans (`src/services/*.ts`, 10 st) |
| Dokumentbytes | R2 om konfigurerat, annars `LocalBlobStore` mot `OPT_LOCAL_BLOB_DIR` |
| Auditspår | AUD0 om `AUD0_URL` satt, annars ingenstans |
Två konsekvenser, båda dokumenterade i koden själv:
1. **Ingen delad state mellan routemoduler.** Varje route-fil instansierar sin
egen tjänst, så en källa registrerad via `POST /api/0pt/sources` är inte
synlig för `POST /api/0pt/sources/{id}/query`. Detta står utskrivet i
route-filens egen kommentar.
2. **Ingen durabilitet.** *(live-verifierat 2026-08-07: `GET /api/0pt/lakehouse/mappings`
och `/api/0pt/sources/rate-policies` svarar `[]`, och `GET /api/health` på
0pt.app rapporterar `"db":"not_configured"`.)* Produktionsinstansen har alltså
**ingen databas** — all state försvinner vid varje kallstart.
### 4.3 Persondata
- `dim_contact`: namn, roll, e-post, telefon, LinkedIn — persondata under GDPR.
- `dim_ubo`: verklig huvudman — persondata, potentiellt känslig.
- `db_grants.conn_encrypted`: kunds DB-anslutningssträng (se §7 S7).
- Dokumentcapture (`app/api/0pt/documents/capture`) tar emot godtyckliga
dokument, inkl. fakturor/mailbilagor via Graph-adaptern — persondata.
### 4.4 Retention
**Ingen retention är implementerad.** `CLAUDE.md` §5 utlovar "deletion-after-job";
inget sådant jobb, ingen TTL, ingen `DELETE`-väg och ingen radering finns i koden.
`audit_log`-tabellen finns men skrivs aldrig av TypeScript-lagret.
---
## 5. Deploy
| Fält | Verifierat läge |
|---|---|
| **Nod** | Ingen SIAX-nod. *(live-verifierat 2026-08-07: `https://0pt.app/api/health` → `Server: Vercel`, `X-Vercel-Id: arn1::iad1::…`)* Frontenden körs alltså på **Vercel**, inte på flottan. |
| **Coolify-app** | Ingen verifierad. `Dockerfile` är skriven för Coolify (node:24-alpine, multistage, `HEALTHCHECK` mot `/api/health`, kör som `node`-användare) men **OKÄNT** om någon Coolify-app pekar på repot. |
| **Domän** | `0pt.app` (live). Docs nämner även `api.0pt.app`, `data.0pt.app`, `dash.0pt.app` via Caddy — **ej verifierade**. |
| **Bygg** | `next build` via pnpm@9. `next.config.mjs` sätter `typescript.ignoreBuildErrors: true` — bygget typkontrollerar alltså **inte** `app/`, `components/`, `lib/` eller `middleware.ts`. |
| **Engine-deploy** | `infra/docker-compose.yml` (postgres, redis, api, worker, nocodb, metabase, caddy) och `services/infra/docker-compose.data-stack.yml` (clickhouse, trino, flaresolverr, tor-proxy, superset, postgres). **Ingen av dem är verifierad som körande.** |
| **Hemligheter** | Rena miljövariabler (`.env.example` listar 20+). **Infisical är inte inkopplat** — ingen referens till Infisical/vault finns i repot. Vercel-projektets env är källan för webblagret. |
| **CI** | `.gitea/workflows/ci.yml`. `quality`-jobb: typecheck (blockerande, endast `src/` + `tests/`), vitest (blockerande, 17 testfiler), eslint (rådgivande), `next build` (blockerande). `security`-jobb: Trivy `--exit-code 0` (rådgivande). Kör på `ubuntu-latest`. |
> **Konflikt att fatta beslut om:** estate-projektet *GCP/Vercel-exit* säger att
> flottan ska bort från Vercel. 0pt:s enda körande instans ligger på Vercel. Se §8.
---
## 6. Drift
### Hälsokontroll
`GET /api/health` → `{ok, service:"0pt-web", db, queue, ts}`. Den svarar **alltid
`ok:true`** oavsett om databasen finns — den rapporterar bara `"not_configured"`
i ett fält. En uptime-monitor som tittar på HTTP 200 kommer därför visa grönt
för en instans utan databas, vilket är exakt vad som gäller idag. Engine har ett
separat `GET /health`.
### Loggar
Ingen strukturerad loggning. `console.warn` i `src/auth/context.ts` är den enda
avsiktliga loggpunkten. Ingen korrelation-id-propagering till logg (ett
`requestId` skapas i `readRequestContext` men loggas aldrig). Ingen
Sentry/GlitchTip/OTel-koppling.
### Backup + senast testad restore
**Ingen backup.** Det finns ingen databas i produktion att säkerhetskopiera
(§4.2), inget backupskript, ingen dumproutine. **Restore har aldrig testats.**
Om/när Postgres provisioneras måste detta lösas separat — jfr estate-blockeraren
"ingen offsite backup".
### Kända fällor
1. **`pnpm install` kräver `--ignore-workspace`.** `pnpm-workspace.yaml` är en
v0-exportartefakt med bara `allowBuilds:` och inget `packages:`-fält, vilket
får pnpm att avbryta. `Dockerfile` skickar flaggan; **`.gitea/workflows/ci.yml`
gör det inte** — CI:s `pnpm install --frozen-lockfile` kan alltså falla på
samma sak som Dockerfilen redan fått ett workaround för. Ej verifierat vilken
pnpm-version som räddar CI idag.
2. **Node-versionsglapp:** CI kör Node 22, Dockerfile bygger på Node 24.
3. **In-memory-state** (§4.2) gör att varje kallstart och varje ny Vercel-lambda
nollställer capability-lagret. Testresultat i CI säger ingenting om detta.
4. `next.config.mjs` sätter `images.unoptimized: true` — inte en bugg, men
förklarar varför bildoptimering saknas.
5. `services/scrapers/output/*.json` innehåller committad skrapdata (brreg-sidor)
i repot.
6. `__pycache__/*.pyc` är committade i `services/`.
---
## 7. Säkerhet
### Vad som faktiskt finns
- **Riktig kryptografisk tokenverifiering.** `src/auth/zitadel.ts` gör äkta
JWKS-baserad `jwtVerify` med issuer-kontroll och `exp`/`nbf` via `jose`.
Tenanthärledning har dokumenterad precedens
(`urn:zitadel:iam:org:id` → `urn:zitadel:iam:user:resourceowner:id` →
`tenant_id` → `tenant`), och ett token utan tenantclaim underkänns.
- **Fail-closed default i koden.** `authEnforced()` returnerar `true` för allt
utom literalen `"false"`, och `assertAuthConfigSafe()` kastar vid
`NODE_ENV=production` + auth av. Designen är korrekt.
- **Stripe-webhooken** verifierar signatur mot `STRIPE_WEBHOOK_SECRET`.
- `SECURITY.md` finns med rapporteringsväg (`security@siax.io`).
### KÄNDA LUCKOR
**S1 — `/api/0pt/*` svarar anonymt i produktion, trots "fail-closed". 🔴**
*(live-verifierat 2026-08-07)* `GET https://0pt.app/api/0pt` returnerar **200**
utan `Authorization`-header. Samma sak med ett medvetet ogiltigt token
(`Bearer aaa.bbb.ccc`) → **200**. `GET /api/0pt/lakehouse/mappings` och
`/api/0pt/sources/rate-policies` returnerar **200 + `[]`** anonymt. Eftersom
lakehouse-routen bara finns i nyare kod är deployen **inte** gammal — middlewaren
kör helt enkelt inte. `README.md` rad 49–52 och merge-commit `6531384`
("make the AUTH_ENFORCE default fail-closed — 0pt.app was answering anonymously")
påstår motsatsen. **Orsaken är OKÄND.** Ej uteslutna hypoteser, ingen verifierad:
(a) `middleware.ts` plockas inte upp av Next 16 i den här filplaceringen när ett
`src/`-träd finns, (b) Vercel-projektet har ingen middleware i sin
build-output, (c) något annat. **Detta måste rotorsaksanalyseras innan repot
beskrivs som skyddat någonstans.**
**S2 — Ingen handler läser den verifierade tenanten. 🔴**
`middleware.ts:56-57` sätter `x-0pt-verified-sub` / `x-0pt-verified-tenant` på
den vidarebefordrade requesten. En grep över hela repot visar att **ingen fil
utanför `middleware.ts` läser de headrarna**. Samtliga `/api/0pt/*`-handlers tar
i stället `tenant_id` från request-body eller querysträng
(`app/api/0pt/sources/[sourceId]/query/route.ts:49`,
`documents/capture/route.ts:27`, `lakehouse/mappings/route.ts:27,49`,
`sources/rate-policies/route.ts:26`, m.fl.). Även med fungerande middleware kan
alltså en innehavare av ett giltigt token för tenant A skicka
`{"tenant_id": "t_b"}` och arbeta i tenant B. Middlewarens 403 vid
`X-Tenant-Id`-konflikt hjälper inte — den kontrollerar bara *headern*, inte
*bodyn*. **Tenantisolering är inte implementerad.**
**S3 — Hela det icke-`/api/0pt`-prefixade API:t saknar auth.**
Matchern täcker bara `/api/0pt/:path*`. Ordrar, adminmetrik, betalningar,
prenumerationer, uppladdningar, AI-parsning och DB-grants är oskyddade i koden.
*(live-verifierat: `GET /api/admin/metrics` och `GET /api/orders` svarar 200
anonymt.)* De är tomma idag enbart för att produktionsinstansen saknar databas.
**S4 — Anonym anropare kan trigga escrow-slutbetalning och flagga leverans.**
`POST /api/payments/{order_id}/release` kräver ingen identitet. Den skapar en
Stripe-checkout på `order.balance_ore` och kör därefter
`setStatus(order_id, "delivered")` **oavsett om betalningen genomförs**.
Statusövergången sker alltså på begäran, inte på betalning. (Skyddas i praktiken
idag bara av att `getOrder` inte hittar något utan databas.)
**S5 — CL0UD-policyklienten är fail-open utan produktionsspärr.**
`src/clients/cl0ud-client.ts:45` — saknas `CL0UD_URL` returneras alltid
`{decision: "allow"}` utan nätanrop. Till skillnad från auth finns **ingen**
`assertAuthConfigSafe`-motsvarighet, ingen `NODE_ENV`-kontroll och ingen varning.
En produktionsdeploy som tappar variabeln godkänner tyst varje
sidoeffektsbärande capability, inklusive Iceberg-writes (L4).
**S6 — AUD0-audit är no-op utan produktionsspärr.**
`src/clients/aud0-client.ts` — saknas `AUD0_URL` skrivs inga auditevent och inga
lineage-event, tyst. Kombinerat med S5 innebär det: inga beslut fattas, inga
spår lämnas. `sql/001_init.sql` har en `audit_log`-tabell som TypeScript-lagret
aldrig skriver till.
**S7 — `db-grant`-routen påstår kryptering som inte finns.**
`app/api/connections/db-grant/route.ts` kommenterar att anslutningssträngen
"lagras KRYPTERAT (AES-GCM via `GRANT_ENC_KEY`), aldrig i klartext". Koden gör
ingenting av detta: utan `GRANT_ENC_KEY` returneras `501`, och **med** nyckeln
returneras ett slumpat `grant_id` — ingen validering av read-only, ingen
kryptering, ingen lagring, ingen audit. Kommentaren beskriver en oimplementerad
funktion i presens.
**S8 — FastAPI-engine har ingen autentisering.**
`services/api/orchestrator/main.py` (12 endpoints, inkl. `POST /orders`,
`PATCH /orders/{oid}`, `POST /orders/{oid}/run`) saknar varje form av
auth-dependency. `lib/engine.ts` skickar `X-Internal-Token`, men servern läser
den aldrig. Var engine än exponeras är den öppen för alla som når porten.
**S9 — Compliance-koden motsäger produktlöftet. 🔴**
`services/scrapers/waf_bypass.py` implementerar FlareSolverr, Tor-rotation,
cloudscraper, TLS-fingerprintbyte, gratis proxypool, "undetected browser"
(nodriver) och "fake identity generation".
`services/infra/docker-compose.data-stack.yml` startar `flaresolverr` och
`tor-proxy`. `services/api/workers/adaptive_router.py` inleds med
`"""Adaptive router — all methods, no compliance restrictions."""`.
Detta står i direkt konflikt med `CLAUDE.md` §1, `README.md` ("Bypass/stealth/
proxy-rotation/fake-account paths are excluded by design") och med hela den
juridiska positionering som säljs på `0pt.app`. Det är samtidigt en
**affärsrisk**: att marknadsföra laglig sourcing medan bypass-kod ligger i repot
är svårt att försvara vid en granskning.
**S10 — Godtycklig anslutningskonfiguration från request-body.**
`POST /api/0pt/sources/{sourceId}/query` tar `connection_config` direkt ur bodyn
och skickar den till adapterbygget (`src/services/source-query-service.ts:95`).
Utan fungerande auth (S1) innebär det att en anonym anropare kan få tjänsten att
öppna utgående anslutningar mot en angiven värd — SSRF-liknande primitiv, och en
väg att låta 0pt-instansen agera proxy.
**S11 — Typkontrollen täcker inte auth-koden.**
`next.config.mjs` sätter `typescript.ignoreBuildErrors: true`, och
`tsconfig.check.json` inkluderar endast `src/**/*.ts` + `tests/**/*.ts`.
**`middleware.ts` ligger i roten och typkontrolleras därför i praktiken inte i
CI** — trots att det är hela authseamen. `eslint` är dessutom rådgivande
(`continue-on-error: true`).
**S12 — Sessions-scaffoldet är svagt.**
`lib/auth.ts` signerar `{email, iat}` med HMAC-SHA256 mot `AUTH_SECRET`, utan
`exp`-kontroll vid verifiering (bara cookiens `maxAge`), utan rotation och utan
bindning till en användarpost. Kommentaren erkänner att det är ett scaffold; det
är ändå den mekanism `GET /api/auth/me` litar på.
---
## 8. Öppna frågor
1. **Varför kör inte middlewaren i produktion (S1)?** Måste rotorsaksanalyseras.
Ingen dokumentation får påstå att `/api/0pt/*` är skyddat innan en anonym
request faktiskt får 401 från `0pt.app`.
2. **Ska `waf_bypass.py`, FlareSolverr och tor-proxy raderas (S9)?**
`CLAUDE.md` säger uttryckligen "remove or disable it". Commit `4a164c5` gick
åt motsatt håll. Detta är ett Simon-beslut med både juridisk och
marknadsföringsmässig innebörd — inte något en session ska avgöra ensidigt.
3. **Vercel eller flottan?** 0pt:s enda körande instans ligger på Vercel medan
estate-projektet *GCP/Vercel-exit* pekar bort därifrån. `Dockerfile` är redan
Coolify-anpassad men ingen nod är tilldelad. `siax.config.json` i det här
repot deklarerar därför `target: "vercel"`, `node: null` — det är dagens
sanning, inte målbilden.
4. **Ska capability-lagret vara i drift alls?** Det är exponerat publikt men helt
in-memory och utan delad state mellan routemoduler (§4.2). Antingen backas det
av Postgres, eller så bör routes stängas tills dess.
5. **Var är `sql/002_*` och `sql/003_*`?** Tre filer refererar dem. Fanns de
någonsin, eller är referenserna aspirationella? `opt_sources` har ingen
migration här.
6. **Ingen databas i produktion.** Är `0pt.app` avsedd att vara en publik
marknadsföringssajt tills vidare, eller är det ett driftfel att
`POSTGRES_URL` aldrig satts? Ordrar som skapas idag försvinner.
7. **`package.json` heter fortfarande `"my-v0-project"`** — v0-exportartefakt.
Byte påverkar inget bygge men bör göras för spårbarhet.
8. **Designsystem:** repot har ett eget dokumenterat mono/editorial-system
(`docs/05-design-system.md`) + shadcn/ui och konsumerar **inte** `@siax/ui`.
Ska 0pt konvergera mot estate-standarden eller är avvikelsen medveten?
`siax.config.json` sätter därför `designSystem: null` snarare än att påstå
något osant.
9. **Tenantmodell:** är en 0pt-tenant 1:1 med en Zitadel-org? Koden antar det i
`deriveTenantId` men det är inte bekräftat mot CL0UD:s tenantregister.
10. **Infisical:** hemligheter ligger i Vercel-env, inte i credential-SSOT.
Behöver bootstrappas.
11. **`GET /api/screen`** proxar mot OpenSanctions utan avtal — kommentaren i
koden säger själv "VERIFY commercial terms before production volume".
12. **Konsumentledet är odokumenterat.** `CLAUDE.md` säger att 0pt matar
Protosell/FiveCRM och VehIQ. Inget i repot visar en sådan integration.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "0PT",
"category": "PLATTFORM",
"subcategory": "Data OS",
"owner": "SIAX Technology AB",
"packageName": "@siax/0pt",
"capabilityPrefix": "0pt",
"deploy": {
"target": "vercel",
"node": null,
"domain": "0pt.app",
"healthPath": "/api/health"
},
"consumes": ["CL0UD", "AUD0", "ST0RE", "N0D"],
"decoupled": false,
"designSystem": null
}