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
518 lines
32 KiB
Markdown
518 lines
32 KiB
Markdown
# 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.
|