(string) config('provisioning.dns.token')]) ->baseUrl('https://dns.hetzner.com/api/v1') ->timeout(15); } private function zoneId(): string { $zone = (string) config('provisioning.dns.zone'); $zones = $this->http()->get('/zones', ['name' => $zone])->throw()->json('zones', []); return $zones[0]['id'] ?? throw new RuntimeException("DNS zone {$zone} not found"); } public function upsertRecord(string $fqdn, string $type, string $value): string { $zoneId = $this->zoneId(); $name = rtrim(str_replace('.'.config('provisioning.dns.zone'), '', $fqdn), '.'); $existing = collect($this->http()->get('/records', ['zone_id' => $zoneId])->throw()->json('records', [])) ->first(fn ($r) => $r['name'] === $name && $r['type'] === $type); if ($existing) { $this->http()->put("/records/{$existing['id']}", [ 'zone_id' => $zoneId, 'type' => $type, 'name' => $name, 'value' => $value, 'ttl' => 300, ])->throw(); return (string) $existing['id']; } return (string) $this->http()->post('/records', [ 'zone_id' => $zoneId, 'type' => $type, 'name' => $name, 'value' => $value, 'ttl' => 300, ])->throw()->json('record.id'); } public function deleteRecord(string $recordId): void { $response = $this->http()->delete("/records/{$recordId}"); // A record that is already gone is the outcome we wanted. Without this, // a lost response or a retry after a later failure turns every further // attempt into a 404 — and a host that can then never be purged. if ($response->notFound()) { return; } $response->throw(); } }