# 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 `/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.