isValidName($name)) { return response()->json([ 'available' => false, 'reason' => 'Invalid name. Use 1-63 lowercase alphanumeric characters.', ]); } $alias = $this->rpc->getAliasByName($name); return response()->json([ 'name' => $name, 'available' => $alias === null, 'fqdn' => "{$name}.lthn", ]); } /** * GET /v1/names/lookup/charon */ public function lookup(string $name): JsonResponse { $alias = $this->rpc->getAliasByName(strtolower(trim($name))); if (! $alias) { return response()->json(['error' => 'Name not registered'], 404); } return response()->json([ 'name' => $name, 'fqdn' => "{$name}.lthn", 'address' => $alias['address'] ?? '', 'comment' => $alias['comment'] ?? '', 'registered' => true, ]); } /** * GET /v1/names/search?q=gate */ public function search(Request $request): JsonResponse { $query = strtolower(trim((string) $request->get('q'))); $result = $this->rpc->getAllAliases(); $aliases = $result['aliases'] ?? []; $matches = array_filter($aliases, function ($alias) use ($query) { return str_contains($alias['alias'] ?? '', $query) || str_contains($alias['comment'] ?? '', $query); }); return response()->json([ 'query' => $query, 'results' => array_values($matches), 'count' => count($matches), ]); } /** * GET /v1/names/directory */ public function directory(): JsonResponse { $result = $this->rpc->getAllAliases(); $aliases = $result['aliases'] ?? []; // Group by type from comment metadata $grouped = ['gateway' => [], 'service' => [], 'exit' => [], 'reserved' => [], 'user' => []]; foreach ($aliases as $alias) { $comment = $alias['comment'] ?? ''; if (str_contains($comment, 'type=gateway')) { $grouped['gateway'][] = $alias; } elseif (str_contains($comment, 'type=exit')) { $grouped['exit'][] = $alias; } elseif (str_contains($comment, 'type=service')) { $grouped['service'][] = $alias; } elseif (str_contains($comment, 'type=reserved')) { $grouped['reserved'][] = $alias; } else { $grouped['user'][] = $alias; } } return response()->json([ 'total' => count($aliases), 'directory' => $grouped, ]); } /** * POST /v1/names/register {"name": "mysite", "address": "iTHN..."} */ public function register(Request $request): JsonResponse { $name = strtolower(trim((string) $request->input('name'))); $address = trim((string) $request->input('address')); $comment = trim((string) $request->input('comment', 'v=lthn1;type=user')); if (! $this->isValidName($name)) { return response()->json([ 'error' => 'Invalid name. Use 1-63 lowercase alphanumeric characters.', ], 422); } if (empty($address) || ! str_starts_with($address, 'iTHN')) { return response()->json([ 'error' => 'Invalid Lethean address.', ], 422); } // Check availability on chain $existing = $this->rpc->getAliasByName($name); if ($existing !== null) { return response()->json([ 'error' => 'Name already registered.', 'name' => $name, ], 409); } // Register via wallet RPC $result = $this->wallet->registerAlias($name, $address, $comment); if (isset($result['code']) || isset($result['error'])) { $message = $result['message'] ?? ($result['error'] ?? 'Unknown error'); $code = 502; if (str_contains($message, 'NOT_ENOUGH_MONEY')) { $message = 'Registrar wallet has insufficient funds. Please try again later.'; $code = 503; } return response()->json([ 'error' => $message, 'name' => $name, ], $code); } if (isset($result['tx_id'])) { return response()->json([ 'name' => $name, 'fqdn' => "{$name}.lthn", 'address' => $address, 'tx_id' => $result['tx_id'], 'status' => 'pending', ], 201); } return response()->json([ 'error' => 'Unexpected response from chain', 'details' => $result, ], 500); } private function isValidName(string $name): bool { return (bool) preg_match('/^[a-z0-9][a-z0-9-]{0,62}$/', $name); } }