refactor: adopt CorePHP lifecycle event patterns in all Mod modules

All 6 Mod modules now use $event->routes() and $event->views() instead
of raw Route:: and app('view')-> calls. Service singletons moved to
FrameworkBooted where appropriate. Website/Api module added for
api.lthn.io domain with proper DomainResolving.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-04 09:13:34 +01:00
parent f1b741da49
commit 646fb0602f
No known key found for this signature in database
GPG key ID: AF404715446AEB41
11 changed files with 217 additions and 40 deletions

View file

@ -14,13 +14,12 @@ class Boot
WebRoutesRegistering::class => 'onBoot',
];
public function onBoot(): void
public function onBoot(WebRoutesRegistering $event): void
{
app('config')->set('chain', require __DIR__ . '/config.php');
app()->singleton(DaemonRpc::class);
app()->singleton(WalletRpc::class);
// Register API auth middleware
app('router')->aliasMiddleware('auth.api', \App\Http\Middleware\ApiTokenAuth::class);
}
}

View file

@ -13,10 +13,12 @@ class Boot
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(): void
public function onWebRoutes(WebRoutesRegistering $event): void
{
app('view')->addNamespace('explorer', __DIR__ . '/Views');
Route::prefix('explorer')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/explorer')->group(__DIR__ . '/Routes/api.php');
$event->views('explorer', __DIR__ . '/Views');
$event->routes(function () {
Route::prefix('explorer')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/explorer')->group(__DIR__ . '/Routes/api.php');
});
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Mod\Gateway;
use Core\Events\FrameworkBooted;
use Core\Events\WebRoutesRegistering;
use Illuminate\Support\Facades\Route;
use Mod\Gateway\Services\GatewayRegistry;
@ -12,11 +13,16 @@ class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
FrameworkBooted::class => 'onFrameworkBooted',
];
public function onWebRoutes(): void
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(fn () => Route::prefix('v1/gateway')->group(__DIR__ . '/Routes/api.php'));
}
public function onFrameworkBooted(FrameworkBooted $event): void
{
app()->singleton(GatewayRegistry::class);
Route::prefix('v1/gateway')->group(__DIR__ . '/Routes/api.php');
}
}

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Mod\Names;
use Core\Events\ApiRoutesRegistering;
use Core\Events\ConsoleBooting;
use Core\Events\WebRoutesRegistering;
use Illuminate\Support\Facades\Route;
@ -17,20 +16,17 @@ class Boot
ConsoleBooting::class => 'onConsole',
];
public function onWebRoutes(): void
public function onWebRoutes(WebRoutesRegistering $event): void
{
app('view')->addNamespace('names', __DIR__ . '/Views');
Route::prefix('v1/names')->group(__DIR__ . '/Routes/api.php');
Route::prefix('names')->group(__DIR__ . '/Routes/web.php');
$event->views('names', __DIR__ . '/Views');
$event->routes(function () {
Route::prefix('names')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/names')->group(__DIR__ . '/Routes/api.php');
});
}
public function onConsole(ConsoleBooting $event): void
{
$event->command(RetryDnsTickets::class);
}
public function onApiRoutes(): void
{
Route::prefix('v1/names')->group(__DIR__ . '/Routes/api.php');
}
}

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Mod\Pool;
use Core\Events\ApiRoutesRegistering;
use Core\Events\FrameworkBooted;
use Core\Events\WebRoutesRegistering;
use Illuminate\Support\Facades\Route;
use Mod\Pool\Services\PoolClient;
@ -13,19 +13,20 @@ class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
FrameworkBooted::class => 'onFrameworkBooted',
];
public function onWebRoutes(): void
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(function () {
Route::prefix('pool')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/pool')->group(__DIR__ . '/Routes/api.php');
});
}
public function onFrameworkBooted(FrameworkBooted $event): void
{
app()->singleton(PoolClient::class);
app('config')->set('pool', require __DIR__ . '/config.php');
Route::prefix('v1/pool')->group(__DIR__ . '/Routes/api.php');
Route::prefix('pool')->group(__DIR__ . '/Routes/web.php');
}
public function onApiRoutes(): void
{
Route::prefix('v1/pool')->group(__DIR__ . '/Routes/api.php');
}
}

