# Quick 261008-dts: Modul "Domains" (AutoDNS) - Research **Researched:** 2026-10-08 **Domain:** AutoDNS/InterNetX Domainrobot JSON API + Tessera-Modulmuster **Confidence:** MEDIUM (Endpunkte und Schemas HIGH aus OpenAPI; reale Antworten der Auftrags-Endpunkte und Demo-Kontext nicht ohne Zugangsdaten pruefbar) Es gibt keine CONTEXT.md fuer diese Aufgabe. Es wird kein neues npm-Paket benoetigt (HTTP ueber `undici` 7.28.0, `class-validator`, `@nestjs/schedule` sind bereits in `apps/api/package.json`), daher entfaellt das Package Legitimacy Audit. [VERIFIED: apps/api/package.json Zeilen 25, 32, 52] ## Project Constraints (aus CLAUDE.md / Memory) - UI-Texte Deutsch, siezen; keine "Mandant"-Begriffe in neuen UI-Texten; keine Lizenzierungs-/Mandantenfaehigkeits-Themen anschneiden. - Keine firmenspezifischen Werte hart in generische Admin-UI (Standard-Nameserver also nur als Einstellung, nicht als Konstante). - Neue Module: `@UseModule(slug)` auf Klassenebene ist Pflicht, Schreib-/Pruefrouten zusaetzlich `@ModuleManage(slug)`; NIE ein Rollen-Decorator auf Verwalten-Handlern. - Statische Routen VOR `:id`-Routen (Unit-Tests fangen das nicht, Reihenfolge im Controller-Spec festschreiben). - Zugangsdaten nie an den Client zurueckgeben (LDAP-Muster: Maske `'********'`). - Tests mit gemockter API; kein Docker-Deploy auf Testserver durch Claude. - Alle Aenderungen laufen ueber einen GSD-Workflow. ## Summary Die AutoDNS-JSON-API deckt alle vier Etappe-1-Funktionen ab. Es gibt KEINEN `POST /domain/_check`; die Verfuegbarkeitspruefung laeuft ueber `POST /domainstudio` (WHOIS-Dienst je Domain, optional PRICE). `POST /domain` ist asynchron und liefert einen Job; der Verlauf wird ueber `GET /job/{id}` verfolgt. Die Antwort-Huelle ist fuer alle Routen gleich (status/stid/object/data/messages). Im Code ist das Nextcloud-Status-Modul (Controller/Seed/Modul) plus Handelsware-Datev (Einstellungs-Tab, Singleton-Konfiguration) das beste Vorbild; die AES-Verschluesselung kommt aus `CryptoService`. Der groesste fachliche Risikopunkt ist Geldsicherheit: `POST /domain` darf pro Bestellung hoechstens einmal abgeschickt werden, und ein Timeout heisst NICHT "nicht bestellt". **Primary recommendation:** Zwei Umgebungen (DEMO/LIVE) mit je eigenem Zugang, feste Basis-URLs (keine freie URL-Eingabe), lokale Bestelltabelle mit atomarem Statuswechsel DRAFT -> SUBMITTING vor dem API-Aufruf, Kontakte und Domains live aus AutoDNS lesen und nur die Kundenzuordnung lokal halten. ## Architectural Responsibility Map | Capability | Primary Tier | Secondary | Rationale | |---|---|---|---| | AutoDNS-Aufrufe, Zugangsdaten | API/Backend | - | Passwort darf nie in den Browser; Basic Auth serverseitig | | Kundenzuordnung Kontakte | Database | API | Nur lokal vorhanden, AutoDNS kennt "Kunde" nicht | | Bestell-Zustand (Idempotenz) | Database + API | - | Atomarer Statuswechsel in DB schuetzt vor Doppelbestellung | | Job-Status-Nachverfolgung | API (Abruf beim Oeffnen + Cron) | Browser (Polling der eigenen API) | Browser spricht nie mit AutoDNS | | Filter/Gruppierung nach Kunde | Browser | API | Kleine Datenmenge, Zusammenfuehren im API-Dienst | | Berechtigung | API (ModuleGuard) | Browser (`useCanManageModule`, nur Anzeige) | Bindend ist nur die API | ## AutoDNS JSON API (verifiziert) Quelle der Schemas: OpenAPI 2.0 `https://raw.githubusercontent.com/InterNetX/domainrobot-api/master/src/domainrobot.json` (Info version `v1`, host `api.autodns.com`, basePath `/v1`), gelesen und ausgewertet am 2026-10-08. [VERIFIED: openapi domainrobot.json] ### Basis, Authentisierung, Limits | Punkt | Wert | Quelle | |---|---|---| | Live-URL | `https://api.autodns.com/v1` | [CITED: help.internetx.com/x/cQbj "Interface addresses"] | | Demo-URL | `https://api.demo.autodns.com/v1` | [CITED: help.internetx.com/x/cQbj; java-domainrobot-sdk README] | | Erreichbarkeit | Beide Hosts antworten von hier mit HTTP 401 ohne Zugangsdaten | [VERIFIED: curl-Probe 2026-10-08, ohne Zugangsdaten] | | Auth | HTTP Basic (Benutzer/Passwort) + Header `X-Domainrobot-Context: `; Alternative Session (`X-Domainrobot-SessionId`), von InterNetX nicht empfohlen ausser bei Zwangs-Timeout | [CITED: help.internetx.com Suchergebnis "Login and Authentication"; js-sdk `DomainRobotService.js` setzt Basic + Context-Header] | | User-Agent | Pflicht ("mandatory") - eigenen setzen, z. B. `Tessera/` | [CITED: help.internetx.com/x/cQbj] | | Rate-Limit | "Only 3 requests per second and IP" - Aufrufe serialisieren, Listen seitenweise, kein paralleles Fan-out | [CITED: help.internetx.com/x/cQbj] | | Verbindungstest | `GET /hello` ("performs no operation, used only for testing the connection and authentication credentials") | [CITED: help.internetx.com/x/cQbj; OpenAPI `/hello`] | | Live-Context | Standard `4` | [CITED: wisecp-Doku und help.internetx.com (Plugin-Seite): "default context for AutoDNS is 4"] | | Demo-Context | UNKLAR: Drittanbieter nennt `1` fuer Demo, die offiziellen SDK-Beispiele nutzen die Demo-URL mit `"4"` | [ASSUMED] - Kontext deshalb je Umgebung als freie Ganzzahl speichern, nicht hartkodieren | | 2FA | OpenAPI kennt Header `X-Domainrobot-2FA-Token`; Hilfeseite sagt 2FA nur fuer XML | [ASSUMED] Widerspruch - dedizierten API-Benutzer OHNE 2FA empfehlen | | `X-Domainrobot-Demo` (boolean Header) | in OpenAPI vorhanden, Wirkung undokumentiert | NICHT verwenden; Umgebung nur ueber die Basis-URL waehlen | Fehlerhafte Anmeldung: Live liefert 401 mit `{"messages":[{"code":"EF00202","text":"User does not exist or password incorrect.","status":"ERROR",...}],"status":{"code":null,"text":null,"type":"ERROR"},"stid":"..."}`; Demo ohne Basic-Header liefert 401 `EF1321001 "The authsession could not be found."`. [VERIFIED: curl-Probe 2026-10-08] Wichtig: wiederholte falsche Anmeldungen koennen den Benutzer sperren [ASSUMED] - Verbindungstest nie in Schleife/Retry. ### Antwort-Huelle ```json { "status": {"code":"S0301","text":"...","type":"SUCCESS"}, "stid": "20180915-app1", "object": {"type":"contact","value":"100101","summary":1}, "messages": [ {"code":"...","text":"...","status":"ERROR","objects":[...]} ], "data": [ { } ] } ``` - `status.type` Enum: `SUCCESS`, `ERROR`, `NOTIFY`, `NOTICE`, `NICCOM_NOTIFY`. [VERIFIED: OpenAPI StatusType] - Code-Praefixe: `S` Erfolg, `E` Fehler, `N` Benachrichtigung ("accepted, further processing required"). [CITED: help.internetx.com/x/cQbj] - Die Hilfeseite zeigt das Feld als `resultCode`, OpenAPI und die reale 401-Antwort nutzen `code`. Parser muss `status.code ?? status.resultCode` lesen. [VERIFIED: Probe + OpenAPI; Abweichung CITED] - `object.summary` = Gesamtzahl bei Listen (Paging). [VERIFIED: OpenAPI ResponseObject "amount of objects found in list tasks"] - HTTP-Status ist nicht verlaesslich fuer Fachfehler: IMMER `status.type === 'ERROR'` bzw. `messages[].status` pruefen, auch bei HTTP 200. [ASSUMED] - Fehlertexte an den Client nur gekuerzt und ohne Header/Zugangsdaten durchreichen (`messages[].text`). ### Routen und minimale Nutzlasten | Zweck | Route | Nutzlast / Hinweis | |---|---|---| | Kontakt anlegen | `POST /contact` | `{"type":"PERSON","fname":"Max","lname":"Muster","address":["Musterstr. 1"],"pcode":"12345","city":"Berlin","country":"DE","email":"x@example.com","phone":"+49 30 123456"}`; `type` Enum `PERSON`, `ORG`, `ROLE`; `ORG` braucht `organization`; `address` ist ein ARRAY; `alias` optional (wird generiert); Antwort `data[0].id` (Ganzzahl). Pflichtfelder sind in der OpenAPI nicht als `required` markiert -> obige Menge ist [ASSUMED], gegen Demo pruefen. `extensions` (z. B. `general.gender`, `it.entityType`) nur TLD-abhaengig, fuer `.de` nicht noetig [ASSUMED]. | | Kontakte listen | `POST /contact/_search` | Body `Query`: `{"filters":[{"key":"lname","value":"%test%","operator":"LIKE"}],"view":{"limit":100,"offset":0},"orders":[{"key":"lname","type":"ASC"}]}`; leere `filters` = alle. Filterbare Schluessel: country, pcode, city, type, title, lname, alias, state, id, email, fname, address, created, phone, organization, comment, updated ... Operatoren: EQUAL, NOT_EQUAL, LIKE, ILIKE, GREATER, LESS, IN, IS_NULL ... [VERIFIED: OpenAPI + js-sdk ContactList.js] | | Kontakt lesen/aendern | `GET/PUT /contact/{id}` | Spaeter; `DELETE /contact/{id}` NICHT in Etappe 1 anbieten | | Verfuegbarkeit | `POST /domainstudio` | Es gibt keine Route `/domain/_check` (alle `/domain*`-Pfade der OpenAPI durchgesehen). Body `DomainEnvelopeSearchRequest`: `{"searchToken":"example","currency":"EUR","checkPortfolio":true,"sources":{"initial":{"tlds":["de"],"services":["WHOIS","PRICE"]}}}`; alternativ `sources.custom.domains:["example.de"]`. Antwort `data[]` = `DomainEnvelope` mit `domain`, `services.whois.data.status` (Enum `FREE`, `ASSIGNED`, `MARKET`, `PREMIUM`, `INVALID`, `ERROR`, `TIMEOUT`, `RESERVED`, `PREMIUM_CLAIM`, `CLAIM`, `OFFER`) und `services.price.data.prices[]` (`amount`, `currency`, `period`). [VERIFIED: OpenAPI Schemas]. Die genaue Kombination `initial.tlds` + `searchToken` bzw. `custom.domains` liefert ein Ergebnis fuer genau die gewuenschte Domain: [ASSUMED], gegen Demo pruefen. Nur `status === 'FREE'` zaehlt als bestellbar; alles andere (auch ERROR/TIMEOUT) = nicht bestellbar. | | Domain bestellen | `POST /domain` | "The operation is asynchronous and creates a job." Body (Domain): `{"name":"beispiel.de","period":{"unit":"YEAR","period":1},"ownerc":{"id":123},"adminc":{"id":123},"techc":{"id":123},"zonec":{"id":123},"nameServers":[{"name":"ns1.example.com"},{"name":"ns2.example.com"}]}`. Contacts als Objekte mit `id` (SDK-Beispiel reicht ganze Kontakt-Objekte; nur `{id}` genuegt vermutlich: [ASSUMED]). Query-Parameter `ignoreWhois`, `nsCheck`, `replyTo` - `ignoreWhois` NIE setzen. Antwort `JsonResponseDataJob`: `data[0]` = Job. Wo genau die Job-Id steht (`data[0].id`, ausserdem vermutlich `object.type="job"`/`object.value`): [ASSUMED], gegen Demo pruefen. `confirmOrder` (Nutzungsbedingungen mancher TLDs) und `period` je nach TLD: [VERIFIED: Feldbeschreibung OpenAPI]. Zusatz `nameServerEntries` nur fuer `.de` und schliesst `nameServers` aus. | | Job lesen | `GET /job/{id}` | Antwort `ObjectJob`: `data[0].job.status` (Enum `RUNNING`, `SUCCESS`, `FAILED`, `CANCELED`, `SUPPORT`, `DEFERRED`, `NOT_SET`, `WAIT`), `.job.subStatus`, `.job.action`, `.object` (Domain-Name), `.job.events[]`. Terminal: `SUCCESS`, `FAILED`, `CANCELED`; `SUPPORT` = Eingriff durch InterNetX noetig (als "Rueckfrage noetig" anzeigen); `WAIT`/`DEFERRED`/`RUNNING` = offen. [VERIFIED: OpenAPI Enum; Terminal-Deutung ASSUMED] | | Jobs suchen | `POST /job/_search` | Query mit Schluesseln `id`, `status`, `object`, `type`, `action`, `created` -> Abgleich nach Timeout (siehe Pitfall 1). | | Domains listen | `POST /domain/_search?keys[]=expire&keys[]=ownerc` | Body `Query` wie bei Kontakten (`view.limit/offset`). Zusatzfelder per wiederholtem Query-Parameter `keys[]` (Format `?keys[]=a&keys[]=b`): zulaessig u. a. `expire`, `ownerc`, `adminc`, `techc`, `zonec`, `nserver`, `autorenew`, `cancelationStatus`, `authinfo`, `certificate`. Statusfelder: `registryStatus` (`ACTIVE`, `HOLD`, `LOCK`, `PENDING` ...), `cancelationStatus`, `action`. [VERIFIED: OpenAPI + js-sdk DomainService.list + php-sdk DomainList.php] | | Domain lesen | `GET /domain/{name}` | Detail/Abgleich nach Timeout. | Spaeter (Architektur offenhalten, nicht bauen): Transfer `POST /domain/_transfer`, Kuendigung `POST /domain/{name}/cancelation`, Zonen `GET/PUT /zone/{name}/{virtualNameServer}`, Auftrag bestaetigen/abbrechen `PUT /job/{id}/_confirm` / `_cancel`, Poll-Nachrichten `GET /poll`. [VERIFIED: OpenAPI paths] Deshalb den AutoDNS-Client als eigene Klasse mit allgemeiner `request(method, path, body, keys)`-Methode plus duenner Fachschicht bauen. ### .de-Besonderheiten (kurz) - Seit 25.05.2018 erhebt DENIC nur noch den Domaininhaber als personenbezogene Kontaktdaten; Admin-C/Tech-C/Zone-C entfallen bei DENIC, zusaetzlich zwei neutrale Adressen (General Request/Abuse). AutoDNS-Objekte kennen weiter `ownerc/adminc/techc/zonec`; sicher ist, den Inhaber vollstaendig mit Anschrift zu fuehren und fuer die uebrigen drei Rollen einen Kontakt zu referenzieren. [CITED: denic.de Pressemitteilung 25.05.2018 laut Suchergebnis; Abbildung auf AutoDNS-Pflichtfelder ASSUMED] - Empfehlung: alle vier IDs im Formular verlangen, Voreinstellung = Inhaber bzw. eigener Firmenkontakt als Technik-/Zonen-Kontakt. - `.de` verlangt 2 bis 6 funktionierende Nameserver; DENIC prueft sie bei der Registrierung (Nast-Check), unerreichbare/nicht eingerichtete Nameserver lassen den Auftrag scheitern. Parameter `nsCheck` existiert. [CITED: namecheap/ans.co.uk Suchergebnis; AutoDNS-Verhalten ASSUMED] - Auftrag `FAILED` mit `messages` verstaendlich anzeigen. - Regeln pro TLD unterscheiden sich stark (`extensions`); Etappe 1 auf Kern-TLDs (.de/.com/.net/.org) beschraenken oder bei fehlenden Pflichtangaben die API-Fehlermeldung durchreichen. ## Codebase-Integration (verifiziert durch Lesen der Dateien) ### Backend: Dateien als Vorlage kopieren | Zweck | Vorlage | Hinweis | |---|---|---| | Modul + Seed beim Start | `apps/api/src/nextcloud-status/nextcloud-status.module.ts`, `nextcloud-status.seed.ts` | `seedModule({slug, name, version, category, description:{de,en}, isSystem:true})`; `onModuleInit` mit try/catch. Kategorie: `'infrastructure'` (existiert, `proxmox`/`nextcloud-status` nutzen sie) oder `'domain-tools'` (existiert, `domaincheck` nutzt sie). [VERIFIED: nextcloud-status.seed.ts; module-categories.service.spec.ts Zeilen 17-20] Empfehlung: `'domain-tools'` zu Domaincheck, Kategorien sind laut 261003-387 durch Admins umbenennbar. | | Controller | `nextcloud-status.controller.ts` | `@Controller('modules/')` + `@UseModule('')` Klasse; `requireTenantId(req)` aus `req.tenantId`; Schreib-/Pruef-/Einstellungsrouten `@ModuleManage('')`; Lese-Routen ohne. | | Singleton-Einstellungen + Tab | `handelsware-datev.controller.ts`, `handelsware-datev.service.ts`, Web `modules/handelsware-datev/page.tsx` + `components/SettingsTab.tsx` | Tab "Einstellungen" nur bei `useCanManageModule`. | | Verschluesselung | `apps/api/src/crypto/crypto.service.ts` (`CryptoModule`) | `encrypt(plain)` -> `iv:authTag:ciphertext` (AES-256-GCM, Schluessel `TESSERA_ENCRYPTION_KEY`, 64 Hex). Entschluesseln-Fehler NICHT schlucken (kein stiller Wechsel auf "ohne Passwort"), wie `ldap-config.service.ts` `decryptBindPassword`. Passwort-Feld im API-Response als `'********'` bzw. `hasPassword: boolean`; leeres Feld beim Speichern = unveraendert (LDAP-Muster `dto.bindPassword \|\| config.bindPassword`). | | HTTP-Client | `apps/api/src/proxmox/proxmox-client.service.ts` | `undiciFetch` (NICHT globales `fetch`), `AbortController`-Timeout, Fehlerdetail gekuerzt (500 Zeichen), Fehlerklassifikation. Fuer AutoDNS: festes Basis-URL-Mapping DEMO/LIVE, kein Eintrag frei waehlbar (kein SSRF), 15-20 s Timeout, Basic-Header aus entschluesselten Daten. | | Hintergrunddienst (Auftragsstatus) | `nextcloud-status-scheduler.service.ts` | `OnApplicationBootstrap`, `SchedulerRegistry` + `require('cron').CronJob`-Umgehung, Ueberlappungsschutz `running`; Systemkontext-Lesen nur ueber `forSystem()` (+ `system_read_policy`). Fuer Etappe 1 genuegt: Status beim Oeffnen/"Aktualisieren" abfragen, optional minuetlicher Cron nur fuer offene Auftraege. | | DTOs | `nextcloud-status/dto/nextcloud-instance.dto.ts` | `class-validator`; globale `ValidationPipe` ist in `main.ts` aktiv. | | DB-Zugriff | `forTenant(this.prisma, tenantId)` aus `prisma/prisma-tenant.extension.ts` | Jeder neue Zugriff in `rls-access-inventory.spec.ts` eintragen, wenn dort Klassifikation verlangt wird. | | Migration | `apps/api/prisma/migrations/20261002130000_handelsware_datev/migration.sql` | Handgeschrieben mit Kopfkommentar; je Tabelle `tenantId`, `ENABLE`+`FORCE ROW LEVEL SECURITY`, `tenant_isolation_policy USING ("tenantId" = current_tenant_id())`; sonst faellt `rls-coverage.spec.ts`. Naechster Zeitstempel > `20261003120000`. | | Registrierungspunkte | `apps/api/src/app.module.ts` (Importliste), `module-manage-handlers.spec.ts` (Pflicht-Test: jede Schreibroute verlangt Manage, Leseroute nicht) | | ### Frontend: Registrierungspunkte (alle noetig, sonst erscheint das Modul nicht korrekt) - `apps/web/src/lib/module-loader.ts`: Eintrag `'': { component: dynamic(() => import('@/app/(portal)/modules//page'), { ssr: false }) }`. - `apps/web/src/lib/module-identity.ts`: Symbol in `ICONS` (z. B. `domaincheck: 'globe'` existiert; neuer Slug braucht eigenen Eintrag; fehlt er, gilt 'tile'). - `apps/web/src/lib/stores/nav-store.ts`: `MODULE_TITLE_KEYS[''] = '.title'`. - `apps/web/src/app/(portal)/modules//layout.tsx`: `` und Eintrag in `module-layouts.test.tsx`. - Seite: `PageHeader` aus `@/components/layout/page-header`, `TabBar` aus `@/components/accounting/tab-bar` (Handelsware-Muster: Tabs Kontakte / Domains / Registrieren / Einstellungen), `useCanManageModule(slug)`. `SettingsSection`/`ControlCenterNav` (`components/control-center/`) gehoeren zum Administrationsbereich (`/admin/...`); fuer ein Modul reicht der Einstellungs-Tab nach Handelsware-Muster. [VERIFIED: ControlCenterNav listet nur /settings und /admin Seiten] - API-Client: `apps/web/src/lib/-api.ts` nach `nextcloud-status-api.ts` (`credentials: 'include'`, `NEXT_PUBLIC_API_URL`). - Texte in `apps/web/src/messages/de.json` UND `en.json` (Paritaets-Tests); deutsche Texte mit echten Umlauten, `umlaut-guard.spec.ts` laeuft ueber de.json - neue Woerter ggf. in `umlaut-dictionary.ts`. - Kein Dashboard-Widget in Etappe 1 (`WIDGET_MODULE_SLUGS` unveraendert). ## Datenmodell-Empfehlung Prinzip: AutoDNS bleibt die Quelle der Wahrheit fuer Kontakte und Domains; lokal liegt nur, was AutoDNS nicht kennt oder was Geld-/Idempotenz-Schutz braucht. | Tabelle (alle mit `tenantId`, RLS) | Inhalt | Begruendung | |---|---|---| | `DomainsConfig` (Singleton je tenantId) | `environment` (`DEMO`/`LIVE`, Standard `DEMO`), je Umgebung `demoUser`, `demoEncryptedPassword`, `demoContext Int?`, `liveUser`, `liveEncryptedPassword`, `liveContext Int? (Standard 4)`, `defaultNameServers` (Text-Array/Json) | Zwei getrennte Zugangssaetze: Umschalter kann nie versehentlich Live-Zugang in Demo tippen und umgekehrt; Wechsel verliert nichts. Passwort-Spalten `iv:authTag:ciphertext`. | | `DomainsCustomer` | `id`, `name` (je Mandant eindeutig), `isOwnCompany Boolean` | Kunden gibt es im System noch nicht (Nextcloud nutzt nur Freitext `customerName`). Eigene kleine Tabelle erlaubt Gruppieren/Filtern ohne Tippfehler-Duplikate; die eigene Firma ist ein Eintrag mit Markierung. Kein Firmenname als Vorgabe. | | `DomainsContactAssignment` | `environment`, `autodnsContactId Int`, `customerId` (FK `DomainsCustomer`, `onDelete: Restrict`) ; `@@unique([tenantId, environment, autodnsContactId])` | AutoDNS-Kontakt-IDs von Demo und Live kollidieren zwangslaeufig, daher Umgebung im Schluessel. Kontakte selbst werden NICHT gespiegelt. | | `DomainsOrder` | `id`, `environment`, `domainName`, `status` (`DRAFT`, `SUBMITTING`, `SUBMITTED`, `SUCCESS`, `FAILED`, `UNKNOWN`, `CANCELED`), `jobId Int?`, `payload Json` (Kontakt-IDs, Nameserver, Laufzeit, angezeigter Preis/Waehrung), `createdByUserId`, `confirmedByUserId`, `confirmedAt`, `errorText`, `lastCheckedAt` ; Teil-Unique auf offene Bestellungen je (`tenantId`,`environment`,`domainName`) | Idempotenz, Nachverfolgbarkeit, Audit "wer hat Geld ausgegeben". | | Kein lokaler Domain-Cache in Etappe 1 | Domainliste wird live seitenweise gelesen (`view.limit`), `ownerc` per `keys[]` mitgeholt; Kunde der Domain = Kunde des Inhaber-Kontakts (aus `DomainsContactAssignment`) | Immer aktuell, keine Synchronisationsfehler. Bei vielen Hundert Domains oder AutoDNS-Ausfall spaeter Snapshot-Tabelle + Cron nachruesten. Kurzer In-Memory-Zwischenspeicher (z. B. 60 s) pro (tenant, environment) schuetzt das 3-Aufrufe-pro-Sekunde-Limit. | "Kunde" einer Domain, die der Inhaber-Kontakt nicht zuordenbar macht (Kontakt ohne Zuordnung): als "Nicht zugeordnet" anzeigen und im Filter anbieten. Spaetere Abweichung (Domain gehoert anderem Kunden als ihr Inhaber) ist eine eigene Zuordnungstabelle in Etappe 2 - Architektur laesst das zu. ## Architektur-Muster fuer die Registrierung (Geldsicherheit) ``` Browser --1 Pruefen(name,tld)--> API --> POST /domainstudio (WHOIS+PRICE) --> Ergebnis FREE? Browser --2 Entwurf(Kontakte,NS,Laufzeit)--> API: legt DomainsOrder DRAFT an, liefert Zusammenfassung + orderId Browser --3 "Verbindlich registrieren" (orderId, Haken gesetzt)--> API: UPDATE ... SET status='SUBMITTING' WHERE id=? AND status='DRAFT' AND environment= (Zaehler == 1 ?) nein -> 409 "Bereits abgeschickt" ja -> POST /domain (EINMAL, KEIN Retry) -> Job-Id speichern, status='SUBMITTED' Netzwerkfehler/Timeout/unlesbare Antwort -> status='UNKNOWN' (nie zurueck auf DRAFT) Browser/Cron --4 Aktualisieren--> GET /job/{id} -> SUCCESS/FAILED/... ; bei UNKNOWN: POST /job/_search bzw. GET /domain/{name} zum Abgleich ``` ## Don't Hand-Roll | Problem | Nicht bauen | Stattdessen | Warum | |---|---|---|---| | Verschluesselung | eigenes Krypto | `CryptoService` | AES-256-GCM, einheitlicher Schluessel | | Moduleberechtigung | eigene Rollenpruefung | `@UseModule` + `@ModuleManage` | Gruppen-/Direktfreigaben, Admin-Kurzschluss | | Mandantenschutz | WHERE tenantId von Hand | `forTenant()` + RLS-Migration | RLS-Tests erzwingen es | | HTTP/Timeout | neuer Client-Stil | `undici` wie Proxmox | bewaehrt, testbar | | Domainnamen-Pruefung | Regex-Eigenbau | Pruefung per Muster `^[a-z0-9-]+\.[a-z.]{2,}$` nur als Vorfilter; Gueltigkeit entscheidet DomainStudio-Status `INVALID` | IDN/TLD-Sonderfaelle | ## Common Pitfalls 1. **Timeout bei `POST /domain` ist unbekannter Ausgang.** Nie automatisch wiederholen. Status `UNKNOWN`; Abgleich per `GET /domain/{name}` (existiert -> bestellt) oder `POST /job/_search`; dem Benutzer "Ergebnis ungeklaert, bitte pruefen" zeigen. Kein globaler Retry-Wrapper fuer POST. 2. **Doppelklick / zwei Browser-Tabs / Wiederholen nach Fehler.** Schutz nur serverseitig durch atomaren Compare-and-Set in der DB (`updateMany ... where status='DRAFT'`, Zaehler pruefen); Button-Sperre im Browser ist nur Komfort. Eine neue Bestellung derselben Domain erst, wenn die alte `FAILED`/`CANCELED` ist. AutoDNS bietet keinen dokumentierten Idempotenz-Schluessel; Header `X-Domainrobot-Ctid` existiert (js-sdk `Headers.js`), eine serverseitige Dublettenerkennung darauf ist [ASSUMED] - nur zur Korrelation im Log verwenden. 3. **Demo/Live verwechselt.** Umgebung ist Teil jeder Bestellung und jeder Zuordnung; beim Bestaetigen muss `order.environment === config.environment` gelten (sonst 409). Deutlicher, dauerhafter Banner "DEMO-System" bzw. "LIVE - kostenpflichtig" in Modulkopf und Bestaetigungsdialog; Live-Registrierung verlangt ausdrueckliches Haeckchen "verbindlich registrieren (kostenpflichtig)". Umgebungswechsel nur mit Verwalten und mit Bestaetigungsdialog; Standard fuer neue Installation: DEMO. Basis-URL ist Aufzaehlung, nie Freitext. 4. **Geheimnisse.** Passwort nie in Responses, Logs oder Fehlertexten (Basic-Header, Request-Body-Logging aus; `request-log.ts` protokolliert nur Methode/Pfad/Status). Fehlermeldungen gekuerzt; AutoDNS-`messages[].text` ist unkritisch, aber ohne `stid`-Zwang anzeigen. Entschluesselungsfehler lautstark loggen, nicht als "kein Passwort" werten. 5. **NestJS-Routenreihenfolge.** `contacts/import`, `contacts/availability`, `registrations/preview`, `domains/check`, `connection-test` stehen VOR `contacts/:id`, `registrations/:id`; im Controller-Spec die Deklarationsreihenfolge pruefen (Vorbild `handelsware-datev.controller.spec.ts`). Jede neue Verwalten-Route in `module-manage-handlers.spec.ts` eintragen. 6. **Rate-Limit 3 Anfragen/s/IP.** Einen Limiter (Warteschlange mit 350 ms Abstand je Prozess) im AutoDNS-Client, Listen in Seiten zu je 100-250; die Domainliste nicht pro Zeile mit `GET /domain/{name}` anreichern. 7. **HTTP 200 mit Fachfehler.** `status.type==='ERROR'` immer auswerten (auch `messages[]`), sonst gilt eine fehlgeschlagene Bestellung als erfolgreich. 8. **Kontakt-IDs ueber Umgebungen.** Siehe Datenmodell; Zuordnungen nie ohne `environment` auswerten. 9. **Kontakte aendern kann Domains beeinflussen** (`confirmOwnerConsent` bei Inhaberwechsel gTLDs). Etappe 1: Kontakte nur anlegen/lesen, nicht aendern. 10. **Nameserver-Check .de.** Standard-Nameserver in den Einstellungen muessen vorab bei AutoDNS/Anbieter eingerichtet sein, sonst scheitert `.de` mit `FAILED`. ## Validation Architecture | Property | Value | |---|---| | Framework | Vitest 3.2.6 (apps/api), 4.1.9 (apps/web) [VERIFIED: CLAUDE.md Stack-Tabelle] | | API-Mock | AutoDNS-Client hinter Schnittstelle (`AutodnsClient`), im Test durch Fake ersetzt; HTTP-Schicht mit `undici` `MockAgent` pruefen: Header (`Authorization`, `X-Domainrobot-Context`, `User-Agent`), URL je Umgebung, Timeout | | Quick run | `pnpm --filter api exec vitest run src/autodns-domains` | | Pflicht-Tests | Controller-Reihenfolge; `module-manage-handlers.spec.ts` erweitern; Passwort nie in Response (`'********'`/`hasPassword`); Bestellung: zweiter Bestaetigungsaufruf -> 409 und `POST /domain` genau einmal; Timeout -> `UNKNOWN` ohne Retry; Umgebungswechsel zwischen DRAFT und Bestaetigung -> 409; HTTP 200 + `status.type=ERROR` -> Fehler; `rls-coverage.spec.ts` und RLS-Inventar; de/en-Paritaet der Texte | | Echtprobe | Erst nach Eintrag von Demo-Zugangsdaten durch den User: Verbindungstest, Kontakt anlegen, Verfuegbarkeit, Bestellung, Job-Antwortform (schliesst die [ASSUMED]-Punkte) | ## Security Domain | ASVS | Gilt | Kontrolle | |---|---|---| | V4 Zugriffskontrolle | ja | `@UseModule` Klasse, `@ModuleManage` auf Registrieren/Kontakt anlegen/Einstellungen/Verbindungstest; `tenantId` nur aus `req.tenantId` | | V5 Eingabevalidierung | ja | `class-validator`-DTOs; Domainname normalisieren (klein, ohne Protokoll/Pfad); Kontakt-/Job-Ids als Integer | | V6 Kryptografie | ja | `CryptoService`, nichts Eigenes | | V9 Kommunikation | ja | nur `https`, feste Hosts (kein SSRF), Zertifikatspruefung an | | V7 Protokollierung | ja | Bestellungen mit Benutzer-Id und Zeitpunkt in `DomainsOrder` (Audit) | ## Assumptions Log | # | Annahme | Abschnitt | Risiko wenn falsch | |---|---|---|---| | A1 | Demo-Context ist unklar (1 laut Drittanbieter, 4 in SDK-Beispielen) | Basis | Verbindungstest Demo scheitert; deshalb frei eintragbar | | A2 | Pflichtfelder Kontakt (type, Name/Organisation, address[], pcode, city, country, email, phone) | Routen | Demo-Fehler beim Anlegen; Formular nachbessern | | A3 | `POST /domainstudio` mit `initial.tlds`+`searchToken` bzw. `custom.domains` liefert genau die gewuenschte Domain mit WHOIS-Status | Routen | Verfuegbarkeitspruefung anders aufbauen | | A4 | Job-Id in Antwort von `POST /domain` unter `data[0].id` | Routen | Statusnachverfolgung muss Antwortform nachziehen; Fallback `POST /job/_search` | | A5 | `{ "id": n }` genuegt als Kontaktverweis in `ownerc/adminc/techc/zonec` | Routen | ganzes Kontakt-Objekt aus `GET /contact/{id}` mitsenden | | A6 | Dedizierter API-Benutzer ohne 2FA; 2FA-Token-Header fuer JSON unsicher | Basis | Anmeldung scheitert bei 2FA-Benutzer | | A7 | HTTP 200 mit Fachfehler moeglich; 429 bei Rate-Limit | Pitfalls | schlimmstenfalls zu vorsichtige Auswertung | | A8 | DENIC/AutoDNS-.de-Pflichten fuer adminc/techc/zonec und Nameserver-Pruefung | .de | Bestellung scheitert mit klarer API-Meldung | | A9 | Falsche Anmeldungen koennen den Benutzer sperren | Basis | Verbindungstest nur einmal je Klick, nie Schleife | ## Open Questions 1. **Existiert beim User ein dedizierter AutoDNS-API-Benutzer (ohne 2FA) und welcher Context gilt fuer Live/Demo?** Klaeren beim Eintragen der Demo-Zugangsdaten; Einstellungsmaske nimmt beides auf. 2. **Preisanzeige in der Zusammenfassung:** `PRICE`-Dienst liefert Betrag/Waehrung; falls er fehlt oder `FAILED` ist, Bestellung nur mit Hinweis "Preis nicht ermittelbar" zulassen, nicht stillschweigend ohne Preis. 3. **Taeglicher Abgleich der Auftraege:** Entscheidung Etappe 1 ohne Cron (Abfrage beim Oeffnen/Aktualisieren) oder mit minuetlichem Cron nur fuer offene Auftraege. Empfehlung: Cron nachruesten, sobald die Demo-Antwortform bekannt ist. ## Sources ### Primary (HIGH) - OpenAPI 2.0 `https://raw.githubusercontent.com/InterNetX/domainrobot-api/master/src/domainrobot.json` - Pfade, Schemas, Enums - `https://help.internetx.com/x/cQbj` (JSON API Basics) - URLs live/demo, Limits, `/hello`, Statuscodes S/E/N - InterNetX SDK-Beispiele: `js-domainrobot-sdk` (`examples/contact|domain|domainstudio`, `src/services/DomainService.js`, `src/lib/Headers.js`), `php-domainrobot-sdk` (`example/domain/DomainList.php`), `java-domainrobot-sdk` README (Demo-URL) - Curl-Proben ohne Zugangsdaten gegen beide Hosts am 2026-10-08 - Codebase: `apps/api/src/nextcloud-status/*`, `apps/api/src/crypto/crypto.service.ts`, `apps/api/src/ldap/ldap-config.service.ts`, `apps/api/src/module-registry/module.guard.ts`, `apps/api/src/handelsware-datev/*`, `apps/api/prisma/migrations/20261002130000_handelsware_datev`, `apps/web/src/lib/module-loader.ts|module-identity.ts|stores/nav-store.ts` ### Secondary/Tertiary (MEDIUM/LOW) - docs.wisecp.com/en/internetx.md (Demo-Context 1, Drittanbieter) - DENIC Pressemitteilung 25.05.2018 und Registrar-Wissensdatenbanken (.de-Regeln, ueber Websuche, nicht im Volltext geprueft) ## Metadata **Confidence:** Standard-Stack HIGH (nichts Neues); AutoDNS-Routen/Schemas HIGH; Antwortformen der Bestell-/Job-Routen MEDIUM; .de-Regeln LOW-MEDIUM **Research date:** 2026-10-08, gueltig ca. 30 Tage (OpenAPI aendert sich selten, aber vor Implementierung gegen Demo gegenpruefen)