Something went wrong. Try again.
Parse and validate AT Protocol Lexicons with DTO generation for Laravel
Something went wrong. Try again.
6.1 kB · 207 lines
PHP
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208<?php
namespace SocialDept\AtpSchema\Parser;
use Illuminate\Support\Facades\Http;use SocialDept\AtpSchema\Contracts\LexiconParser;use SocialDept\AtpSchema\Contracts\LexiconResolver;use SocialDept\AtpSchema\Data\LexiconDocument;use SocialDept\AtpSchema\Exceptions\SchemaNotFoundException;
class DnsLexiconResolver implements LexiconResolver{ /** * Whether DNS resolution is enabled. */ protected bool $enabled;
/** * HTTP timeout in seconds. */ protected int $httpTimeout;
/** * Lexicon parser instance. */ protected LexiconParser $parser;
/** * Whether the atp-resolver package is available. */ protected bool $hasResolver;
/** * Whether we've shown the resolver warning. */ protected static bool $resolverWarningShown = false;
/** * Create a new DnsLexiconResolver. */ public function __construct( bool $enabled = true, int $httpTimeout = 10, ?LexiconParser $parser = null ) { $this->enabled = $enabled; $this->httpTimeout = $httpTimeout; $this->parser = $parser ?? new DefaultLexiconParser(); $this->hasResolver = class_exists('SocialDept\\AtpResolver\\Resolver'); }
/** * Resolve NSID to Lexicon schema via DNS and XRPC. */ public function resolve(string $nsid): LexiconDocument { if (! $this->enabled) { throw SchemaNotFoundException::forNsid($nsid); }
if (! $this->hasResolver) { $this->showResolverWarning();
throw SchemaNotFoundException::forNsid($nsid); }
try { $nsidParsed = Nsid::parse($nsid);
// Step 1: Query DNS TXT record for DID $did = $this->lookupDns($nsidParsed->getAuthority()); if ($did === null) { throw SchemaNotFoundException::forNsid($nsid); }
// Step 2: Resolve DID to PDS endpoint $pdsUrl = $this->resolvePdsEndpoint($did); if ($pdsUrl === null) { throw SchemaNotFoundException::forNsid($nsid); }
// Step 3: Fetch lexicon schema from repository $schema = $this->retrieveSchema($pdsUrl, $did, $nsid);
return $this->parser->parseArray($schema); } catch (SchemaNotFoundException $e) { throw $e; } catch (\Exception $e) { throw SchemaNotFoundException::forNsid($nsid); } }
/** * Perform DNS TXT lookup for _lexicon.{authority}. */ public function lookupDns(string $authority): ?string { // Convert authority to domain (e.g., pub.leaflet -> leaflet.pub) $parts = explode('.', $authority); $domain = implode('.', array_reverse($parts));
// Query DNS TXT record at _lexicon.<domain> $hostname = "_lexicon.{$domain}";
try { $records = dns_get_record($hostname, DNS_TXT);
if ($records === false || empty($records)) { return null; }
// Look for TXT record with did= prefix foreach ($records as $record) { if (isset($record['txt']) && str_starts_with($record['txt'], 'did=')) { return substr($record['txt'], 4); // Remove 'did=' prefix } } } catch (\Exception $e) { // DNS query failed return null; }
return null; }
/** * Retrieve schema via XRPC from PDS. */ public function retrieveSchema(string $pdsEndpoint, string $did, string $nsid): array { try { // Construct XRPC call to com.atproto.repo.getRecord $response = Http::timeout($this->httpTimeout) ->get("{$pdsEndpoint}/xrpc/com.atproto.repo.getRecord", [ 'repo' => $did, 'collection' => 'com.atproto.lexicon.schema', 'rkey' => $nsid, ]);
if ($response->successful()) { $data = $response->json();
// Extract the lexicon schema from the record value if (isset($data['value']) && is_array($data['value']) && isset($data['value']['lexicon'])) { return $data['value']; } } } catch (\Exception $e) { throw SchemaNotFoundException::forNsid($nsid); }
throw SchemaNotFoundException::forNsid($nsid); }
/** * Check if DNS resolution is enabled. */ public function isEnabled(): bool { return $this->enabled; }
/** * Resolve DID to PDS endpoint using atp-resolver. */ protected function resolvePdsEndpoint(string $did): ?string { if (! $this->hasResolver) { return null; }
try { // Get resolver from Laravel container if available if (function_exists('app') && app()->has(\SocialDept\AtpResolver\Resolver::class)) { $resolver = app(\SocialDept\AtpResolver\Resolver::class); } else { // Can't instantiate without dependencies return null; }
// Use the resolvePds method which handles DID resolution and PDS extraction return $resolver->resolvePds($did); } catch (\Exception $e) { return null; } }
/** * Show warning about missing atp-resolver package. */ protected function showResolverWarning(): void { if (self::$resolverWarningShown) { return; }
if (function_exists('logger')) { logger()->warning( 'DNS-based lexicon resolution requires the socialdept/atp-resolver package. '. 'Install it with: composer require socialdept/atp-resolver '. 'Falling back to local lexicon sources only.' ); }
self::$resolverWarningShown = true; }}