···2233namespace SocialDept\Beacon;
4455+use SocialDept\Beacon\Contracts\CacheStore;
66+use SocialDept\Beacon\Contracts\DidResolver;
77+use SocialDept\Beacon\Contracts\HandleResolver;
88+use SocialDept\Beacon\Data\DidDocument;
99+use SocialDept\Beacon\Exceptions\DidResolutionException;
1010+use SocialDept\Beacon\Exceptions\HandleResolutionException;
1111+use SocialDept\Beacon\Support\Concerns\HasConfig;
1212+use SocialDept\Beacon\Support\Identity;
1313+514class Beacon
615{
77- // Build wonderful things
88-}1616+ use HasConfig;
1717+1818+ /**
1919+ * Create a new Beacon instance.
2020+ */
2121+ public function __construct(
2222+ protected DidResolver $didResolver,
2323+ protected HandleResolver $handleResolver,
2424+ protected CacheStore $cache
2525+ ) {
2626+ }
2727+2828+ /**
2929+ * Resolve a DID to a DID Document.
3030+ *
3131+ * @param string $did
3232+ * @param bool $useCache
3333+ * @return DidDocument
3434+ * @throws DidResolutionException
3535+ */
3636+ public function resolveDid(string $did, bool $useCache = true): DidDocument
3737+ {
3838+ $cacheKey = "did:{$did}";
3939+4040+ if ($useCache && $this->cache->has($cacheKey)) {
4141+ $cached = $this->cache->get($cacheKey);
4242+4343+ if ($cached instanceof DidDocument) {
4444+ return $cached;
4545+ }
4646+ }
4747+4848+ $document = $this->didResolver->resolve($did);
4949+5050+ if ($useCache) {
5151+ $ttl = $this->getConfig('beacon.cache.did_ttl', 3600);
5252+ $this->cache->put($cacheKey, $document, $ttl);
5353+ }
5454+5555+ return $document;
5656+ }
5757+5858+ /**
5959+ * Convert a handle to its DID.
6060+ *
6161+ * @param string $handle
6262+ * @param bool $useCache
6363+ * @return string
6464+ * @throws HandleResolutionException
6565+ */
6666+ public function handleToDid(string $handle, bool $useCache = true): string
6767+ {
6868+ $cacheKey = "handle:{$handle}";
6969+7070+ if ($useCache && $this->cache->has($cacheKey)) {
7171+ return $this->cache->get($cacheKey);
7272+ }
7373+7474+ $did = $this->handleResolver->resolve($handle);
7575+7676+ if ($useCache) {
7777+ $ttl = $this->getConfig('beacon.cache.handle_ttl', 3600);
7878+ $this->cache->put($cacheKey, $did, $ttl);
7979+ }
8080+8181+ return $did;
8282+ }
8383+8484+ /**
8585+ * Resolve a handle to its DID Document.
8686+ *
8787+ * @param string $handle
8888+ * @param bool $useCache
8989+ * @return DidDocument
9090+ * @throws DidResolutionException
9191+ * @throws HandleResolutionException
9292+ */
9393+ public function resolveHandle(string $handle, bool $useCache = true): DidDocument
9494+ {
9595+ $did = $this->handleToDid($handle, $useCache);
9696+9797+ return $this->resolveDid($did, $useCache);
9898+ }
9999+100100+ /**
101101+ * Resolve an identity (DID or handle) to its DID Document.
102102+ *
103103+ * @param string $actor A DID or handle
104104+ * @param bool $useCache
105105+ * @return DidDocument
106106+ * @throws DidResolutionException
107107+ * @throws HandleResolutionException
108108+ */
109109+ public function resolveIdentity(string $actor, bool $useCache = true): DidDocument
110110+ {
111111+ return Identity::isDid($actor)
112112+ ? $this->resolveDid($actor, $useCache)
113113+ : $this->resolveHandle($actor, $useCache);
114114+ }
115115+116116+ /**
117117+ * Clear cached data for a DID.
118118+ *
119119+ * @param string $did
120120+ */
121121+ public function clearDidCache(string $did): void
122122+ {
123123+ $this->cache->forget("did:{$did}");
124124+ }
125125+126126+ /**
127127+ * Clear cached data for a handle.
128128+ *
129129+ * @param string $handle
130130+ */
131131+ public function clearHandleCache(string $handle): void
132132+ {
133133+ $this->cache->forget("handle:{$handle}");
134134+ }
135135+136136+ /**
137137+ * Clear all cached data.
138138+ */
139139+ public function clearCache(): void
140140+ {
141141+ $this->cache->flush();
142142+ }
143143+144144+ /**
145145+ * Resolve a DID or handle to its PDS endpoint.
146146+ *
147147+ * @param string $actor A DID (e.g., "did:plc:abc123") or handle (e.g., "user.bsky.social")
148148+ * @param bool $useCache
149149+ * @return string|null The PDS endpoint URL or null if not found
150150+ * @throws DidResolutionException
151151+ * @throws HandleResolutionException
152152+ */
153153+ public function resolvePds(string $actor, bool $useCache = true): ?string
154154+ {
155155+ $cacheKey = "pds:{$actor}";
156156+157157+ if ($useCache && $this->cache->has($cacheKey)) {
158158+ return $this->cache->get($cacheKey);
159159+ }
160160+161161+ // Determine if input is a DID or handle
162162+ $document = $this->resolveIdentity($actor, $useCache);
163163+164164+ $pdsEndpoint = $document->getPdsEndpoint();
165165+166166+ if ($useCache && $pdsEndpoint !== null) {
167167+ $ttl = $this->getConfig('beacon.cache.pds_ttl', 3600);
168168+ $this->cache->put($cacheKey, $pdsEndpoint, $ttl);
169169+ }
170170+171171+ return $pdsEndpoint;
172172+ }
173173+174174+ /**
175175+ * Clear cached PDS endpoint for a DID or handle.
176176+ *
177177+ * @param string $actor
178178+ */
179179+ public function clearPdsCache(string $actor): void
180180+ {
181181+ $this->cache->forget("pds:{$actor}");
182182+ }
183183+}
+44-47
src/BeaconServiceProvider.php
···33namespace SocialDept\Beacon;
4455use Illuminate\Support\ServiceProvider;
66+use SocialDept\Beacon\Cache\LaravelCacheStore;
77+use SocialDept\Beacon\Contracts\CacheStore;
88+use SocialDept\Beacon\Contracts\DidResolver;
99+use SocialDept\Beacon\Contracts\HandleResolver;
1010+use SocialDept\Beacon\Resolvers\AtProtoHandleResolver;
1111+use SocialDept\Beacon\Resolvers\DidResolverManager;
612713class BeaconServiceProvider extends ServiceProvider
814{
915 /**
1010- * Perform post-registration booting of services.
1111- *
1212- * @return void
1616+ * Register any package services.
1317 */
1414- public function boot(): void
1818+ public function register(): void
1519 {
1616- // $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'social-dept');
1717- // $this->loadViewsFrom(__DIR__.'/../resources/views', 'social-dept');
1818- // $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
1919- // $this->loadRoutesFrom(__DIR__.'/routes.php');
2020+ $this->mergeConfigFrom(__DIR__.'/../config/beacon.php', 'beacon');
20212121- // Publishing is only necessary when using the CLI.
2222- if ($this->app->runningInConsole()) {
2323- $this->bootForConsole();
2424- }
2222+ // Register cache store
2323+ $this->app->singleton(CacheStore::class, function ($app) {
2424+ return new LaravelCacheStore($app->make('cache')->store());
2525+ });
2626+2727+ // Register DID resolver
2828+ $this->app->singleton(DidResolver::class, function ($app) {
2929+ return new DidResolverManager();
3030+ });
3131+3232+ // Register handle resolver
3333+ $this->app->singleton(HandleResolver::class, function ($app) {
3434+ return new AtProtoHandleResolver();
3535+ });
3636+3737+ // Register Beacon service
3838+ $this->app->singleton('beacon', function ($app) {
3939+ return new Beacon(
4040+ $app->make(DidResolver::class),
4141+ $app->make(HandleResolver::class),
4242+ $app->make(CacheStore::class),
4343+ );
4444+ });
4545+4646+ $this->app->alias('beacon', Beacon::class);
2547 }
26482749 /**
2828- * Register any package services.
2929- *
3030- * @return void
5050+ * Bootstrap the application services.
3151 */
3232- public function register(): void
5252+ public function boot(): void
3353 {
3434- $this->mergeConfigFrom(__DIR__.'/../config/beacon.php', 'beacon');
3535-3636- // Register the service the package provides.
3737- $this->app->singleton('beacon', function ($app) {
3838- return new Beacon;
3939- });
5454+ if ($this->app->runningInConsole()) {
5555+ $this->bootForConsole();
5656+ }
4057 }
41584259 /**
4360 * Get the services provided by the provider.
4461 *
4545- * @return array
6262+ * @return array<string>
4663 */
4747- public function provides()
6464+ public function provides(): array
4865 {
4949- return ['beacon'];
6666+ return ['beacon', Beacon::class];
5067 }
51685269 /**
5370 * Console-specific booting.
5454- *
5555- * @return void
5671 */
5772 protected function bootForConsole(): void
5873 {
5959- // Publishing the configuration file.
7474+ // Publish config
6075 $this->publishes([
6176 __DIR__.'/../config/beacon.php' => config_path('beacon.php'),
6262- ], 'beacon.config');
6363-6464- // Publishing the views.
6565- /*$this->publishes([
6666- __DIR__.'/../resources/views' => base_path('resources/views/vendor/social-dept'),
6767- ], 'beacon.views');*/
6868-6969- // Publishing assets.
7070- /*$this->publishes([
7171- __DIR__.'/../resources/assets' => public_path('vendor/social-dept'),
7272- ], 'beacon.assets');*/
7373-7474- // Publishing the translation files.
7575- /*$this->publishes([
7676- __DIR__.'/../resources/lang' => resource_path('lang/vendor/social-dept'),
7777- ], 'beacon.lang');*/
7878-7979- // Registering package commands.
8080- // $this->commands([]);
7777+ ], 'beacon-config');
8178 }
8279}
+69
src/Cache/LaravelCacheStore.php
···11+<?php
22+33+namespace SocialDept\Beacon\Cache;
44+55+use Illuminate\Contracts\Cache\Repository;
66+use SocialDept\Beacon\Contracts\CacheStore;
77+88+class LaravelCacheStore implements CacheStore
99+{
1010+ protected string $prefix = 'beacon:';
1111+1212+ /**
1313+ * Create a new Laravel cache store instance.
1414+ */
1515+ public function __construct(
1616+ protected Repository $cache
1717+ ) {
1818+ }
1919+2020+ /**
2121+ * Get a cached value.
2222+ *
2323+ * @param string $key
2424+ */
2525+ public function get(string $key): mixed
2626+ {
2727+ return $this->cache->get($this->prefix.$key);
2828+ }
2929+3030+ /**
3131+ * Store a value in the cache.
3232+ *
3333+ * @param string $key
3434+ * @param mixed $value
3535+ * @param int $ttl Time to live in seconds
3636+ */
3737+ public function put(string $key, mixed $value, int $ttl): void
3838+ {
3939+ $this->cache->put($this->prefix.$key, $value, $ttl);
4040+ }
4141+4242+ /**
4343+ * Check if a key exists in the cache.
4444+ *
4545+ * @param string $key
4646+ */
4747+ public function has(string $key): bool
4848+ {
4949+ return $this->cache->has($this->prefix.$key);
5050+ }
5151+5252+ /**
5353+ * Remove a value from the cache.
5454+ *
5555+ * @param string $key
5656+ */
5757+ public function forget(string $key): void
5858+ {
5959+ $this->cache->forget($this->prefix.$key);
6060+ }
6161+6262+ /**
6363+ * Clear all cached values.
6464+ */
6565+ public function flush(): void
6666+ {
6767+ $this->cache->flush();
6868+ }
6969+}
+42
src/Contracts/CacheStore.php
···11+<?php
22+33+namespace SocialDept\Beacon\Contracts;
44+55+interface CacheStore
66+{
77+ /**
88+ * Get a cached value.
99+ *
1010+ * @param string $key
1111+ * @return mixed
1212+ */
1313+ public function get(string $key): mixed;
1414+1515+ /**
1616+ * Store a value in the cache.
1717+ *
1818+ * @param string $key
1919+ * @param mixed $value
2020+ * @param int $ttl Time to live in seconds
2121+ */
2222+ public function put(string $key, mixed $value, int $ttl): void;
2323+2424+ /**
2525+ * Check if a key exists in the cache.
2626+ *
2727+ * @param string $key
2828+ */
2929+ public function has(string $key): bool;
3030+3131+ /**
3232+ * Remove a value from the cache.
3333+ *
3434+ * @param string $key
3535+ */
3636+ public function forget(string $key): void;
3737+3838+ /**
3939+ * Clear all cached values.
4040+ */
4141+ public function flush(): void;
4242+}
+25
src/Contracts/DidResolver.php
···11+<?php
22+33+namespace SocialDept\Beacon\Contracts;
44+55+use SocialDept\Beacon\Data\DidDocument;
66+77+interface DidResolver
88+{
99+ /**
1010+ * Resolve a DID to a DID Document.
1111+ *
1212+ * @param string $did The DID to resolve (e.g., "did:plc:abc123" or "did:web:example.com")
1313+ * @return DidDocument
1414+ *
1515+ * @throws \SocialDept\Beacon\Exceptions\DidResolutionException
1616+ */
1717+ public function resolve(string $did): DidDocument;
1818+1919+ /**
2020+ * Check if this resolver supports the given DID method.
2121+ *
2222+ * @param string $method The DID method (e.g., "plc", "web")
2323+ */
2424+ public function supports(string $method): bool;
2525+}
+16
src/Contracts/HandleResolver.php
···11+<?php
22+33+namespace SocialDept\Beacon\Contracts;
44+55+interface HandleResolver
66+{
77+ /**
88+ * Resolve a handle to a DID.
99+ *
1010+ * @param string $handle The handle to resolve (e.g., "user.bsky.social")
1111+ * @return string The resolved DID
1212+ *
1313+ * @throws \SocialDept\Beacon\Exceptions\HandleResolutionException
1414+ */
1515+ public function resolve(string $handle): string;
1616+}
+76
src/Data/DidDocument.php
···11+<?php
22+33+namespace SocialDept\Beacon\Data;
44+55+class DidDocument
66+{
77+ /**
88+ * Create a new DID Document instance.
99+ *
1010+ * @param string $id The DID (e.g., "did:plc:abc123")
1111+ * @param array $alsoKnownAs Alternative identifiers (handles)
1212+ * @param array $verificationMethod Verification methods (keys)
1313+ * @param array $service Service endpoints (PDS, etc.)
1414+ * @param array $raw The raw DID document
1515+ */
1616+ public function __construct(
1717+ public readonly string $id,
1818+ public readonly array $alsoKnownAs = [],
1919+ public readonly array $verificationMethod = [],
2020+ public readonly array $service = [],
2121+ public readonly array $raw = [],
2222+ ) {
2323+ }
2424+2525+ /**
2626+ * Create a DID Document from a raw array.
2727+ *
2828+ * @param array $data
2929+ */
3030+ public static function fromArray(array $data): self
3131+ {
3232+ return new self(
3333+ id: $data['id'] ?? '',
3434+ alsoKnownAs: $data['alsoKnownAs'] ?? [],
3535+ verificationMethod: $data['verificationMethod'] ?? [],
3636+ service: $data['service'] ?? [],
3737+ raw: $data,
3838+ );
3939+ }
4040+4141+ /**
4242+ * Get the PDS (Personal Data Server) endpoint.
4343+ */
4444+ public function getPdsEndpoint(): ?string
4545+ {
4646+ foreach ($this->service as $service) {
4747+ if (($service['type'] ?? '') === 'AtprotoPersonalDataServer') {
4848+ return $service['serviceEndpoint'] ?? null;
4949+ }
5050+ }
5151+5252+ return null;
5353+ }
5454+5555+ /**
5656+ * Get the handle from alsoKnownAs.
5757+ */
5858+ public function getHandle(): ?string
5959+ {
6060+ foreach ($this->alsoKnownAs as $alias) {
6161+ if (str_starts_with($alias, 'at://')) {
6262+ return substr($alias, 5);
6363+ }
6464+ }
6565+6666+ return null;
6767+ }
6868+6969+ /**
7070+ * Convert to array.
7171+ */
7272+ public function toArray(): array
7373+ {
7474+ return $this->raw;
7575+ }
7676+}