--- phase: 13-scraping-adapters-cross-source-dedup plan: 02 type: execute wave: 1 depends_on: [] files_modified: - apps/api/src/tenders/source-registry.ts - apps/api/src/tenders/source-registry.spec.ts - apps/api/src/tenders/adapters/tender-source-adapter.interface.ts autonomous: true requirements: [INGEST-07] must_haves: truths: - "Der Versuch, einen Adapter mit portal 'vergabe24' oder 'aumass' zu registrieren, wirft eine DeniedPortalError-Exception im Code (nicht nur Doku)." - "Ein Test beweist die Ablehnung (Erfolgskriterium 4)." - "Ein Adapter deklariert seine bedienten Portale ueber ein readonly portals-Array (NetServer: 3)." artifacts: - apps/api/src/tenders/source-registry.ts - apps/api/src/tenders/adapters/tender-source-adapter.interface.ts key_links: - "SourceRegistry.register() -> assertNotDenied ueber alle adapter.portals bei DI-Boot." --- Baut das INGEST-07-Rueckgrat: eine `SourceRegistry` mit hartem Denylist-Gate, die vergabe24/aumass strukturell im Code ablehnt (D-06 / Erfolgskriterium 4), plus die Generalisierung des Adapter-Vertrags (`portals`-Array), damit EIN NetServer-Adapter mehrere Portale bedienen kann. Voll getestet, ohne jede Scraping-Abhaengigkeit (D-01). Purpose: Compliance-/Sicherheitsgrenze gehoert testbar in den DI-Layer, bevor irgendein Poll laeuft. Output: source-registry.ts (Registry + DeniedPortalError), source-registry.spec.ts (Refusal-Beweis), generalisiertes TenderSourceAdapter-Interface. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md @.planning/phases/13-scraping-adapters-cross-source-dedup/13-RESEARCH.md @apps/api/src/tenders/adapters/tender-source-adapter.interface.ts @apps/api/src/tenders/adapters/doe-opendata.adapter.ts Task 1: Adapter-Interface generalisieren (portals-Array) apps/api/src/tenders/adapters/tender-source-adapter.interface.ts Erweitere `TenderSourceAdapter` (13-RESEARCH.md Pattern 1) um `readonly portals: readonly string[]` — die Liste der von diesem Adapter bedienten Quell-Portale (DÖE: `['doe-opendata']`; NetServer spaeter: `['tender24','lhs-vpbw','vergabe.landbw']`). `sourceType` und `fetchTenders(dayCursor)` bleiben unveraendert. Aktualisiere den bestehenden `DoeOpenDataAdapter`, sodass er `readonly portals = ['doe-opendata'] as const` deklariert (minimaler additiver Edit, keine Verhaltensaenderung). Kommentar: `portals` entkoppelt "ein Adapter" von "ein Portal" (config-driven multi-portal, INGEST-02). cd apps/api && npx tsc --noEmit -p tsconfig.json Interface hat `portals`; DoeOpenDataAdapter deklariert es; `tsc --noEmit` gruen; bestehende doe-Adapter-Spec bleibt gruen. Task 2: SourceRegistry + Denylist-Gate + Refusal-Test (INGEST-07 / D-06) apps/api/src/tenders/source-registry.ts, apps/api/src/tenders/source-registry.spec.ts - register() eines Fake-Adapters mit portals ['vergabe24'] wirft DeniedPortalError. - register() mit portals ['aumass'] wirft DeniedPortalError. - register() eines legitimen Adapters (portals ['tender24']) fuegt ihn hinzu; get(sourceType) liefert ihn zurueck. - activeAdapters() liefert alle registrierten Adapter. - get() eines nicht registrierten sourceType liefert undefined (kein Throw). - Ein gemischter Adapter (portals ['tender24','aumass']) wird komplett abgelehnt (ein verbotenes Portal genuegt). Implementiere `source-registry.ts` nach 13-RESEARCH.md Pattern 2: exportiere `DENYLISTED_PORTALS = ['vergabe24','aumass'] as const`, Klasse `DeniedPortalError extends Error` (deutsche Meldung: AGB-seitig gesperrt), und `@Injectable() class SourceRegistry` mit `private adapters = new Map()`, `register(adapter)` (iteriert `adapter.portals`, wirft DeniedPortalError bei Treffer in DENYLISTED_PORTALS, sonst set), `get(type)` und `activeAdapters()`. Der Denylist-Vergleich ist der Sicherheitskern (D-06) — harte Code-Exception, NIE nur Doku. Schreibe die Spec RED-first exakt nach behavior-Block; sie ist der Beweis fuer Erfolgskriterium 4. Verwende einen Fake-Adapter-Stub im Test (kein echtes Scraping). Beachte die comment-text-Disziplin: kein negativer grep-Gate auf 'vergabe24'/'aumass' im Datei-Body ausserhalb der Konstante. pnpm --filter @tessera/api test -- source-registry register() wirft DeniedPortalError fuer vergabe24 UND aumass (Test gruen); legitime Registrierung + get()/activeAdapters() funktionieren; kein Prisma/Scraping-Import. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Adapter-Registrierung -> Poll-System | Ein verbotenes Portal darf nie in den aktiven Poll-Pfad gelangen (AGB/Compliance). | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-13-02-01 | Elevation of Privilege | Registrierung eines Denylist-Portals | high | mitigate | Harte DeniedPortalError-Exception in register() bei DI-Boot; Test beweist Refusal (D-06, INGEST-07). | | T-13-02-02 | Tampering | Umgehung des Gates via gemischtem portals-Array | high | mitigate | Iteration ueber ALLE portals; ein verbotenes Portal lehnt den ganzen Adapter ab. | | T-13-02-SC | Tampering | keine neue Dependency in diesem Plan | low | accept | Kern nutzt nur bestehende Deps; node-html-parser erst in 13-04 hinter Checkpoint. | - `pnpm --filter @tessera/api test -- source-registry` gruen (Refusal beweist Erfolgskriterium 4). - `npx tsc --noEmit` gruen. SourceRegistry lehnt vergabe24/aumass im Code ab (getestet); Adapter-Vertrag um portals generalisiert. Erfuellt INGEST-07 strukturell. Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-02-SUMMARY.md` when done