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