feat(13-02): implement SourceRegistry with hard denylist gate
GREEN — SourceRegistry.register() throws DeniedPortalError when any of an adapter's declared portals is in DENYLISTED_PORTALS (vergabe24, aumass), enforced at DI-registration time (INGEST-07/ D-06), not just documented. get()/activeAdapters() support the Plan 13-03 poll-once-fan-out-many scheduler. 6/6 tests pass, no Prisma/scraping import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import type { SourceType } from './tender.types';
|
||||||
|
import type { TenderSourceAdapter } from './adapters/tender-source-adapter.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portals whose AGB (Nutzungsbedingungen) prohibit automated/scraping
|
||||||
|
* access (INGEST-07/D-06). This is the hard code boundary — NOT just
|
||||||
|
* documentation: `SourceRegistry.register()` checks every adapter's
|
||||||
|
* `portals` against this list and refuses registration outright if any
|
||||||
|
* entry matches, at DI-boot time, before any poll can run.
|
||||||
|
*/
|
||||||
|
export const DENYLISTED_PORTALS = ['vergabe24', 'aumass'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown by `SourceRegistry.register()` when an adapter declares a
|
||||||
|
* denylisted portal. Proves Erfolgskriterium 4 (13-CONTEXT.md D-06) —
|
||||||
|
* see source-registry.spec.ts.
|
||||||
|
*/
|
||||||
|
export class DeniedPortalError extends Error {
|
||||||
|
constructor(portal: string) {
|
||||||
|
super(
|
||||||
|
`Portal '${portal}' ist AGB-seitig für automatisierten Zugriff gesperrt und darf nicht registriert werden.`,
|
||||||
|
);
|
||||||
|
this.name = 'DeniedPortalError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central registry mapping `SourceType` -> `TenderSourceAdapter`, gated by
|
||||||
|
* the AGB denylist (T-13-02-01/T-13-02-02). `pollDueSources` (Plan 13-03)
|
||||||
|
* uses `get()`/`activeAdapters()` to fan out over all registered sources
|
||||||
|
* (poll-once-fan-out-many) instead of hardwiring a single adapter.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SourceRegistry {
|
||||||
|
private readonly adapters = new Map<SourceType, TenderSourceAdapter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an adapter. Iterates ALL of the adapter's declared `portals`
|
||||||
|
* — a single denylisted entry rejects the whole adapter, even if the
|
||||||
|
* rest of its portals are legitimate (T-13-02-02: no partial/mixed
|
||||||
|
* registration).
|
||||||
|
*/
|
||||||
|
register(adapter: TenderSourceAdapter): void {
|
||||||
|
for (const portal of adapter.portals) {
|
||||||
|
if ((DENYLISTED_PORTALS as readonly string[]).includes(portal)) {
|
||||||
|
throw new DeniedPortalError(portal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.adapters.set(adapter.sourceType, adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the adapter for a sourceType, or undefined if none is registered — never throws. */
|
||||||
|
get(type: SourceType): TenderSourceAdapter | undefined {
|
||||||
|
return this.adapters.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All currently registered adapters, for fan-out scheduling. */
|
||||||
|
activeAdapters(): TenderSourceAdapter[] {
|
||||||
|
return [...this.adapters.values()];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user