handleAuthorize(); // zero-secret registration * if ($path === '/dominio/session') json($dominio->handoff($email, $name, $tenantId, $resource, $purpose, $suggest)); * if ($path === '/dominio/webhook') { $e = $dominio->webhook(); ... } // signed events, null if invalid * $host = $dominio->edgeHost() ?? $_SERVER['HTTP_HOST']; // customer domains through the edge * * Registration: the first call registers automatically — this client issues a nonce, asks Dominio to register it, and * Dominio fetches the integration key from /dominio/authorize. Nobody copies a secret. * Signature of every call and webhook: X-Interfaz-Timestamp + X-Interfaz-Signature = hex(hmac_sha256(ts "\n" body, key)). */ final class DominioClient { /** @var null|callable(string,string,array,string):array{0:int,1:string} test transport */ public static $transport = null; public function __construct(private string $product, private string $dir, private string $base = 'https://dominio.interfaz.digital') { $this->base = rtrim($base, '/'); if (!is_dir($dir)) @mkdir($dir, 0700, true); } private function file(string $name): string { return $this->dir . '/' . $name; } private function read(string $name): array { $x = json_decode((string)@file_get_contents($this->file($name)), true); return is_array($x) ? $x : []; } private function write(string $name, array $v): void { $p = $this->file($name); file_put_contents($p . '.tmp', json_encode($v), LOCK_EX); @chmod($p . '.tmp', 0600); rename($p . '.tmp', $p); } /** Our integration key (generated once, kept in the private directory). */ public function key(): string { $k = (string)($this->read('key.json')['key'] ?? ''); if ($k === '') { $k = bin2hex(random_bytes(32)); $this->write('key.json', ['key' => $k, 'created_at' => gmdate('c')]); } return $k; } /** POST /dominio/authorize: answers Dominio only for the nonce this client issued in the last 5 minutes. */ public function handleAuthorize(): never { $b = json_decode((string)file_get_contents('php://input'), true); $nonce = is_array($b) ? (string)($b['nonce'] ?? '') : ''; $want = $this->read('nonce.json'); header('Content-Type: application/json'); if ($nonce === '' || !hash_equals((string)($want['n'] ?? ''), $nonce) || time() - (int)($want['at'] ?? 0) > 300) { http_response_code(403); echo '{"ok":false,"error":"unknown_nonce"}'; exit; } $this->write('nonce.json', []); echo json_encode(['ok' => true, 'product' => $this->product, 'integration_key' => $this->key()]); exit; } public function register(): bool { $nonce = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); $this->write('nonce.json', ['n' => $nonce, 'at' => time()]); $this->key(); [$code, $raw] = $this->http($this->base . '/api/v1/register', (string)json_encode(['product' => $this->product, 'nonce' => $nonce]), []); return $code === 200 && !empty(json_decode($raw, true)['ok']); } /** @param array $body @return array */ public function call(string $action, array $body = [], bool $retry = true): array { $raw = (string)json_encode(['product' => $this->product] + $body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $ts = (string)time(); [$code, $resp] = $this->http($this->base . '/api/v1/' . $action, $raw, ['X-Interfaz-Timestamp: ' . $ts, 'X-Interfaz-Signature: ' . hash_hmac('sha256', $ts . "\n" . $raw, $this->key())]); $x = json_decode($resp, true); $x = is_array($x) ? $x : ['ok' => false, 'error' => 'http_' . $code]; if ($retry && $code === 401 && in_array((string)($x['error'] ?? ''), ['product_not_registered', 'invalid_signature'], true) && $this->register()) return $this->call($action, $body, false); return $x + ['http_status' => $code]; } /** One-time link into the purchase/connect/manage flow for a person already signed in to the product. */ public function handoff(string $email, string $name, string $tenantId, string $resource, string $purpose = 'buy', string $suggest = '', ?int $personId = null, bool $embed = true): array { return $this->call('handoff', ['email' => $email, 'name' => $name, 'tenant_id' => $tenantId, 'resource' => $resource, 'purpose' => $purpose, 'suggest' => $suggest, 'person_id' => $personId, 'embed' => $embed]); } public function search(string $query): array { return $this->call('search', ['query' => $query]); } public function links(string $tenantId = '', string $resource = ''): array { return $this->call('links', ['tenant_id' => $tenantId, 'resource' => $resource]); } public function connect(string $email, string $tenantId, string $resource, string $hostname): array { return $this->call('connect', ['email' => $email, 'tenant_id' => $tenantId, 'resource' => $resource, 'hostname' => $hostname]); } public function disconnect(string $hostname, string $resource = ''): array { return $this->call('disconnect', ['hostname' => $hostname, 'resource' => $resource]); } public function status(string $hostname): array { return $this->call('status', ['hostname' => $hostname]); } /** Verified webhook payload (domain.active, domain.expiring, domain.expired, domain.renewed, domain.removed) or null. */ public function webhook(?string $raw = null, ?string $ts = null, ?string $sig = null): ?array { $raw ??= (string)file_get_contents('php://input'); $ts ??= (string)($_SERVER['HTTP_X_INTERFAZ_TIMESTAMP'] ?? ''); $sig ??= strtolower((string)($_SERVER['HTTP_X_INTERFAZ_SIGNATURE'] ?? '')); if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300 || !hash_equals(hash_hmac('sha256', $ts . "\n" . $raw, $this->key()), $sig)) return null; $x = json_decode($raw, true); return is_array($x) && ($x['product'] ?? '') === $this->product ? $x : null; } /** Host of a request that arrived through the edge Worker on a customer domain (verified), or null. */ public function edgeHost(): ?string { $host = strtolower(trim((string)($_SERVER['HTTP_X_INTERFAZ_EDGE_HOST'] ?? ''))); $sent = (string)($_SERVER['HTTP_X_INTERFAZ_EDGE_KEY'] ?? ''); if ($host === '' || $sent === '' || !preg_match('/^[a-z0-9.-]{4,190}$/', $host)) return null; $cache = $this->read('edge.json'); if (empty($cache['key']) || time() - (int)($cache['at'] ?? 0) > 3600) { $r = $this->call('edge-key'); if (!empty($r['edge_key'])) { $cache = ['key' => (string)$r['edge_key'], 'at' => time()]; $this->write('edge.json', $cache); } } return !empty($cache['key']) && hash_equals((string)$cache['key'], $sent) ? $host : null; } /** @param list $headers @return array{0:int,1:string} */ private function http(string $url, string $body, array $headers): array { $headers = array_merge(['Content-Type: application/json', 'Accept: application/json'], $headers); if (self::$transport) return (self::$transport)('POST', $url, $headers, $body); $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 6, CURLOPT_HTTPHEADER => $headers]); $raw = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return [$code, is_string($raw) ? $raw : '']; } }