View file

@ -13,8 +13,8 @@ class Boot
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(): void
public function onWebRoutes(WebRoutesRegistering $event): void
{
Route::prefix('v1/proxy')->group(__DIR__ . '/Routes/api.php');
$event->routes(fn () => Route::prefix('v1/proxy')->group(__DIR__ . '/Routes/api.php'));
}
}

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Mod\Trade;
use Core\Events\ApiRoutesRegistering;
use Core\Events\WebRoutesRegistering;
use Illuminate\Support\Facades\Route;
@ -12,17 +11,13 @@ class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(): void
public function onWebRoutes(WebRoutesRegistering $event): void
{
Route::prefix('v1/trade')->group(__DIR__ . '/Routes/api.php');
Route::prefix('trade')->group(__DIR__ . '/Routes/web.php');
}
public function onApiRoutes(): void
{
Route::prefix('v1/trade')->group(__DIR__ . '/Routes/api.php');
$event->routes(function () {
Route::prefix('trade')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/trade')->group(__DIR__ . '/Routes/api.php');
});
}
}

65
app/Website/Api/Boot.php Normal file
View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Website\Api;
use Core\Events\DomainResolving;
use Core\Events\WebRoutesRegistering;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
/**
* api.lthn.io REST API homepage.
*
* When api.lthn.io is the resolved domain, serves an API reference
* landing page at /. API endpoints themselves are registered by Mod
* modules via ApiRoutesRegistering.
*/
class Boot extends ServiceProvider
{
public static array $domains = [
'/^api\.lthn\.io$/',
'/^api\.lthn\.sh$/',
'/^api\.(test|localhost)$/',
];
public static array $listens = [
DomainResolving::class => 'onDomain',
WebRoutesRegistering::class => 'onWebRoutes',
];
public function register(): void {}
public function boot(): void
{
$this->loadViewsFrom(__DIR__ . '/Views', 'api');
foreach (static::$listens as $event => $method) {
Event::listen($event, [$this, $method]);
}
}
public function onDomain(DomainResolving $event): void
{
foreach (static::$domains as $pattern) {
if (preg_match($pattern, $event->domain)) {
$event->resolve($this);
return;
}
}
}
public function onWebRoutes(WebRoutesRegistering $event): void
{
$host = $_SERVER['HTTP_HOST'] ?? '';
foreach (static::$domains as $pattern) {
if (preg_match($pattern, $host)) {
$event->routes(fn () => Route::group([], __DIR__ . '/Routes/web.php'));
return;
}
}
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Website\Api\Controllers;
use Illuminate\Routing\Controller;
/**
* api.lthn.io landing page.
*
* GET / API reference and endpoint index
*/
class ApiDocsController extends Controller
{
public function index(): \Illuminate\View\View
{
return view('api::index');
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Website\Api\Controllers\ApiDocsController;
// Homepage — API reference (only serves on api.lthn.io domain)
Route::get('/', [ApiDocsController::class, 'index'])->name('api.index');

View file

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>.lthn API</title>
<meta name="description" content="REST API for the Lethean .lthn TLD registrar — names, DNS, proxy, gateway, and blockchain explorer.">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:#0a0e17;color:#e5e7eb;padding:2rem}
.container{max-width:900px;margin:0 auto}
h1{font-size:1.5rem;font-weight:600;margin-bottom:0.5rem}
h2{font-size:1.1rem;font-weight:600;margin:2rem 0 0.75rem;color:#e5e7eb}
p{color:#9ca3af;line-height:1.6;margin-bottom:1rem}
a{color:#818cf8;text-decoration:none}
a:hover{text-decoration:underline}
code{background:#111827;padding:0.15rem 0.4rem;border-radius:0.25rem;font-size:0.85rem}
table{width:100%;border-collapse:collapse;margin-bottom:1.5rem}
th{text-align:left;padding:0.5rem;color:#6b7280;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #1f2937}
td{padding:0.5rem;border-bottom:1px solid #1f2937;font-size:0.85rem}
.get{background:#065f46;color:#34d399;padding:0.15rem 0.5rem;border-radius:0.2rem;font-size:0.7rem;font-weight:600}
.post{background:#78350f;color:#fbbf24;padding:0.15rem 0.5rem;border-radius:0.2rem;font-size:0.7rem;font-weight:600}
.muted{color:#6b7280}
.card{background:#111827;border:1px solid #1f2937;border-radius:0.5rem;padding:1rem;margin-bottom:1rem}
.header{display:flex;align-items:baseline;gap:1rem;margin-bottom:1.5rem}
.header .badge{background:rgba(52,211,153,0.15);color:#34d399;padding:0.25rem 0.75rem;border-radius:1rem;font-size:0.75rem;font-weight:500}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>.lthn API</h1>
<span class="badge">v1</span>
</div>
<p>REST API for the Lethean .lthn TLD registrar. All endpoints return JSON when called with <code>Accept: application/json</code>.</p>
<p>Base URL: <code>https://api.lthn.io/v1</code> &middot; Website: <a href="https://lthn.io">lthn.io</a> &middot; Full docs: <a href="https://lthn.io/docs">lthn.io/docs</a></p>
<h2>Names</h2>
<table>
<tr><th>Method</th><th>Endpoint</th><th>Description</th></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/names/available/darbs">/v1/names/available/{name}</a></td><td class="muted">Check availability</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/names/lookup/charon">/v1/names/lookup/{name}</a></td><td class="muted">Look up registered name</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/names/directory">/v1/names/directory</a></td><td class="muted">All names by type</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/names/search?q=ch">/v1/names/search?q={query}</a></td><td class="muted">Search names</td></tr>
<tr><td><span class="post">POST</span></td><td>/v1/names/register</td><td class="muted">Register a name</td></tr>
<tr><td><span class="post">POST</span></td><td>/v1/names/claim</td><td class="muted">Pre-register a claim</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/names/records/charon">/v1/names/records/{name}</a></td><td class="muted">DNS records</td></tr>
<tr><td><span class="post">POST</span></td><td>/v1/names/records/{name}</td><td class="muted">Update DNS records</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/names/health">/v1/names/health</a></td><td class="muted">Registrar health</td></tr>
</table>
<h2>Explorer</h2>
<table>
<tr><th>Method</th><th>Endpoint</th><th>Description</th></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/explorer/info">/v1/explorer/info</a></td><td class="muted">Chain info</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/explorer/stats">/v1/explorer/stats</a></td><td class="muted">Chain statistics</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/explorer/block/100">/v1/explorer/block/{id}</a></td><td class="muted">Block by height or hash</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/explorer/aliases">/v1/explorer/aliases</a></td><td class="muted">All chain aliases</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/explorer/alias/charon">/v1/explorer/alias/{name}</a></td><td class="muted">Alias details</td></tr>
</table>
<h2>Proxy</h2>
<table>
<tr><th>Method</th><th>Endpoint</th><th>Description</th></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/proxy/status">/v1/proxy/status</a></td><td class="muted">Network availability + pricing</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/proxy/nodes">/v1/proxy/nodes</a></td><td class="muted">List nodes by capability</td></tr>
<tr><td><span class="post">POST</span></td><td>/v1/proxy/connect</td><td class="muted">Get gateway connection</td></tr>
</table>
<h2>Gateway</h2>
<table>
<tr><th>Method</th><th>Endpoint</th><th>Description</th></tr>
<tr><td><span class="post">POST</span></td><td>/v1/gateway/pair</td><td class="muted">Pair a gateway node</td></tr>
<tr><td><span class="post">POST</span></td><td>/v1/gateway/heartbeat</td><td class="muted">Report alive + stats</td></tr>
<tr><td><span class="get">GET</span></td><td><a href="/v1/gateway/live">/v1/gateway/live</a></td><td class="muted">Live paired gateways</td></tr>
</table>
<div class="card">
<strong>Authentication</strong>
<p style="margin-top:0.5rem">Read endpoints (GET) are public. Write endpoints (POST) require <code>Authorization: Bearer {token}</code>. Get keys at <a href="https://order.lthn.ai">order.lthn.ai</a>.</p>
</div>
</div>
</body>
</html>