Files
0pt/docs/architecture/ARCHITECTURE.md
T
admin a1557b2a7d
CI (SIAX Cloud) / integration (push) Has been skipped
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 14s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 30s
CI (SIAX Cloud) / quality (push) Successful in 35s
CI (SIAX Cloud) / integration (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m31s
feat(payments): bind orders to verified tenant and buyer
2026-09-15 19:53:58 +02:00

518 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 0PT — Arkitektur
**Kategori:** platform (K1) **Underkategori:** data-os **Status:** under utveckling (deployad frontend i drift, capability-lagret ej produktionsklart)
**Senast verifierad mot kod:** 2026-08-07 (commit `6531384`, main) · **Klassificeringsuppdatering:** 2026-08-10 (`siax.repo.json`) · **v2.0-standardmigrering (§5 tillagd, §6–§9 omnumrerade; §6 Deploy-fältet nedan uppdaterat mot `lib/health-checks.ts`):** 2026-08-11 — scoped till dessa avsnitt, ingen fullständig radrad-omverifiering av hela dokumentet gjord denna pass
> **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 §8. 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.
### 1.1 Klassificering (SSOT: `siax.repo.json` / `siax.config.json`, VERIFIERAT 2026-08-10)
| Fält | Värde |
|---|---|
| category / subcategory | `platform` (K1) / `data-os` |
| appId / packageName | `0PT` / `@siax/0pt` |
| capabilityPrefix | `0pt` |
| node | `null` (ingen SIAX-nod tilldelad — ingen drift på flottan än) |
| domains | `0pt.app` (live) · `api.0pt.app` (target per `.env.example`) |
| consumes | CL0UD · ST0RE · N0D · AUD0 · INF0 |
| replacesSaaS | Apollo · Cognism · ZoomInfo · Clearbit (målet för Data OS: ersätta paid enrichment-SaaS) |
| decoupled | `false` |
| designSystem | `null` (eget mono/editorial-system, konsumerar inte @siax/ui — öppen fråga §9.8) |
Ersätter krav på betald enrichment-SaaS (Apollo/Cognism/ZoomInfo/Clearbit) med
egen compliant sourcing + provenance — målet är att Datahub/VehIQ/data-avdelningar
konsumerar `0pt.*`-capabilities i stället för att köpa listor. *(ANTAGANDE om
konsumentledet: inga inkommande integrationer är verifierade i detta repo.)*
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 §8 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 (§8 S5) |
| AUD0 (audit) | `src/clients/aud0-client.ts` | `AUD0_URL` | **No-op** — inga auditevent skrivs (§8 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 (§8 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 §8 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, §8 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 (§8 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) |
| Readiness | `GET /api/ready` | 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 | Verifierad buyer-principal i source; UI-SSO återstår |
| **Lista buyer-ordrar** | `GET /api/orders` | **L3** | Verifierad tenant och tenant-scoped SQL i source |
| Läs/ändra buyer-order | `GET,PATCH /api/orders/{id}` | L3 | Verifierad tenant och tenant-scoped SQL i source |
| **Adminmetrik (orderpipeline, belopp)** | `GET /api/admin/metrics` | **L3** | Route-auth i source; äldre liveobservation ej omverifierad |
| Deposition (Stripe checkout) | `POST /api/payments/{order_id}/deposit` | **L4** | Verifierad tenant + ägd order i source; live-UI återstår |
| **Frigör escrow-slutbetalning** | `POST /api/payments/{order_id}/release` | **L4** | Ägd order; `delivered` först efter betald webhook; live-UI återstår |
| 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`; §8 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 §8 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 §8 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. Ytor och paritet
**Migrering till standard v2.0 (BACKLOG P1-20/DOC-02):** denna sektion är ny —
v1.0-mallen hade 8 rubriker, v2.0 har 9 (Deploy/Drift/Säkerhet/Öppna frågor
numrerade om §6–§9 nedan, tidigare §5–§8).
Källa: `cl0ud packages/capability-registry/src/registry.ts` (samma verifiering
som `docs/02-architecture.md`s "Six-surface capability model"-avsnitt, ej
omgjord här). 14 `0pt.*`-kapabiliteter är registrerade. Registret har fält för
`api`/`cli`/`mcp`/`ui`/`job`; "CL0UD-konsol" nedan är en separat fråga (om
kapabiliteten är inbäddad i den delade cl0ud-konsolen) som INTE går att
verifiera från detta repo — markerad OKÄNT genomgående, inte gissad.
"Egen konsol" är **ANTAGANDE baserat på sidnamn/routestruktur under `app/`**,
inte spårat anrop-för-anrop mot varje handler — verifiera innan den läses som
hårt bevisad.
| Kapabilitet | API | CLI | MCP | Egen konsol | CL0UD-konsol |
|---|---|---|---|---|---|
| `0pt.order_create` | ✅ | ❌ | ❌ | ✅ (`app/bestall/*`) | OKÄNT |
| `0pt.order_payment` | ✅ | ❌ | ❌ | ✅ (`app/bestall/forskott`) | OKÄNT |
| `0pt.estimate_compute` | ✅ | ❌ | ❌ | ✅ (`app/bestall/beskriv`, ANTAGANDE) | OKÄNT |
| `0pt.catalog_list` | ✅ | ❌ | ❌ | ✅ (`app/katalog`) | OKÄNT |
| `0pt.source_registry_list` | ✅ | ❌ | ❌ | ✅ (`app/kallor`, `app/admin/sources`) | OKÄNT |
| `0pt.query_source` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.vehicle_lookup` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.screen_sanctions` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.document_ocr` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.lakehouse_mapping_manage` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.source_rate_policy_manage` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.lakehouse_write` | ✅ (+job) | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.write_iceberg_table` | ✅ (+job) | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT |
| `0pt.write_external_record` | ❌ (job-only, `apiEnabled: false`) | ❌ | ❌ | ❌ | OKÄNT |
**Luckor, sammanfattat:** CLI = 0/14, MCP = 0/14 (matchar BACKLOG P1-5/P1-6:
ingen CLI-yta, ingen MCP-yta alls finns i detta repo ännu). API = 13/14 (allt
utom det renodlade job-kapabiliteten). Egen konsol ≈ 5/14, koncentrerat till
order-/katalog-/källflödet; kapabiliteter under `app/api/0pt/*` (lakehouse,
rate-policy, källfråga, sanktionsscreening, dokument-OCR) har ingen synlig UI
alls — matchar BACKLOG P1-8 ("`/api/0pt/*`-kapabiliteterna saknar UI").
---
## 6. 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 — fortfarande **OKÄNT** om någon Coolify-app pekar på repot (2026-08-09: `git.siax.io/sax3l/0pt` genomsökt, ingen egen fleet-åtkomst att fråga Coolify API direkt denna pass). `Dockerfile` är skriven för Coolify (node:24-alpine, multistage, `HEALTHCHECK` mot `/api/health`, kör som `node`-användare). `siax.config.json` deklarerar nu `deploy.target: "coolify"` (ändrat från `"vercel"` 2026-08-09, PR i `fix/remove-vercel-deploy-target`) och ett nytt `.gitea/workflows/deploy.yml` finns, ported från n0d/st0re-mönstret — men appens UUID i den filen är en medveten platshållare som får jobbet att faila högljutt tills en riktig Coolify-app skapats och tokens/uuid fyllts i. Config-flippen ändrar alltså **avsikt**, inte trafik. |
| **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 §9.
---
## 7. Drift
### Hälsokontroll
**Fixat (BACKLOG P0-3):** `GET /api/health` → `{ok, service:"0pt-web", db, queue,
engine, stripe, ts}`. `db`/`engine`/`stripe` rapporterar nu en riktig
beroendeprobe (`lib/health-checks.ts`): `"ok"` = lyckad live-anrop, `"error"` =
konfigurerad men onåbar (se `detail`), `"not_configured"` = env-variabel ej
satt. `queue` kan bara vara `"not_configured"`/`"not_integrated"` — ingen
Redis-klient finns i repot, så en satt `REDIS_URL` rapporteras ärligt i stället
för att låtsas testa en anslutning som aldrig görs. `ok` är `true` endast när
ingen konfigurerad dependency ligger i `"error"`. HTTP-statusen är fortsatt
alltid 200 (liveness — processen svarar) oavsett dependency-läge; en extern
beroendekrasch ska inte omstarta instansen. Ny `GET /api/ready` failar stängt
(503) när databasen (den enda lastbärande dependencyn idag) inte både är
konfigurerad och nåbar — peka en Coolify/lastbalanserar-readiness-probe dit,
inte mot `/api/health`. Engine har ett separat `GET /health` som `checkEngine()`
nu faktiskt anropar (tidigare kollades bara `ENGINE_URL`-närvaro).
### 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/`.
---
## 8. 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`.
Om Stripe-konfiguration saknas svarar den nu `503` i stället för att
kvittera ett obearbetat event med `200`; saknad signatur ger `400`.
Deposit-checkout bär nu order-id/kind i sessionsmetadata. Webhooken
flyttar bara en order efter verifierad signatur, `payment_status=paid`,
matchande belopp och valuta. En skapad slutbetalnings-Checkout markerar
inte längre ordern `delivered` innan betalt event har verifierats.
Deposit-sidans demo-fallback kan inte längre märka en misslyckad
betalning som lyckad. Klient-PATCH kräver nu verifierad auth, följer
tillåtna statusövergångar och kan aldrig skriva `in_production`/`delivered`;
concurrent writes använder jämför-och-sätt. Slutbetalningsrouten kräver
också verifierad auth. Ny order-POST kräver nu en verifierad
Zitadel-principal; dennes tenant och subject lagras i
`orders.tenant_id`/`buyer_subject` via `sql/011`. Buyer GET/list/PATCH
och deposit/release använder tenant-predikat i SQL; legacy-ordrar utan
owner är osynliga och obetalbara. **Fortfarande öppet före live-läge:**
migrationen har bara smoke-testats på engångs-Postgres och måste appliceras
i målmiljön före deploy. Browser-UI saknar produktions-SSO/Bearer och
stagingköp/refund är ännu inte verifierat. Beslut och nästa steg finns i
`ADR-001-order-ownership.md`.
- `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 — Historiskt anonymt icke-`/api/0pt`-API; delvis åtgärdat i source.**
Matchern täcker bara `/api/0pt/:path*`. Orderkö/adminmetrik, enskild
order-GET, deposit- och release-checkout samt order-PATCH har nu explicita
route-auth-grindar i source. De äldre live-observationerna av anonymt 200
beskriver tidigare deploy, inte verifierat nuläge. Övriga icke-prefixade
API:er kräver egen inventering. Buyer-order-API:t har nu tenant-predikat i
source, men migration, UI-SSO och liveverifiering återstår enligt ADR-001.
**S4 — Historisk escrow-bugg åtgärdad i source; liveköp återstår.**
`POST /api/payments/{order_id}/release` kräver nu route-auth och skapar
Checkout först efter `accepted`. En skapad session sätter inte `delivered`;
endast en signerad, betald och matchande Stripe-webhook får göra det.
Cross-tenant-skydd av buyer-ordern finns nu i SQL-predikat, men är inte
produktionsbevisat innan `sql/011` har applicerats, UI-SSO är kopplat och
stagingköp/refund är verifierat. Livebetalning får därför inte aktiveras.
**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å.
---
## 9. Ö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?** *(config-delen löst 2026-08-09, trafik-delen kvarstår.)*
0pt:s enda **körande** instans ligger fortfarande på Vercel — den ändringen är
medvetet INTE gjord här (skulle kräva en egen cutover-plan: verifiera att inget
riktigt kunddata/trafik ligger bakom `0pt.app` — vilket är fallet idag, se §6/§7:
`/api/health` visar `db/queue: not_configured`, ordrar lever bara in-memory —
provisionera en Coolify-app, peka DNS om, och avveckla Vercel-projektet, i den
ordningen). Det som ÄR gjort: `siax.config.json` deklarerar nu `target: "coolify"`
(matchar varje annat SIAX Cloud OS-repo) och `.gitea/workflows/deploy.yml` finns
med en medveten platshållar-UUID som failar tills en riktig Coolify-app skapats.
Ingen nod är tilldelad (`node: null`, matchar st0re/aud0:s mönster). Se README
"Deploy" för samma sammanfattning.
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.