Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
29 KiB
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: <int>; 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/<version> |
[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
{ "status": {"code":"S0301","text":"...","type":"SUCCESS"},
"stid": "20180915-app1",
"object": {"type":"contact","value":"100101","summary":1},
"messages": [ {"code":"...","text":"...","status":"ERROR","objects":[...]} ],
"data": [ { } ] }
status.typeEnum:SUCCESS,ERROR,NOTIFY,NOTICE,NICCOM_NOTIFY. [VERIFIED: OpenAPI StatusType]- Code-Praefixe:
SErfolg,EFehler,NBenachrichtigung ("accepted, further processing required"). [CITED: help.internetx.com/x/cQbj] - Die Hilfeseite zeigt das Feld als
resultCode, OpenAPI und die reale 401-Antwort nutzencode. Parser mussstatus.code ?? status.resultCodelesen. [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[].statuspruefen, 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. .deverlangt 2 bis 6 funktionierende Nameserver; DENIC prueft sie bei der Registrierung (Nast-Check), unerreichbare/nicht eingerichtete Nameserver lassen den Auftrag scheitern. ParameternsCheckexistiert. [CITED: namecheap/ans.co.uk Suchergebnis; AutoDNS-Verhalten ASSUMED] - AuftragFAILEDmitmessagesverstaendlich 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/<slug>') + @UseModule('<slug>') Klasse; requireTenantId(req) aus req.tenantId; Schreib-/Pruef-/Einstellungsrouten @ModuleManage('<slug>'); 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'<slug>': { component: dynamic(() => import('@/app/(portal)/modules/<slug>/page'), { ssr: false }) }.apps/web/src/lib/module-identity.ts: Symbol inICONS(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['<slug>'] = '<ns>.title'.apps/web/src/app/(portal)/modules/<slug>/layout.tsx:<ModuleAccessGate moduleSlug="<slug>">und Eintrag inmodule-layouts.test.tsx.- Seite:
PageHeaderaus@/components/layout/page-header,TabBaraus@/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/<name>-api.tsnachnextcloud-status-api.ts(credentials: 'include',NEXT_PUBLIC_API_URL). - Texte in
apps/web/src/messages/de.jsonUNDen.json(Paritaets-Tests); deutsche Texte mit echten Umlauten,umlaut-guard.spec.tslaeuft ueber de.json - neue Woerter ggf. inumlaut-dictionary.ts. - Kein Dashboard-Widget in Etappe 1 (
WIDGET_MODULE_SLUGSunveraendert).
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=<aktive> (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
- Timeout bei
POST /domainist unbekannter Ausgang. Nie automatisch wiederholen. StatusUNKNOWN; Abgleich perGET /domain/{name}(existiert -> bestellt) oderPOST /job/_search; dem Benutzer "Ergebnis ungeklaert, bitte pruefen" zeigen. Kein globaler Retry-Wrapper fuer POST. - 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 alteFAILED/CANCELEDist. AutoDNS bietet keinen dokumentierten Idempotenz-Schluessel; HeaderX-Domainrobot-Ctidexistiert (js-sdkHeaders.js), eine serverseitige Dublettenerkennung darauf ist [ASSUMED] - nur zur Korrelation im Log verwenden. - Demo/Live verwechselt. Umgebung ist Teil jeder Bestellung und jeder Zuordnung; beim Bestaetigen muss
order.environment === config.environmentgelten (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. - Geheimnisse. Passwort nie in Responses, Logs oder Fehlertexten (Basic-Header, Request-Body-Logging aus;
request-log.tsprotokolliert nur Methode/Pfad/Status). Fehlermeldungen gekuerzt; AutoDNS-messages[].textist unkritisch, aber ohnestid-Zwang anzeigen. Entschluesselungsfehler lautstark loggen, nicht als "kein Passwort" werten. - NestJS-Routenreihenfolge.
contacts/import,contacts/availability,registrations/preview,domains/check,connection-teststehen VORcontacts/:id,registrations/:id; im Controller-Spec die Deklarationsreihenfolge pruefen (Vorbildhandelsware-datev.controller.spec.ts). Jede neue Verwalten-Route inmodule-manage-handlers.spec.tseintragen. - 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. - HTTP 200 mit Fachfehler.
status.type==='ERROR'immer auswerten (auchmessages[]), sonst gilt eine fehlgeschlagene Bestellung als erfolgreich. - Kontakt-IDs ueber Umgebungen. Siehe Datenmodell; Zuordnungen nie ohne
environmentauswerten. - Kontakte aendern kann Domains beeinflussen (
confirmOwnerConsentbei Inhaberwechsel gTLDs). Etappe 1: Kontakte nur anlegen/lesen, nicht aendern. - Nameserver-Check .de. Standard-Nameserver in den Einstellungen muessen vorab bei AutoDNS/Anbieter eingerichtet sein, sonst scheitert
.demitFAILED.
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
- 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.
- Preisanzeige in der Zusammenfassung:
PRICE-Dienst liefert Betrag/Waehrung; falls er fehlt oderFAILEDist, Bestellung nur mit Hinweis "Preis nicht ermittelbar" zulassen, nicht stillschweigend ohne Preis. - 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-sdkREADME (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)