+35
.php-cs-fixer.php
+35
.php-cs-fixer.php
···
1
+
<?php
2
+
3
+
use PhpCsFixer\Config;
4
+
use PhpCsFixer\Finder;
5
+
6
+
$finder = Finder::create()
7
+
->in(__DIR__ . '/src')
8
+
->in(__DIR__ . '/tests')
9
+
->name('*.php')
10
+
->notName('*.blade.php')
11
+
->ignoreDotFiles(true)
12
+
->ignoreVCS(true);
13
+
14
+
return (new Config())
15
+
->setRules([
16
+
'@PSR12' => true,
17
+
'array_syntax' => ['syntax' => 'short'],
18
+
'ordered_imports' => ['sort_algorithm' => 'alpha'],
19
+
'no_unused_imports' => true,
20
+
'not_operator_with_successor_space' => true,
21
+
'trailing_comma_in_multiline' => true,
22
+
'phpdoc_scalar' => true,
23
+
'unary_operator_spaces' => true,
24
+
'binary_operator_spaces' => true,
25
+
'blank_line_before_statement' => [
26
+
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
27
+
],
28
+
'phpdoc_single_line_var_spacing' => true,
29
+
'phpdoc_var_without_name' => true,
30
+
'method_argument_space' => [
31
+
'on_multiline' => 'ensure_fully_multiline',
32
+
'keep_multiple_spaces_after_comma' => true,
33
+
],
34
+
])
35
+
->setFinder($finder);
+327
-31
README.md
+327
-31
README.md
···
1
-
# Beacon
1
+
[](https://github.com/socialdept/atp-signals)
2
+
3
+
<h3 align="center">
4
+
Resolve AT Protocol identities in your Laravel application.
5
+
</h3>
6
+
7
+
<p align="center">
8
+
<br>
9
+
<a href="https://packagist.org/packages/socialdept/atp-beacon" title="Latest Version on Packagist"><img src="https://img.shields.io/packagist/v/socialdept/atp-beacon.svg?style=flat-square"></a>
10
+
<a href="https://packagist.org/packages/socialdept/atp-beacon" title="Total Downloads"><img src="https://img.shields.io/packagist/dt/socialdept/atp-beacon.svg?style=flat-square"></a>
11
+
<a href="https://github.com/socialdept/atp-beacon/actions/workflows/tests.yml" title="GitHub Tests Action Status"><img src="https://img.shields.io/github/actions/workflow/status/socialdept/atp-beacon/tests.yml?branch=main&label=tests&style=flat-square"></a>
12
+
<a href="LICENSE" title="Software License"><img src="https://img.shields.io/github/license/socialdept/atp-beacon?style=flat-square"></a>
13
+
</p>
14
+
15
+
---
16
+
17
+
## What is Beacon?
18
+
19
+
**Beacon** is a Laravel package that resolves AT Protocol identities. Convert DIDs to handles, find PDS endpoints, and resolve DID documents with automatic caching and fallback support for both `did:plc` and `did:web` methods.
20
+
21
+
Think of it as a Swiss Army knife for AT Protocol identity resolution.
22
+
23
+
## Why use Beacon?
2
24
3
-
[![Latest Version on Packagist][ico-version]][link-packagist]
4
-
[![Total Downloads][ico-downloads]][link-downloads]
5
-
[![Build Status][ico-travis]][link-travis]
6
-
[![StyleCI][ico-styleci]][link-styleci]
25
+
- **Simple API** - Resolve DIDs and handles with one method call
26
+
- **Automatic caching** - Smart caching with configurable TTLs
27
+
- **Multiple DID methods** - Support for `did:plc` and `did:web`
28
+
- **PDS discovery** - Find the correct PDS endpoint for any user
29
+
- **Production ready** - Battle-tested with proper error handling
30
+
- **Zero config** - Works out of the box with sensible defaults
7
31
8
-
This is where your description should go. Take a look at [contributing.md](CONTRIBUTING.md) to see a to do list.
32
+
## Quick Example
33
+
34
+
```php
35
+
use SocialDept\Beacon\Facades\Beacon;
36
+
37
+
// Resolve a DID to its document
38
+
$document = Beacon::resolveDid('did:plc:ewvi7nxzyoun6zhxrhs64oiz');
39
+
$handle = $document->getHandle(); // "user.bsky.social"
40
+
$pds = $document->getPdsEndpoint(); // "https://bsky.social"
41
+
42
+
// Resolve a handle to its DID
43
+
$did = Beacon::resolveHandle('user.bsky.social');
44
+
// "did:plc:ewvi7nxzyoun6zhxrhs64oiz"
45
+
46
+
// Find someone's PDS endpoint
47
+
$pds = Beacon::resolvePds('alice.bsky.social');
48
+
// "https://bsky.social"
49
+
```
9
50
10
51
## Installation
11
52
12
-
Via Composer
53
+
```bash
54
+
composer require socialdept/atp-beacon
55
+
```
56
+
57
+
Beacon will auto-register with Laravel. Optionally publish the config:
13
58
14
59
```bash
15
-
composer require social-dept/beacon
60
+
php artisan vendor:publish --tag=beacon-config
61
+
```
62
+
63
+
## Basic Usage
64
+
65
+
### Resolving DIDs
66
+
67
+
Beacon supports both `did:plc` and `did:web` methods:
68
+
69
+
```php
70
+
use SocialDept\Beacon\Facades\Beacon;
71
+
72
+
// PLC directory resolution
73
+
$document = Beacon::resolveDid('did:plc:ewvi7nxzyoun6zhxrhs64oiz');
74
+
75
+
// Web DID resolution
76
+
$document = Beacon::resolveDid('did:web:example.com');
77
+
78
+
// Access document data
79
+
$handle = $document->getHandle();
80
+
$pdsEndpoint = $document->getPdsEndpoint();
81
+
$services = $document->service;
82
+
```
83
+
84
+
### Resolving Handles
85
+
86
+
Convert human-readable handles to DIDs:
87
+
88
+
```php
89
+
$did = Beacon::resolveHandle('alice.bsky.social');
90
+
// "did:plc:ewvi7nxzyoun6zhxrhs64oiz"
91
+
92
+
// Get the full DID document
93
+
$document = Beacon::resolveHandleToDid('alice.bsky.social');
94
+
```
95
+
96
+
### Finding PDS Endpoints
97
+
98
+
Automatically discover a user's Personal Data Server:
99
+
100
+
```php
101
+
// From a DID
102
+
$pds = Beacon::resolvePds('did:plc:ewvi7nxzyoun6zhxrhs64oiz');
103
+
104
+
// From a handle
105
+
$pds = Beacon::resolvePds('alice.bsky.social');
106
+
107
+
// Returns: "https://bsky.social" or user's custom PDS
108
+
```
109
+
110
+
This is particularly useful when you need to make API calls to a user's PDS instead of hardcoding Bluesky's public instance.
111
+
112
+
### Cache Management
113
+
114
+
Beacon automatically caches resolutions. Clear the cache when needed:
115
+
116
+
```php
117
+
// Clear specific DID cache
118
+
Beacon::clearDidCache('did:plc:abc123');
119
+
120
+
// Clear specific handle cache
121
+
Beacon::clearHandleCache('alice.bsky.social');
122
+
123
+
// Clear specific PDS cache
124
+
Beacon::clearPdsCache('alice.bsky.social');
125
+
126
+
// Clear all cached data
127
+
Beacon::clearCache();
128
+
```
129
+
130
+
### Disable Caching
131
+
132
+
Pass `false` as the second parameter to bypass cache:
133
+
134
+
```php
135
+
$document = Beacon::resolveDid('did:plc:abc123', useCache: false);
136
+
$did = Beacon::resolveHandle('alice.bsky.social', useCache: false);
137
+
$pds = Beacon::resolvePds('alice.bsky.social', useCache: false);
16
138
```
17
139
18
-
## Usage
140
+
### Identity Validation
19
141
20
-
## Change log
142
+
Beacon includes static helper methods to validate DIDs and handles:
21
143
22
-
Please see the [changelog](changelog.md) for more information on what has changed recently.
144
+
```php
145
+
use SocialDept\Beacon\Support\Identity;
23
146
24
-
## Testing
147
+
// Validate handles
148
+
Identity::isHandle('alice.bsky.social'); // true
149
+
Identity::isHandle('invalid'); // false
25
150
26
-
```bash
27
-
composer test
151
+
// Validate DIDs
152
+
Identity::isDid('did:plc:ewvi7nxzyoun6zhxrhs64oiz'); // true
153
+
Identity::isDid('did:web:example.com'); // true
154
+
Identity::isDid('invalid'); // false
155
+
156
+
// Extract DID method
157
+
Identity::extractDidMethod('did:plc:abc123'); // "plc"
158
+
Identity::extractDidMethod('did:web:test'); // "web"
159
+
160
+
// Check specific DID types
161
+
Identity::isPlcDid('did:plc:abc123'); // true
162
+
Identity::isWebDid('did:web:test'); // true
28
163
```
29
164
30
-
## Contributing
165
+
These helpers are useful for validating user input before making resolution calls.
31
166
32
-
Please see [contributing.md](CONTRIBUTING.md) for details and a todolist.
167
+
## Configuration
33
168
34
-
## Security
169
+
Beacon works great with zero configuration, but you can customize behavior in `config/beacon.php`:
170
+
171
+
```php
172
+
return [
173
+
// PLC directory for did:plc resolution
174
+
'plc_directory' => env('BEACON_PLC_DIRECTORY', 'https://plc.directory'),
35
175
36
-
If you discover any security related issues, please email author@email.com instead of using the issue tracker.
176
+
// Default PDS endpoint for handle resolution
177
+
'pds_endpoint' => env('BEACON_PDS_ENDPOINT', 'https://bsky.social'),
178
+
179
+
// HTTP request timeout
180
+
'timeout' => env('BEACON_TIMEOUT', 10),
181
+
182
+
// Cache configuration
183
+
'cache' => [
184
+
'enabled' => env('BEACON_CACHE_ENABLED', true),
185
+
186
+
// Cache TTL for DID documents (1 hour)
187
+
'did_ttl' => env('BEACON_CACHE_DID_TTL', 3600),
188
+
189
+
// Cache TTL for handle resolutions (1 hour)
190
+
'handle_ttl' => env('BEACON_CACHE_HANDLE_TTL', 3600),
191
+
192
+
// Cache TTL for PDS endpoints (1 hour)
193
+
'pds_ttl' => env('BEACON_CACHE_PDS_TTL', 3600),
194
+
],
195
+
];
196
+
```
197
+
198
+
## API Reference
199
+
200
+
### Available Methods
201
+
202
+
```php
203
+
// DID Resolution
204
+
Beacon::resolveDid(string $did, bool $useCache = true): DidDocument
205
+
206
+
// Handle Resolution
207
+
Beacon::handleToDid(string $handle, bool $useCache = true): string
208
+
Beacon::resolveHandle(string $handle, bool $useCache = true): DidDocument
209
+
210
+
// Identity Resolution
211
+
Beacon::resolveIdentity(string $actor, bool $useCache = true): DidDocument
212
+
213
+
// PDS Resolution
214
+
Beacon::resolvePds(string $actor, bool $useCache = true): ?string
215
+
216
+
// Cache Management
217
+
Beacon::clearDidCache(string $did): void
218
+
Beacon::clearHandleCache(string $handle): void
219
+
Beacon::clearPdsCache(string $actor): void
220
+
Beacon::clearCache(): void
221
+
222
+
// Identity Validation (static helpers)
223
+
Identity::isHandle(?string $handle): bool
224
+
Identity::isDid(?string $did): bool
225
+
Identity::extractDidMethod(string $did): ?string
226
+
Identity::isPlcDid(string $did): bool
227
+
Identity::isWebDid(string $did): bool
228
+
```
229
+
230
+
### DidDocument Object
231
+
232
+
```php
233
+
$document->id; // string - The DID
234
+
$document->alsoKnownAs; // array - Alternative identifiers
235
+
$document->verificationMethod; // array - Verification methods
236
+
$document->service; // array - Service endpoints
237
+
$document->raw; // array - Raw DID document
238
+
239
+
// Helper methods
240
+
$document->getHandle(); // ?string - Extract handle from alsoKnownAs
241
+
$document->getPdsEndpoint(); // ?string - Extract PDS service endpoint
242
+
$document->toArray(); // array - Convert to array
243
+
```
244
+
245
+
## Error Handling
246
+
247
+
Beacon throws descriptive exceptions when resolution fails:
248
+
249
+
```php
250
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
251
+
use SocialDept\Beacon\Exceptions\HandleResolutionException;
252
+
253
+
try {
254
+
$document = Beacon::resolveDid('did:invalid:format');
255
+
} catch (DidResolutionException $e) {
256
+
// Handle DID resolution errors
257
+
logger()->error('DID resolution failed', [
258
+
'message' => $e->getMessage(),
259
+
]);
260
+
}
261
+
262
+
try {
263
+
$did = Beacon::resolveHandle('invalid-handle');
264
+
} catch (HandleResolutionException $e) {
265
+
// Handle handle resolution errors
266
+
}
267
+
```
268
+
269
+
## Use Cases
270
+
271
+
### Building an AppView
272
+
273
+
```php
274
+
// Resolve user identity from DID
275
+
$document = Beacon::resolveDid($event->did);
276
+
$handle = $document->getHandle();
277
+
278
+
// Make authenticated requests to their PDS
279
+
$pds = Beacon::resolvePds($event->did);
280
+
$client = new AtProtoClient($pds);
281
+
```
282
+
283
+
### Custom Feed Generators
284
+
285
+
```php
286
+
// Resolve multiple handles efficiently (caching kicks in)
287
+
$dids = collect(['alice.bsky.social', 'bob.bsky.social'])
288
+
->map(fn($handle) => Beacon::resolveHandle($handle))
289
+
->all();
290
+
```
291
+
292
+
### Profile Resolution
293
+
294
+
```php
295
+
// Get complete identity information
296
+
$document = Beacon::resolveHandleToDid($username);
297
+
298
+
$profile = [
299
+
'did' => $document->id,
300
+
'handle' => $document->getHandle(),
301
+
'pds' => $document->getPdsEndpoint(),
302
+
];
303
+
```
304
+
305
+
### Input Validation
306
+
307
+
```php
308
+
use SocialDept\Beacon\Support\Identity;
309
+
use SocialDept\Beacon\Facades\Beacon;
310
+
311
+
// Validate user input before resolving
312
+
$userInput = request()->input('identifier');
313
+
314
+
if (Identity::isHandle($userInput)) {
315
+
$did = Beacon::resolveHandle($userInput);
316
+
} elseif (Identity::isDid($userInput)) {
317
+
$document = Beacon::resolveDid($userInput);
318
+
} else {
319
+
abort(422, 'Invalid handle or DID');
320
+
}
321
+
```
322
+
323
+
## Requirements
324
+
325
+
- PHP 8.2+
326
+
- Laravel 11+
327
+
- `ext-gmp` extension
328
+
329
+
## Resources
330
+
331
+
- [AT Protocol Documentation](https://atproto.com/)
332
+
- [DID:PLC Specification](https://github.com/did-method-plc/did-method-plc)
333
+
- [DID:Web Specification](https://w3c-ccg.github.io/did-method-web/)
334
+
- [PLC Directory](https://plc.directory/)
335
+
336
+
## Support & Contributing
337
+
338
+
Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-beacon/issues).
339
+
340
+
Want to contribute? We'd love your help! Check out the [contribution guidelines](CONTRIBUTING.md).
37
341
38
342
## Credits
39
343
40
-
- [Author Name][link-author]
41
-
- [All Contributors][link-contributors]
344
+
- [Miguel Batres](https://batres.co) - founder & lead maintainer
345
+
- [All contributors](https://github.com/socialdept/atp-beacon/graphs/contributors)
42
346
43
347
## License
44
348
45
-
MIT. Please see the [license file](LICENSE) for more information.
349
+
Beacon is open-source software licensed under the [MIT license](LICENSE).
46
350
47
-
[ico-version]: https://img.shields.io/packagist/v/social-dept/beacon.svg?style=flat-square
48
-
[ico-downloads]: https://img.shields.io/packagist/dt/social-dept/beacon.svg?style=flat-square
49
-
[ico-travis]: https://img.shields.io/travis/social-dept/beacon/master.svg?style=flat-square
50
-
[ico-styleci]: https://styleci.io/repos/12345678/shield
351
+
---
51
352
52
-
[link-packagist]: https://packagist.org/packages/social-dept/beacon
53
-
[link-downloads]: https://packagist.org/packages/social-dept/beacon
54
-
[link-travis]: https://travis-ci.org/social-dept/beacon
55
-
[link-styleci]: https://styleci.io/repos/12345678
56
-
[link-author]: https://github.com/social-dept
57
-
[link-contributors]: ../../contributors
353
+
**Built for the Federation** • By Social Dept.
+12
-14
composer.json
+12
-14
composer.json
···
1
1
{
2
2
"name": "socialdept/atp-beacon",
3
-
"description": ":package_description",
3
+
"description": "Resolve AT Protocol DIDs, handles, and lexicon schemas in Laravel",
4
+
"type": "library",
4
5
"license": "MIT",
5
-
"authors": [
6
-
{
7
-
"name": "Author Name",
8
-
"email": "author@email.com",
9
-
"homepage": "http://author.com"
10
-
}
11
-
],
12
-
"homepage": "https://github.com/social-dept/beacon",
13
-
"keywords": ["Laravel", "Beacon"],
14
6
"require": {
15
-
"illuminate/support": "~9"
7
+
"php": "^8.2",
8
+
"ext-gmp": "*",
9
+
"illuminate/support": "^11.0|^12.0",
10
+
"illuminate/console": "^11.0|^12.0",
11
+
"illuminate/cache": "^11.0|^12.0",
12
+
"guzzlehttp/guzzle": "^7.5"
16
13
},
17
14
"require-dev": {
18
-
"phpunit/phpunit": "~9.0",
19
-
"orchestra/testbench": "~7"
15
+
"orchestra/testbench": "^9.0",
16
+
"phpunit/phpunit": "^11.0",
17
+
"friendsofphp/php-cs-fixer": "^3.89"
20
18
},
21
19
"autoload": {
22
20
"psr-4": {
···
25
23
},
26
24
"autoload-dev": {
27
25
"psr-4": {
28
-
"SocialDept\\Beacon\\Tests\\": "tests"
26
+
"SocialDept\\Beacon\\Tests\\": "tests/"
29
27
}
30
28
},
31
29
"extra": {
+67
-1
config/beacon.php
+67
-1
config/beacon.php
···
1
1
<?php
2
2
3
3
return [
4
-
//
4
+
5
+
/*
6
+
|--------------------------------------------------------------------------
7
+
| PLC Directory URL
8
+
|--------------------------------------------------------------------------
9
+
|
10
+
| The URL of the PLC (Public Ledger of Credentials) directory used for
11
+
| resolving DID:PLC identifiers. The default is the official AT Protocol
12
+
| PLC directory.
13
+
|
14
+
*/
15
+
16
+
'plc_directory' => env('BEACON_PLC_DIRECTORY', 'https://plc.directory'),
17
+
18
+
/*
19
+
|--------------------------------------------------------------------------
20
+
| PDS Endpoint
21
+
|--------------------------------------------------------------------------
22
+
|
23
+
| The Personal Data Server endpoint used for handle resolution. This is
24
+
| used when resolving handles to DIDs via the AT Protocol API.
25
+
|
26
+
*/
27
+
28
+
'pds_endpoint' => env('BEACON_PDS_ENDPOINT', 'https://bsky.social'),
29
+
30
+
/*
31
+
|--------------------------------------------------------------------------
32
+
| Request Timeout
33
+
|--------------------------------------------------------------------------
34
+
|
35
+
| The timeout in seconds for HTTP requests to external services when
36
+
| resolving DIDs, handles, and lexicons.
37
+
|
38
+
*/
39
+
40
+
'timeout' => env('BEACON_TIMEOUT', 10),
41
+
42
+
/*
43
+
|--------------------------------------------------------------------------
44
+
| Cache Configuration
45
+
|--------------------------------------------------------------------------
46
+
|
47
+
| Configure caching behavior for resolved DIDs, handles, and lexicons.
48
+
| TTL values are in seconds.
49
+
|
50
+
*/
51
+
52
+
'cache' => [
53
+
54
+
// Enable or disable caching globally
55
+
'enabled' => env('BEACON_CACHE_ENABLED', true),
56
+
57
+
// Cache TTL for DID documents (1 hour default)
58
+
'did_ttl' => env('BEACON_CACHE_DID_TTL', 3600),
59
+
60
+
// Cache TTL for handle resolutions (1 hour default)
61
+
'handle_ttl' => env('BEACON_CACHE_HANDLE_TTL', 3600),
62
+
63
+
// Cache TTL for PDS endpoints (1 hour default)
64
+
'pds_ttl' => env('BEACON_CACHE_PDS_TTL', 3600),
65
+
66
+
// Cache TTL for lexicon schemas (24 hours default)
67
+
'lexicon_ttl' => env('BEACON_CACHE_LEXICON_TTL', 86400),
68
+
69
+
],
70
+
5
71
];
header.png
header.png
This is a binary file and will not be displayed.
+1
-1
phpunit.xml
+1
-1
phpunit.xml
+177
-2
src/Beacon.php
+177
-2
src/Beacon.php
···
2
2
3
3
namespace SocialDept\Beacon;
4
4
5
+
use SocialDept\Beacon\Contracts\CacheStore;
6
+
use SocialDept\Beacon\Contracts\DidResolver;
7
+
use SocialDept\Beacon\Contracts\HandleResolver;
8
+
use SocialDept\Beacon\Data\DidDocument;
9
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
10
+
use SocialDept\Beacon\Exceptions\HandleResolutionException;
11
+
use SocialDept\Beacon\Support\Concerns\HasConfig;
12
+
use SocialDept\Beacon\Support\Identity;
13
+
5
14
class Beacon
6
15
{
7
-
// Build wonderful things
8
-
}
16
+
use HasConfig;
17
+
18
+
/**
19
+
* Create a new Beacon instance.
20
+
*/
21
+
public function __construct(
22
+
protected DidResolver $didResolver,
23
+
protected HandleResolver $handleResolver,
24
+
protected CacheStore $cache
25
+
) {
26
+
}
27
+
28
+
/**
29
+
* Resolve a DID to a DID Document.
30
+
*
31
+
* @param string $did
32
+
* @param bool $useCache
33
+
* @return DidDocument
34
+
* @throws DidResolutionException
35
+
*/
36
+
public function resolveDid(string $did, bool $useCache = true): DidDocument
37
+
{
38
+
$cacheKey = "did:{$did}";
39
+
40
+
if ($useCache && $this->cache->has($cacheKey)) {
41
+
$cached = $this->cache->get($cacheKey);
42
+
43
+
if ($cached instanceof DidDocument) {
44
+
return $cached;
45
+
}
46
+
}
47
+
48
+
$document = $this->didResolver->resolve($did);
49
+
50
+
if ($useCache) {
51
+
$ttl = $this->getConfig('beacon.cache.did_ttl', 3600);
52
+
$this->cache->put($cacheKey, $document, $ttl);
53
+
}
54
+
55
+
return $document;
56
+
}
57
+
58
+
/**
59
+
* Convert a handle to its DID.
60
+
*
61
+
* @param string $handle
62
+
* @param bool $useCache
63
+
* @return string
64
+
* @throws HandleResolutionException
65
+
*/
66
+
public function handleToDid(string $handle, bool $useCache = true): string
67
+
{
68
+
$cacheKey = "handle:{$handle}";
69
+
70
+
if ($useCache && $this->cache->has($cacheKey)) {
71
+
return $this->cache->get($cacheKey);
72
+
}
73
+
74
+
$did = $this->handleResolver->resolve($handle);
75
+
76
+
if ($useCache) {
77
+
$ttl = $this->getConfig('beacon.cache.handle_ttl', 3600);
78
+
$this->cache->put($cacheKey, $did, $ttl);
79
+
}
80
+
81
+
return $did;
82
+
}
83
+
84
+
/**
85
+
* Resolve a handle to its DID Document.
86
+
*
87
+
* @param string $handle
88
+
* @param bool $useCache
89
+
* @return DidDocument
90
+
* @throws DidResolutionException
91
+
* @throws HandleResolutionException
92
+
*/
93
+
public function resolveHandle(string $handle, bool $useCache = true): DidDocument
94
+
{
95
+
$did = $this->handleToDid($handle, $useCache);
96
+
97
+
return $this->resolveDid($did, $useCache);
98
+
}
99
+
100
+
/**
101
+
* Resolve an identity (DID or handle) to its DID Document.
102
+
*
103
+
* @param string $identifier A DID or handle
104
+
* @param bool $useCache
105
+
* @return DidDocument
106
+
* @throws DidResolutionException
107
+
* @throws HandleResolutionException
108
+
*/
109
+
public function resolveIdentity(string $identifier, bool $useCache = true): DidDocument
110
+
{
111
+
return Identity::isDid($identifier)
112
+
? $this->resolveDid($identifier, $useCache)
113
+
: $this->resolveHandle($identifier, $useCache);
114
+
}
115
+
116
+
/**
117
+
* Clear cached data for a DID.
118
+
*
119
+
* @param string $did
120
+
*/
121
+
public function clearDidCache(string $did): void
122
+
{
123
+
$this->cache->forget("did:{$did}");
124
+
}
125
+
126
+
/**
127
+
* Clear cached data for a handle.
128
+
*
129
+
* @param string $handle
130
+
*/
131
+
public function clearHandleCache(string $handle): void
132
+
{
133
+
$this->cache->forget("handle:{$handle}");
134
+
}
135
+
136
+
/**
137
+
* Clear all cached data.
138
+
*/
139
+
public function clearCache(): void
140
+
{
141
+
$this->cache->flush();
142
+
}
143
+
144
+
/**
145
+
* Resolve a DID or handle to its PDS endpoint.
146
+
*
147
+
* @param string $actor A DID (e.g., "did:plc:abc123") or handle (e.g., "user.bsky.social")
148
+
* @param bool $useCache
149
+
* @return string|null The PDS endpoint URL or null if not found
150
+
* @throws DidResolutionException
151
+
* @throws HandleResolutionException
152
+
*/
153
+
public function resolvePds(string $actor, bool $useCache = true): ?string
154
+
{
155
+
$cacheKey = "pds:{$actor}";
156
+
157
+
if ($useCache && $this->cache->has($cacheKey)) {
158
+
return $this->cache->get($cacheKey);
159
+
}
160
+
161
+
// Determine if input is a DID or handle
162
+
$document = $this->resolveIdentity($actor, $useCache);
163
+
164
+
$pdsEndpoint = $document->getPdsEndpoint();
165
+
166
+
if ($useCache && $pdsEndpoint !== null) {
167
+
$ttl = $this->getConfig('beacon.cache.pds_ttl', 3600);
168
+
$this->cache->put($cacheKey, $pdsEndpoint, $ttl);
169
+
}
170
+
171
+
return $pdsEndpoint;
172
+
}
173
+
174
+
/**
175
+
* Clear cached PDS endpoint for a DID or handle.
176
+
*
177
+
* @param string $actor
178
+
*/
179
+
public function clearPdsCache(string $actor): void
180
+
{
181
+
$this->cache->forget("pds:{$actor}");
182
+
}
183
+
}
+44
-47
src/BeaconServiceProvider.php
+44
-47
src/BeaconServiceProvider.php
···
3
3
namespace SocialDept\Beacon;
4
4
5
5
use Illuminate\Support\ServiceProvider;
6
+
use SocialDept\Beacon\Cache\LaravelCacheStore;
7
+
use SocialDept\Beacon\Contracts\CacheStore;
8
+
use SocialDept\Beacon\Contracts\DidResolver;
9
+
use SocialDept\Beacon\Contracts\HandleResolver;
10
+
use SocialDept\Beacon\Resolvers\AtProtoHandleResolver;
11
+
use SocialDept\Beacon\Resolvers\DidResolverManager;
6
12
7
13
class BeaconServiceProvider extends ServiceProvider
8
14
{
9
15
/**
10
-
* Perform post-registration booting of services.
11
-
*
12
-
* @return void
16
+
* Register any package services.
13
17
*/
14
-
public function boot(): void
18
+
public function register(): void
15
19
{
16
-
// $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'social-dept');
17
-
// $this->loadViewsFrom(__DIR__.'/../resources/views', 'social-dept');
18
-
// $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
19
-
// $this->loadRoutesFrom(__DIR__.'/routes.php');
20
+
$this->mergeConfigFrom(__DIR__.'/../config/beacon.php', 'beacon');
20
21
21
-
// Publishing is only necessary when using the CLI.
22
-
if ($this->app->runningInConsole()) {
23
-
$this->bootForConsole();
24
-
}
22
+
// Register cache store
23
+
$this->app->singleton(CacheStore::class, function ($app) {
24
+
return new LaravelCacheStore($app->make('cache')->store());
25
+
});
26
+
27
+
// Register DID resolver
28
+
$this->app->singleton(DidResolver::class, function ($app) {
29
+
return new DidResolverManager();
30
+
});
31
+
32
+
// Register handle resolver
33
+
$this->app->singleton(HandleResolver::class, function ($app) {
34
+
return new AtProtoHandleResolver();
35
+
});
36
+
37
+
// Register Beacon service
38
+
$this->app->singleton('beacon', function ($app) {
39
+
return new Beacon(
40
+
$app->make(DidResolver::class),
41
+
$app->make(HandleResolver::class),
42
+
$app->make(CacheStore::class),
43
+
);
44
+
});
45
+
46
+
$this->app->alias('beacon', Beacon::class);
25
47
}
26
48
27
49
/**
28
-
* Register any package services.
29
-
*
30
-
* @return void
50
+
* Bootstrap the application services.
31
51
*/
32
-
public function register(): void
52
+
public function boot(): void
33
53
{
34
-
$this->mergeConfigFrom(__DIR__.'/../config/beacon.php', 'beacon');
35
-
36
-
// Register the service the package provides.
37
-
$this->app->singleton('beacon', function ($app) {
38
-
return new Beacon;
39
-
});
54
+
if ($this->app->runningInConsole()) {
55
+
$this->bootForConsole();
56
+
}
40
57
}
41
58
42
59
/**
43
60
* Get the services provided by the provider.
44
61
*
45
-
* @return array
62
+
* @return array<string>
46
63
*/
47
-
public function provides()
64
+
public function provides(): array
48
65
{
49
-
return ['beacon'];
66
+
return ['beacon', Beacon::class];
50
67
}
51
68
52
69
/**
53
70
* Console-specific booting.
54
-
*
55
-
* @return void
56
71
*/
57
72
protected function bootForConsole(): void
58
73
{
59
-
// Publishing the configuration file.
74
+
// Publish config
60
75
$this->publishes([
61
76
__DIR__.'/../config/beacon.php' => config_path('beacon.php'),
62
-
], 'beacon.config');
63
-
64
-
// Publishing the views.
65
-
/*$this->publishes([
66
-
__DIR__.'/../resources/views' => base_path('resources/views/vendor/social-dept'),
67
-
], 'beacon.views');*/
68
-
69
-
// Publishing assets.
70
-
/*$this->publishes([
71
-
__DIR__.'/../resources/assets' => public_path('vendor/social-dept'),
72
-
], 'beacon.assets');*/
73
-
74
-
// Publishing the translation files.
75
-
/*$this->publishes([
76
-
__DIR__.'/../resources/lang' => resource_path('lang/vendor/social-dept'),
77
-
], 'beacon.lang');*/
78
-
79
-
// Registering package commands.
80
-
// $this->commands([]);
77
+
], 'beacon-config');
81
78
}
82
79
}
+69
src/Cache/LaravelCacheStore.php
+69
src/Cache/LaravelCacheStore.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Cache;
4
+
5
+
use Illuminate\Contracts\Cache\Repository;
6
+
use SocialDept\Beacon\Contracts\CacheStore;
7
+
8
+
class LaravelCacheStore implements CacheStore
9
+
{
10
+
protected string $prefix = 'beacon:';
11
+
12
+
/**
13
+
* Create a new Laravel cache store instance.
14
+
*/
15
+
public function __construct(
16
+
protected Repository $cache
17
+
) {
18
+
}
19
+
20
+
/**
21
+
* Get a cached value.
22
+
*
23
+
* @param string $key
24
+
*/
25
+
public function get(string $key): mixed
26
+
{
27
+
return $this->cache->get($this->prefix.$key);
28
+
}
29
+
30
+
/**
31
+
* Store a value in the cache.
32
+
*
33
+
* @param string $key
34
+
* @param mixed $value
35
+
* @param int $ttl Time to live in seconds
36
+
*/
37
+
public function put(string $key, mixed $value, int $ttl): void
38
+
{
39
+
$this->cache->put($this->prefix.$key, $value, $ttl);
40
+
}
41
+
42
+
/**
43
+
* Check if a key exists in the cache.
44
+
*
45
+
* @param string $key
46
+
*/
47
+
public function has(string $key): bool
48
+
{
49
+
return $this->cache->has($this->prefix.$key);
50
+
}
51
+
52
+
/**
53
+
* Remove a value from the cache.
54
+
*
55
+
* @param string $key
56
+
*/
57
+
public function forget(string $key): void
58
+
{
59
+
$this->cache->forget($this->prefix.$key);
60
+
}
61
+
62
+
/**
63
+
* Clear all cached values.
64
+
*/
65
+
public function flush(): void
66
+
{
67
+
$this->cache->flush();
68
+
}
69
+
}
+42
src/Contracts/CacheStore.php
+42
src/Contracts/CacheStore.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Contracts;
4
+
5
+
interface CacheStore
6
+
{
7
+
/**
8
+
* Get a cached value.
9
+
*
10
+
* @param string $key
11
+
* @return mixed
12
+
*/
13
+
public function get(string $key): mixed;
14
+
15
+
/**
16
+
* Store a value in the cache.
17
+
*
18
+
* @param string $key
19
+
* @param mixed $value
20
+
* @param int $ttl Time to live in seconds
21
+
*/
22
+
public function put(string $key, mixed $value, int $ttl): void;
23
+
24
+
/**
25
+
* Check if a key exists in the cache.
26
+
*
27
+
* @param string $key
28
+
*/
29
+
public function has(string $key): bool;
30
+
31
+
/**
32
+
* Remove a value from the cache.
33
+
*
34
+
* @param string $key
35
+
*/
36
+
public function forget(string $key): void;
37
+
38
+
/**
39
+
* Clear all cached values.
40
+
*/
41
+
public function flush(): void;
42
+
}
+25
src/Contracts/DidResolver.php
+25
src/Contracts/DidResolver.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Contracts;
4
+
5
+
use SocialDept\Beacon\Data\DidDocument;
6
+
7
+
interface DidResolver
8
+
{
9
+
/**
10
+
* Resolve a DID to a DID Document.
11
+
*
12
+
* @param string $did The DID to resolve (e.g., "did:plc:abc123" or "did:web:example.com")
13
+
* @return DidDocument
14
+
*
15
+
* @throws \SocialDept\Beacon\Exceptions\DidResolutionException
16
+
*/
17
+
public function resolve(string $did): DidDocument;
18
+
19
+
/**
20
+
* Check if this resolver supports the given DID method.
21
+
*
22
+
* @param string $method The DID method (e.g., "plc", "web")
23
+
*/
24
+
public function supports(string $method): bool;
25
+
}
+16
src/Contracts/HandleResolver.php
+16
src/Contracts/HandleResolver.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Contracts;
4
+
5
+
interface HandleResolver
6
+
{
7
+
/**
8
+
* Resolve a handle to a DID.
9
+
*
10
+
* @param string $handle The handle to resolve (e.g., "user.bsky.social")
11
+
* @return string The resolved DID
12
+
*
13
+
* @throws \SocialDept\Beacon\Exceptions\HandleResolutionException
14
+
*/
15
+
public function resolve(string $handle): string;
16
+
}
+76
src/Data/DidDocument.php
+76
src/Data/DidDocument.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Data;
4
+
5
+
class DidDocument
6
+
{
7
+
/**
8
+
* Create a new DID Document instance.
9
+
*
10
+
* @param string $id The DID (e.g., "did:plc:abc123")
11
+
* @param array $alsoKnownAs Alternative identifiers (handles)
12
+
* @param array $verificationMethod Verification methods (keys)
13
+
* @param array $service Service endpoints (PDS, etc.)
14
+
* @param array $raw The raw DID document
15
+
*/
16
+
public function __construct(
17
+
public readonly string $id,
18
+
public readonly array $alsoKnownAs = [],
19
+
public readonly array $verificationMethod = [],
20
+
public readonly array $service = [],
21
+
public readonly array $raw = [],
22
+
) {
23
+
}
24
+
25
+
/**
26
+
* Create a DID Document from a raw array.
27
+
*
28
+
* @param array $data
29
+
*/
30
+
public static function fromArray(array $data): self
31
+
{
32
+
return new self(
33
+
id: $data['id'] ?? '',
34
+
alsoKnownAs: $data['alsoKnownAs'] ?? [],
35
+
verificationMethod: $data['verificationMethod'] ?? [],
36
+
service: $data['service'] ?? [],
37
+
raw: $data,
38
+
);
39
+
}
40
+
41
+
/**
42
+
* Get the PDS (Personal Data Server) endpoint.
43
+
*/
44
+
public function getPdsEndpoint(): ?string
45
+
{
46
+
foreach ($this->service as $service) {
47
+
if (($service['type'] ?? '') === 'AtprotoPersonalDataServer') {
48
+
return $service['serviceEndpoint'] ?? null;
49
+
}
50
+
}
51
+
52
+
return null;
53
+
}
54
+
55
+
/**
56
+
* Get the handle from alsoKnownAs.
57
+
*/
58
+
public function getHandle(): ?string
59
+
{
60
+
foreach ($this->alsoKnownAs as $alias) {
61
+
if (str_starts_with($alias, 'at://')) {
62
+
return substr($alias, 5);
63
+
}
64
+
}
65
+
66
+
return null;
67
+
}
68
+
69
+
/**
70
+
* Convert to array.
71
+
*/
72
+
public function toArray(): array
73
+
{
74
+
return $this->raw;
75
+
}
76
+
}
+10
src/Exceptions/BeaconException.php
+10
src/Exceptions/BeaconException.php
+43
src/Exceptions/DidResolutionException.php
+43
src/Exceptions/DidResolutionException.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Exceptions;
4
+
5
+
class DidResolutionException extends BeaconException
6
+
{
7
+
/**
8
+
* Create a new exception for unsupported DID method.
9
+
*
10
+
* @param string $method
11
+
*/
12
+
public static function unsupportedMethod(string $method): self
13
+
{
14
+
return new self("Unsupported DID method: {$method}");
15
+
}
16
+
17
+
/**
18
+
* Create a new exception for invalid DID format.
19
+
*
20
+
* @param string $did
21
+
*/
22
+
public static function invalidFormat(string $did): self
23
+
{
24
+
return new self("Invalid DID format: {$did}");
25
+
}
26
+
27
+
/**
28
+
* Create a new exception for resolution failure.
29
+
*
30
+
* @param string $did
31
+
* @param string $reason
32
+
*/
33
+
public static function resolutionFailed(string $did, string $reason = ''): self
34
+
{
35
+
$message = "Failed to resolve DID: {$did}";
36
+
37
+
if ($reason) {
38
+
$message .= " ({$reason})";
39
+
}
40
+
41
+
return new self($message);
42
+
}
43
+
}
+33
src/Exceptions/HandleResolutionException.php
+33
src/Exceptions/HandleResolutionException.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Exceptions;
4
+
5
+
class HandleResolutionException extends BeaconException
6
+
{
7
+
/**
8
+
* Create a new exception for invalid handle format.
9
+
*
10
+
* @param string $handle
11
+
*/
12
+
public static function invalidFormat(string $handle): self
13
+
{
14
+
return new self("Invalid handle format: {$handle}");
15
+
}
16
+
17
+
/**
18
+
* Create a new exception for resolution failure.
19
+
*
20
+
* @param string $handle
21
+
* @param string $reason
22
+
*/
23
+
public static function resolutionFailed(string $handle, string $reason = ''): self
24
+
{
25
+
$message = "Failed to resolve handle: {$handle}";
26
+
27
+
if ($reason) {
28
+
$message .= " ({$reason})";
29
+
}
30
+
31
+
return new self($message);
32
+
}
33
+
}
+75
src/Resolvers/AtProtoHandleResolver.php
+75
src/Resolvers/AtProtoHandleResolver.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Resolvers;
4
+
5
+
use GuzzleHttp\Client;
6
+
use GuzzleHttp\Exception\GuzzleException;
7
+
use SocialDept\Beacon\Contracts\HandleResolver;
8
+
use SocialDept\Beacon\Exceptions\HandleResolutionException;
9
+
use SocialDept\Beacon\Support\Concerns\HasConfig;
10
+
11
+
class AtProtoHandleResolver implements HandleResolver
12
+
{
13
+
use HasConfig;
14
+
15
+
protected Client $client;
16
+
17
+
protected string $pdsEndpoint;
18
+
19
+
/**
20
+
* Create a new AT Protocol handle resolver instance.
21
+
*
22
+
* @param string|null $pdsEndpoint The PDS endpoint to use for resolution
23
+
*/
24
+
public function __construct(?string $pdsEndpoint = null, ?int $timeout = null)
25
+
{
26
+
$this->pdsEndpoint = $pdsEndpoint ?? $this->getConfig('beacon.pds_endpoint', 'https://bsky.social');
27
+
$this->client = new Client([
28
+
'timeout' => $timeout ?? $this->getConfig('beacon.timeout', 10),
29
+
'headers' => [
30
+
'Accept' => 'application/json',
31
+
'User-Agent' => 'Beacon/1.0',
32
+
],
33
+
]);
34
+
}
35
+
36
+
/**
37
+
* Resolve a handle to a DID.
38
+
*
39
+
* @param string $handle The handle to resolve (e.g., "user.bsky.social")
40
+
* @return string The resolved DID
41
+
*/
42
+
public function resolve(string $handle): string
43
+
{
44
+
$this->validateHandle($handle);
45
+
46
+
try {
47
+
$response = $this->client->get("{$this->pdsEndpoint}/xrpc/com.atproto.identity.resolveHandle", [
48
+
'query' => ['handle' => $handle],
49
+
]);
50
+
51
+
$data = json_decode($response->getBody()->getContents(), true);
52
+
53
+
if (! isset($data['did'])) {
54
+
throw HandleResolutionException::resolutionFailed($handle, 'No DID in response');
55
+
}
56
+
57
+
return $data['did'];
58
+
} catch (GuzzleException $e) {
59
+
throw HandleResolutionException::resolutionFailed($handle, $e->getMessage());
60
+
}
61
+
}
62
+
63
+
/**
64
+
* Validate a handle format.
65
+
*
66
+
* @param string $handle
67
+
*/
68
+
protected function validateHandle(string $handle): void
69
+
{
70
+
// Handle must be a valid domain name
71
+
if (! preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $handle)) {
72
+
throw HandleResolutionException::invalidFormat($handle);
73
+
}
74
+
}
75
+
}
+74
src/Resolvers/DidResolverManager.php
+74
src/Resolvers/DidResolverManager.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Resolvers;
4
+
5
+
use SocialDept\Beacon\Contracts\DidResolver;
6
+
use SocialDept\Beacon\Data\DidDocument;
7
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
8
+
use SocialDept\Beacon\Support\Concerns\ParsesDid;
9
+
10
+
class DidResolverManager implements DidResolver
11
+
{
12
+
use ParsesDid;
13
+
14
+
/**
15
+
* @var array<string, DidResolver>
16
+
*/
17
+
protected array $resolvers = [];
18
+
19
+
/**
20
+
* Create a new DID resolver manager instance.
21
+
*/
22
+
public function __construct()
23
+
{
24
+
$this->registerDefaultResolvers();
25
+
}
26
+
27
+
/**
28
+
* Resolve a DID to a DID Document.
29
+
*
30
+
* @param string $did The DID to resolve
31
+
*/
32
+
public function resolve(string $did): DidDocument
33
+
{
34
+
$method = $this->extractMethod($did);
35
+
36
+
if (! $this->supports($method)) {
37
+
throw DidResolutionException::unsupportedMethod($method);
38
+
}
39
+
40
+
return $this->resolvers[$method]->resolve($did);
41
+
}
42
+
43
+
/**
44
+
* Check if this resolver supports the given DID method.
45
+
*
46
+
* @param string $method The DID method
47
+
*/
48
+
public function supports(string $method): bool
49
+
{
50
+
return isset($this->resolvers[$method]);
51
+
}
52
+
53
+
/**
54
+
* Register a DID resolver for a specific method.
55
+
*
56
+
* @param string $method
57
+
* @param DidResolver $resolver
58
+
*/
59
+
public function register(string $method, DidResolver $resolver): self
60
+
{
61
+
$this->resolvers[$method] = $resolver;
62
+
63
+
return $this;
64
+
}
65
+
66
+
/**
67
+
* Register the default resolvers.
68
+
*/
69
+
protected function registerDefaultResolvers(): void
70
+
{
71
+
$this->register('plc', new PlcDidResolver());
72
+
$this->register('web', new WebDidResolver());
73
+
}
74
+
}
+73
src/Resolvers/PlcDidResolver.php
+73
src/Resolvers/PlcDidResolver.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Resolvers;
4
+
5
+
use GuzzleHttp\Client;
6
+
use GuzzleHttp\Exception\GuzzleException;
7
+
use SocialDept\Beacon\Contracts\DidResolver;
8
+
use SocialDept\Beacon\Data\DidDocument;
9
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
10
+
use SocialDept\Beacon\Support\Concerns\HasConfig;
11
+
use SocialDept\Beacon\Support\Concerns\ParsesDid;
12
+
13
+
class PlcDidResolver implements DidResolver
14
+
{
15
+
use HasConfig;
16
+
use ParsesDid;
17
+
18
+
protected Client $client;
19
+
20
+
protected string $plcDirectory;
21
+
22
+
/**
23
+
* Create a new PLC DID resolver instance.
24
+
*
25
+
* @param string $plcDirectory The PLC directory URL
26
+
*/
27
+
public function __construct(?string $plcDirectory = null, ?int $timeout = null)
28
+
{
29
+
$this->plcDirectory = $plcDirectory ?? $this->getConfig('beacon.plc_directory', 'https://plc.directory');
30
+
$this->client = new Client([
31
+
'timeout' => $timeout ?? $this->getConfig('beacon.timeout', 10),
32
+
'headers' => [
33
+
'Accept' => 'application/json',
34
+
'User-Agent' => 'Beacon/1.0',
35
+
],
36
+
]);
37
+
}
38
+
39
+
/**
40
+
* Resolve a DID:PLC to a DID Document.
41
+
*
42
+
* @param string $did The DID to resolve (e.g., "did:plc:abc123")
43
+
*/
44
+
public function resolve(string $did): DidDocument
45
+
{
46
+
if (! $this->supports($this->extractMethod($did))) {
47
+
throw DidResolutionException::unsupportedMethod($this->extractMethod($did));
48
+
}
49
+
50
+
try {
51
+
$response = $this->client->get("{$this->plcDirectory}/{$did}");
52
+
$data = json_decode($response->getBody()->getContents(), true);
53
+
54
+
if (! is_array($data)) {
55
+
throw DidResolutionException::resolutionFailed($did, 'Invalid response format');
56
+
}
57
+
58
+
return DidDocument::fromArray($data);
59
+
} catch (GuzzleException $e) {
60
+
throw DidResolutionException::resolutionFailed($did, $e->getMessage());
61
+
}
62
+
}
63
+
64
+
/**
65
+
* Check if this resolver supports the given DID method.
66
+
*
67
+
* @param string $method The DID method (e.g., "plc")
68
+
*/
69
+
public function supports(string $method): bool
70
+
{
71
+
return $method === 'plc';
72
+
}
73
+
}
+96
src/Resolvers/WebDidResolver.php
+96
src/Resolvers/WebDidResolver.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Resolvers;
4
+
5
+
use GuzzleHttp\Client;
6
+
use GuzzleHttp\Exception\GuzzleException;
7
+
use SocialDept\Beacon\Contracts\DidResolver;
8
+
use SocialDept\Beacon\Data\DidDocument;
9
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
10
+
use SocialDept\Beacon\Support\Concerns\HasConfig;
11
+
use SocialDept\Beacon\Support\Concerns\ParsesDid;
12
+
13
+
class WebDidResolver implements DidResolver
14
+
{
15
+
use HasConfig;
16
+
use ParsesDid;
17
+
18
+
protected Client $client;
19
+
20
+
/**
21
+
* Create a new Web DID resolver instance.
22
+
*/
23
+
public function __construct(?int $timeout = null)
24
+
{
25
+
$this->client = new Client([
26
+
'timeout' => $timeout ?? $this->getConfig('beacon.timeout', 10),
27
+
'headers' => [
28
+
'Accept' => 'application/json',
29
+
'User-Agent' => 'Beacon/1.0',
30
+
],
31
+
]);
32
+
}
33
+
34
+
/**
35
+
* Resolve a DID:Web to a DID Document.
36
+
*
37
+
* @param string $did The DID to resolve (e.g., "did:web:example.com")
38
+
*/
39
+
public function resolve(string $did): DidDocument
40
+
{
41
+
if (! $this->supports($this->extractMethod($did))) {
42
+
throw DidResolutionException::unsupportedMethod($this->extractMethod($did));
43
+
}
44
+
45
+
$url = $this->buildDidUrl($did);
46
+
47
+
try {
48
+
$response = $this->client->get($url);
49
+
$data = json_decode($response->getBody()->getContents(), true);
50
+
51
+
if (! is_array($data)) {
52
+
throw DidResolutionException::resolutionFailed($did, 'Invalid response format');
53
+
}
54
+
55
+
return DidDocument::fromArray($data);
56
+
} catch (GuzzleException $e) {
57
+
throw DidResolutionException::resolutionFailed($did, $e->getMessage());
58
+
}
59
+
}
60
+
61
+
/**
62
+
* Check if this resolver supports the given DID method.
63
+
*
64
+
* @param string $method The DID method (e.g., "web")
65
+
*/
66
+
public function supports(string $method): bool
67
+
{
68
+
return $method === 'web';
69
+
}
70
+
71
+
/**
72
+
* Build the URL to fetch the DID document from.
73
+
*
74
+
* @param string $did
75
+
*/
76
+
protected function buildDidUrl(string $did): string
77
+
{
78
+
$identifier = $this->extractIdentifier($did);
79
+
80
+
// Decode URL-encoded characters
81
+
$domain = str_replace('%3A', ':', $identifier);
82
+
83
+
// Split domain and path
84
+
$parts = explode(':', $domain);
85
+
$domainName = array_shift($parts);
86
+
87
+
// If there's a path, append it; otherwise use .well-known/did.json
88
+
if (count($parts) > 0) {
89
+
$path = implode('/', $parts).'/did.json';
90
+
} else {
91
+
$path = '.well-known/did.json';
92
+
}
93
+
94
+
return "https://{$domainName}/{$path}";
95
+
}
96
+
}
+22
src/Support/Concerns/HasConfig.php
+22
src/Support/Concerns/HasConfig.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Support\Concerns;
4
+
5
+
trait HasConfig
6
+
{
7
+
/**
8
+
* Get configuration value with fallback for testing.
9
+
*/
10
+
protected function getConfig(string $key, mixed $default): mixed
11
+
{
12
+
if (! function_exists('config')) {
13
+
return $default;
14
+
}
15
+
16
+
try {
17
+
return config($key, $default);
18
+
} catch (\Throwable) {
19
+
return $default;
20
+
}
21
+
}
22
+
}
+40
src/Support/Concerns/ParsesDid.php
+40
src/Support/Concerns/ParsesDid.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Support\Concerns;
4
+
5
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
6
+
7
+
trait ParsesDid
8
+
{
9
+
/**
10
+
* Extract the method from a DID.
11
+
*/
12
+
protected function extractMethod(string $did): string
13
+
{
14
+
if (! str_starts_with($did, 'did:')) {
15
+
throw DidResolutionException::invalidFormat($did);
16
+
}
17
+
18
+
$parts = explode(':', $did);
19
+
20
+
if (count($parts) < 3) {
21
+
throw DidResolutionException::invalidFormat($did);
22
+
}
23
+
24
+
return $parts[1];
25
+
}
26
+
27
+
/**
28
+
* Extract the identifier from a DID.
29
+
*/
30
+
protected function extractIdentifier(string $did): string
31
+
{
32
+
$parts = explode(':', $did);
33
+
34
+
if (count($parts) < 3) {
35
+
throw DidResolutionException::invalidFormat($did);
36
+
}
37
+
38
+
return implode(':', array_slice($parts, 2));
39
+
}
40
+
}
+69
src/Support/Identity.php
+69
src/Support/Identity.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Support;
4
+
5
+
class Identity
6
+
{
7
+
// "***.bsky.social" "alice.test"
8
+
protected const HANDLE_REGEX = '/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/';
9
+
10
+
// "did:plc:1234..." "did:web:alice.test"
11
+
protected const DID_REGEX = '/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/';
12
+
13
+
/**
14
+
* Check if a string is a valid handle.
15
+
*
16
+
* @param string|null $handle
17
+
*/
18
+
public static function isHandle(?string $handle): bool
19
+
{
20
+
return preg_match(self::HANDLE_REGEX, $handle ?? '') === 1;
21
+
}
22
+
23
+
/**
24
+
* Check if a string is a valid DID.
25
+
*
26
+
* @param string|null $did
27
+
*/
28
+
public static function isDid(?string $did): bool
29
+
{
30
+
return preg_match(self::DID_REGEX, $did ?? '') === 1;
31
+
}
32
+
33
+
/**
34
+
* Extract the DID method from a DID string.
35
+
*
36
+
* @param string $did
37
+
* @return string|null Returns the method (e.g., "plc", "web") or null if invalid
38
+
*/
39
+
public static function extractDidMethod(string $did): ?string
40
+
{
41
+
if (! self::isDid($did)) {
42
+
return null;
43
+
}
44
+
45
+
$parts = explode(':', $did);
46
+
47
+
return $parts[1] ?? null;
48
+
}
49
+
50
+
/**
51
+
* Check if a DID uses the PLC method.
52
+
*
53
+
* @param string $did
54
+
*/
55
+
public static function isPlcDid(string $did): bool
56
+
{
57
+
return self::extractDidMethod($did) === 'plc';
58
+
}
59
+
60
+
/**
61
+
* Check if a DID uses the Web method.
62
+
*
63
+
* @param string $did
64
+
*/
65
+
public static function isWebDid(string $did): bool
66
+
{
67
+
return self::extractDidMethod($did) === 'web';
68
+
}
69
+
}
+121
tests/Unit/BeaconIdentityTest.php
+121
tests/Unit/BeaconIdentityTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Tests\Unit;
4
+
5
+
use PHPUnit\Framework\TestCase;
6
+
use SocialDept\Beacon\Beacon;
7
+
use SocialDept\Beacon\Contracts\CacheStore;
8
+
use SocialDept\Beacon\Contracts\DidResolver;
9
+
use SocialDept\Beacon\Contracts\HandleResolver;
10
+
use SocialDept\Beacon\Data\DidDocument;
11
+
12
+
class BeaconIdentityTest extends TestCase
13
+
{
14
+
public function test_it_can_convert_handle_to_did(): void
15
+
{
16
+
$didResolver = $this->createMock(DidResolver::class);
17
+
$handleResolver = $this->createMock(HandleResolver::class);
18
+
$cache = $this->createMock(CacheStore::class);
19
+
20
+
$handleResolver->expects($this->once())
21
+
->method('resolve')
22
+
->with('user.bsky.social')
23
+
->willReturn('did:plc:abc123');
24
+
25
+
$cache->method('has')->willReturn(false);
26
+
$cache->expects($this->once())
27
+
->method('put')
28
+
->with('handle:user.bsky.social', 'did:plc:abc123', $this->anything());
29
+
30
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
31
+
$did = $beacon->handleToDid('user.bsky.social');
32
+
33
+
$this->assertSame('did:plc:abc123', $did);
34
+
}
35
+
36
+
public function test_it_can_resolve_handle(): void
37
+
{
38
+
$didResolver = $this->createMock(DidResolver::class);
39
+
$handleResolver = $this->createMock(HandleResolver::class);
40
+
$cache = $this->createMock(CacheStore::class);
41
+
42
+
$handleResolver->expects($this->once())
43
+
->method('resolve')
44
+
->with('user.bsky.social')
45
+
->willReturn('did:plc:abc123');
46
+
47
+
$didDocument = DidDocument::fromArray([
48
+
'id' => 'did:plc:abc123',
49
+
'alsoKnownAs' => ['at://user.bsky.social'],
50
+
]);
51
+
52
+
$didResolver->expects($this->once())
53
+
->method('resolve')
54
+
->with('did:plc:abc123')
55
+
->willReturn($didDocument);
56
+
57
+
$cache->method('has')->willReturn(false);
58
+
59
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
60
+
$document = $beacon->resolveHandle('user.bsky.social');
61
+
62
+
$this->assertInstanceOf(DidDocument::class, $document);
63
+
$this->assertSame('did:plc:abc123', $document->id);
64
+
}
65
+
66
+
public function test_it_can_resolve_identity_with_did(): void
67
+
{
68
+
$didResolver = $this->createMock(DidResolver::class);
69
+
$handleResolver = $this->createMock(HandleResolver::class);
70
+
$cache = $this->createMock(CacheStore::class);
71
+
72
+
$didDocument = DidDocument::fromArray([
73
+
'id' => 'did:plc:abc123',
74
+
]);
75
+
76
+
$didResolver->expects($this->once())
77
+
->method('resolve')
78
+
->with('did:plc:abc123')
79
+
->willReturn($didDocument);
80
+
81
+
$cache->method('has')->willReturn(false);
82
+
$handleResolver->expects($this->never())->method('resolve');
83
+
84
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
85
+
$document = $beacon->resolveIdentity('did:plc:abc123');
86
+
87
+
$this->assertInstanceOf(DidDocument::class, $document);
88
+
$this->assertSame('did:plc:abc123', $document->id);
89
+
}
90
+
91
+
public function test_it_can_resolve_identity_with_handle(): void
92
+
{
93
+
$didResolver = $this->createMock(DidResolver::class);
94
+
$handleResolver = $this->createMock(HandleResolver::class);
95
+
$cache = $this->createMock(CacheStore::class);
96
+
97
+
$handleResolver->expects($this->once())
98
+
->method('resolve')
99
+
->with('user.bsky.social')
100
+
->willReturn('did:plc:abc123');
101
+
102
+
$didDocument = DidDocument::fromArray([
103
+
'id' => 'did:plc:abc123',
104
+
'alsoKnownAs' => ['at://user.bsky.social'],
105
+
]);
106
+
107
+
$didResolver->expects($this->once())
108
+
->method('resolve')
109
+
->with('did:plc:abc123')
110
+
->willReturn($didDocument);
111
+
112
+
$cache->method('has')->willReturn(false);
113
+
114
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
115
+
$document = $beacon->resolveIdentity('user.bsky.social');
116
+
117
+
$this->assertInstanceOf(DidDocument::class, $document);
118
+
$this->assertSame('did:plc:abc123', $document->id);
119
+
$this->assertSame('user.bsky.social', $document->getHandle());
120
+
}
121
+
}
+161
tests/Unit/BeaconTest.php
+161
tests/Unit/BeaconTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Tests\Unit;
4
+
5
+
use PHPUnit\Framework\TestCase;
6
+
use SocialDept\Beacon\Beacon;
7
+
use SocialDept\Beacon\Contracts\CacheStore;
8
+
use SocialDept\Beacon\Contracts\DidResolver;
9
+
use SocialDept\Beacon\Contracts\HandleResolver;
10
+
use SocialDept\Beacon\Data\DidDocument;
11
+
12
+
class BeaconTest extends TestCase
13
+
{
14
+
public function test_it_can_resolve_pds_from_did(): void
15
+
{
16
+
$didResolver = $this->createMock(DidResolver::class);
17
+
$handleResolver = $this->createMock(HandleResolver::class);
18
+
$cache = $this->createMock(CacheStore::class);
19
+
20
+
$didDocument = DidDocument::fromArray([
21
+
'id' => 'did:plc:abc123',
22
+
'service' => [
23
+
[
24
+
'type' => 'AtprotoPersonalDataServer',
25
+
'serviceEndpoint' => 'https://pds.example.com',
26
+
],
27
+
],
28
+
]);
29
+
30
+
$didResolver->expects($this->once())
31
+
->method('resolve')
32
+
->with('did:plc:abc123')
33
+
->willReturn($didDocument);
34
+
35
+
$cache->method('has')->willReturn(false);
36
+
37
+
// Expect multiple cache puts (DID document + PDS endpoint)
38
+
$cache->expects($this->exactly(2))
39
+
->method('put')
40
+
->willReturnCallback(function ($key, $value, $ttl) {
41
+
$this->assertContains($key, ['did:did:plc:abc123', 'pds:did:plc:abc123']);
42
+
43
+
return null;
44
+
});
45
+
46
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
47
+
$pds = $beacon->resolvePds('did:plc:abc123');
48
+
49
+
$this->assertSame('https://pds.example.com', $pds);
50
+
}
51
+
52
+
public function test_it_can_resolve_pds_from_handle(): void
53
+
{
54
+
$didResolver = $this->createMock(DidResolver::class);
55
+
$handleResolver = $this->createMock(HandleResolver::class);
56
+
$cache = $this->createMock(CacheStore::class);
57
+
58
+
$handleResolver->expects($this->once())
59
+
->method('resolve')
60
+
->with('user.bsky.social')
61
+
->willReturn('did:plc:abc123');
62
+
63
+
$didDocument = DidDocument::fromArray([
64
+
'id' => 'did:plc:abc123',
65
+
'service' => [
66
+
[
67
+
'type' => 'AtprotoPersonalDataServer',
68
+
'serviceEndpoint' => 'https://pds.example.com',
69
+
],
70
+
],
71
+
]);
72
+
73
+
$didResolver->expects($this->once())
74
+
->method('resolve')
75
+
->with('did:plc:abc123')
76
+
->willReturn($didDocument);
77
+
78
+
$cache->method('has')->willReturn(false);
79
+
80
+
// Expect multiple cache puts (handle + DID document + PDS endpoint)
81
+
$cache->expects($this->exactly(3))
82
+
->method('put')
83
+
->willReturnCallback(function ($key, $value, $ttl) {
84
+
$this->assertContains($key, ['handle:user.bsky.social', 'did:did:plc:abc123', 'pds:user.bsky.social']);
85
+
86
+
return null;
87
+
});
88
+
89
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
90
+
$pds = $beacon->resolvePds('user.bsky.social');
91
+
92
+
$this->assertSame('https://pds.example.com', $pds);
93
+
}
94
+
95
+
public function test_it_returns_null_when_no_pds_endpoint(): void
96
+
{
97
+
$didResolver = $this->createMock(DidResolver::class);
98
+
$handleResolver = $this->createMock(HandleResolver::class);
99
+
$cache = $this->createMock(CacheStore::class);
100
+
101
+
$didDocument = DidDocument::fromArray([
102
+
'id' => 'did:plc:abc123',
103
+
'service' => [],
104
+
]);
105
+
106
+
$didResolver->expects($this->once())
107
+
->method('resolve')
108
+
->with('did:plc:abc123')
109
+
->willReturn($didDocument);
110
+
111
+
$cache->method('has')->willReturn(false);
112
+
113
+
// DID document is still cached, but PDS endpoint is not (since it's null)
114
+
$cache->expects($this->once())
115
+
->method('put')
116
+
->with('did:did:plc:abc123', $didDocument, $this->anything());
117
+
118
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
119
+
$pds = $beacon->resolvePds('did:plc:abc123');
120
+
121
+
$this->assertNull($pds);
122
+
}
123
+
124
+
public function test_it_uses_cached_pds_endpoint(): void
125
+
{
126
+
$didResolver = $this->createMock(DidResolver::class);
127
+
$handleResolver = $this->createMock(HandleResolver::class);
128
+
$cache = $this->createMock(CacheStore::class);
129
+
130
+
$cache->expects($this->once())
131
+
->method('has')
132
+
->with('pds:did:plc:abc123')
133
+
->willReturn(true);
134
+
135
+
$cache->expects($this->once())
136
+
->method('get')
137
+
->with('pds:did:plc:abc123')
138
+
->willReturn('https://cached-pds.example.com');
139
+
140
+
$didResolver->expects($this->never())->method('resolve');
141
+
142
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
143
+
$pds = $beacon->resolvePds('did:plc:abc123');
144
+
145
+
$this->assertSame('https://cached-pds.example.com', $pds);
146
+
}
147
+
148
+
public function test_it_can_clear_pds_cache(): void
149
+
{
150
+
$didResolver = $this->createMock(DidResolver::class);
151
+
$handleResolver = $this->createMock(HandleResolver::class);
152
+
$cache = $this->createMock(CacheStore::class);
153
+
154
+
$cache->expects($this->once())
155
+
->method('forget')
156
+
->with('pds:did:plc:abc123');
157
+
158
+
$beacon = new Beacon($didResolver, $handleResolver, $cache);
159
+
$beacon->clearPdsCache('did:plc:abc123');
160
+
}
161
+
}
+87
tests/Unit/DidDocumentTest.php
+87
tests/Unit/DidDocumentTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Tests\Unit;
4
+
5
+
use PHPUnit\Framework\TestCase;
6
+
use SocialDept\Beacon\Data\DidDocument;
7
+
8
+
class DidDocumentTest extends TestCase
9
+
{
10
+
public function test_it_can_be_created_from_array(): void
11
+
{
12
+
$data = [
13
+
'id' => 'did:plc:abc123',
14
+
'alsoKnownAs' => ['at://user.bsky.social'],
15
+
'verificationMethod' => [],
16
+
'service' => [
17
+
[
18
+
'type' => 'AtprotoPersonalDataServer',
19
+
'serviceEndpoint' => 'https://bsky.social',
20
+
],
21
+
],
22
+
];
23
+
24
+
$document = DidDocument::fromArray($data);
25
+
26
+
$this->assertSame('did:plc:abc123', $document->id);
27
+
$this->assertSame(['at://user.bsky.social'], $document->alsoKnownAs);
28
+
$this->assertCount(1, $document->service);
29
+
}
30
+
31
+
public function test_it_can_get_pds_endpoint(): void
32
+
{
33
+
$document = DidDocument::fromArray([
34
+
'id' => 'did:plc:abc123',
35
+
'service' => [
36
+
[
37
+
'type' => 'AtprotoPersonalDataServer',
38
+
'serviceEndpoint' => 'https://bsky.social',
39
+
],
40
+
],
41
+
]);
42
+
43
+
$this->assertSame('https://bsky.social', $document->getPdsEndpoint());
44
+
}
45
+
46
+
public function test_it_returns_null_when_no_pds_endpoint(): void
47
+
{
48
+
$document = DidDocument::fromArray([
49
+
'id' => 'did:plc:abc123',
50
+
'service' => [],
51
+
]);
52
+
53
+
$this->assertNull($document->getPdsEndpoint());
54
+
}
55
+
56
+
public function test_it_can_get_handle(): void
57
+
{
58
+
$document = DidDocument::fromArray([
59
+
'id' => 'did:plc:abc123',
60
+
'alsoKnownAs' => ['at://user.bsky.social'],
61
+
]);
62
+
63
+
$this->assertSame('user.bsky.social', $document->getHandle());
64
+
}
65
+
66
+
public function test_it_returns_null_when_no_handle(): void
67
+
{
68
+
$document = DidDocument::fromArray([
69
+
'id' => 'did:plc:abc123',
70
+
'alsoKnownAs' => [],
71
+
]);
72
+
73
+
$this->assertNull($document->getHandle());
74
+
}
75
+
76
+
public function test_it_can_convert_to_array(): void
77
+
{
78
+
$data = [
79
+
'id' => 'did:plc:abc123',
80
+
'alsoKnownAs' => ['at://user.bsky.social'],
81
+
];
82
+
83
+
$document = DidDocument::fromArray($data);
84
+
85
+
$this->assertSame($data, $document->toArray());
86
+
}
87
+
}
+82
tests/Unit/DidResolverManagerTest.php
+82
tests/Unit/DidResolverManagerTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Tests\Unit;
4
+
5
+
use PHPUnit\Framework\TestCase;
6
+
use SocialDept\Beacon\Contracts\DidResolver;
7
+
use SocialDept\Beacon\Data\DidDocument;
8
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
9
+
use SocialDept\Beacon\Resolvers\DidResolverManager;
10
+
11
+
class DidResolverManagerTest extends TestCase
12
+
{
13
+
public function test_it_supports_plc_method_by_default(): void
14
+
{
15
+
$manager = new DidResolverManager();
16
+
17
+
$this->assertTrue($manager->supports('plc'));
18
+
}
19
+
20
+
public function test_it_supports_web_method_by_default(): void
21
+
{
22
+
$manager = new DidResolverManager();
23
+
24
+
$this->assertTrue($manager->supports('web'));
25
+
}
26
+
27
+
public function test_it_does_not_support_unknown_methods(): void
28
+
{
29
+
$manager = new DidResolverManager();
30
+
31
+
$this->assertFalse($manager->supports('unknown'));
32
+
}
33
+
34
+
public function test_it_can_register_custom_resolver(): void
35
+
{
36
+
$manager = new DidResolverManager();
37
+
38
+
$customResolver = $this->createMock(DidResolver::class);
39
+
$customResolver->method('supports')->willReturn(true);
40
+
41
+
$manager->register('custom', $customResolver);
42
+
43
+
$this->assertTrue($manager->supports('custom'));
44
+
}
45
+
46
+
public function test_it_throws_exception_for_unsupported_method(): void
47
+
{
48
+
$this->expectException(DidResolutionException::class);
49
+
$this->expectExceptionMessage('Unsupported DID method: unknown');
50
+
51
+
$manager = new DidResolverManager();
52
+
$manager->resolve('did:unknown:abc123');
53
+
}
54
+
55
+
public function test_it_throws_exception_for_invalid_did_format(): void
56
+
{
57
+
$this->expectException(DidResolutionException::class);
58
+
$this->expectExceptionMessage('Invalid DID format');
59
+
60
+
$manager = new DidResolverManager();
61
+
$manager->resolve('not-a-did');
62
+
}
63
+
64
+
public function test_it_delegates_to_registered_resolver(): void
65
+
{
66
+
$manager = new DidResolverManager();
67
+
68
+
$mockDocument = DidDocument::fromArray(['id' => 'did:custom:abc123']);
69
+
70
+
$customResolver = $this->createMock(DidResolver::class);
71
+
$customResolver->expects($this->once())
72
+
->method('resolve')
73
+
->with('did:custom:abc123')
74
+
->willReturn($mockDocument);
75
+
76
+
$manager->register('custom', $customResolver);
77
+
78
+
$result = $manager->resolve('did:custom:abc123');
79
+
80
+
$this->assertSame($mockDocument, $result);
81
+
}
82
+
}
+62
tests/Unit/IdentityTest.php
+62
tests/Unit/IdentityTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Tests\Unit;
4
+
5
+
use PHPUnit\Framework\TestCase;
6
+
use SocialDept\Beacon\Support\Identity;
7
+
8
+
class IdentityTest extends TestCase
9
+
{
10
+
public function test_it_validates_handles(): void
11
+
{
12
+
$this->assertTrue(Identity::isHandle('alice.bsky.social'));
13
+
$this->assertTrue(Identity::isHandle('user.test'));
14
+
$this->assertTrue(Identity::isHandle('my-handle.example.com'));
15
+
$this->assertTrue(Identity::isHandle('a.b.c.d.example.com'));
16
+
17
+
$this->assertFalse(Identity::isHandle(''));
18
+
$this->assertFalse(Identity::isHandle(null));
19
+
$this->assertFalse(Identity::isHandle('invalid'));
20
+
$this->assertFalse(Identity::isHandle('no-tld'));
21
+
$this->assertFalse(Identity::isHandle('.invalid'));
22
+
$this->assertFalse(Identity::isHandle('invalid.'));
23
+
}
24
+
25
+
public function test_it_validates_dids(): void
26
+
{
27
+
$this->assertTrue(Identity::isDid('did:plc:ewvi7nxzyoun6zhxrhs64oiz'));
28
+
$this->assertTrue(Identity::isDid('did:web:example.com'));
29
+
$this->assertTrue(Identity::isDid('did:plc:abc123'));
30
+
$this->assertTrue(Identity::isDid('did:web:alice.test'));
31
+
32
+
$this->assertFalse(Identity::isDid(''));
33
+
$this->assertFalse(Identity::isDid(null));
34
+
$this->assertFalse(Identity::isDid('invalid'));
35
+
$this->assertFalse(Identity::isDid('did:'));
36
+
$this->assertFalse(Identity::isDid('did:plc:'));
37
+
$this->assertFalse(Identity::isDid('not-a-did'));
38
+
}
39
+
40
+
public function test_it_extracts_did_method(): void
41
+
{
42
+
$this->assertSame('plc', Identity::extractDidMethod('did:plc:ewvi7nxzyoun6zhxrhs64oiz'));
43
+
$this->assertSame('web', Identity::extractDidMethod('did:web:example.com'));
44
+
45
+
$this->assertNull(Identity::extractDidMethod('invalid'));
46
+
$this->assertNull(Identity::extractDidMethod(''));
47
+
}
48
+
49
+
public function test_it_checks_plc_did(): void
50
+
{
51
+
$this->assertTrue(Identity::isPlcDid('did:plc:ewvi7nxzyoun6zhxrhs64oiz'));
52
+
$this->assertFalse(Identity::isPlcDid('did:web:example.com'));
53
+
$this->assertFalse(Identity::isPlcDid('invalid'));
54
+
}
55
+
56
+
public function test_it_checks_web_did(): void
57
+
{
58
+
$this->assertTrue(Identity::isWebDid('did:web:example.com'));
59
+
$this->assertFalse(Identity::isWebDid('did:plc:ewvi7nxzyoun6zhxrhs64oiz'));
60
+
$this->assertFalse(Identity::isWebDid('invalid'));
61
+
}
62
+
}
+46
tests/Unit/PlcDidResolverTest.php
+46
tests/Unit/PlcDidResolverTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Tests\Unit;
4
+
5
+
use PHPUnit\Framework\TestCase;
6
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
7
+
use SocialDept\Beacon\Resolvers\PlcDidResolver;
8
+
9
+
class PlcDidResolverTest extends TestCase
10
+
{
11
+
public function test_it_supports_plc_method(): void
12
+
{
13
+
$resolver = new PlcDidResolver();
14
+
15
+
$this->assertTrue($resolver->supports('plc'));
16
+
$this->assertFalse($resolver->supports('web'));
17
+
$this->assertFalse($resolver->supports('unknown'));
18
+
}
19
+
20
+
public function test_it_throws_exception_for_invalid_did_format(): void
21
+
{
22
+
$this->expectException(DidResolutionException::class);
23
+
$this->expectExceptionMessage('Invalid DID format');
24
+
25
+
$resolver = new PlcDidResolver();
26
+
$resolver->resolve('invalid-did');
27
+
}
28
+
29
+
public function test_it_throws_exception_for_incomplete_did(): void
30
+
{
31
+
$this->expectException(DidResolutionException::class);
32
+
$this->expectExceptionMessage('Invalid DID format');
33
+
34
+
$resolver = new PlcDidResolver();
35
+
$resolver->resolve('did:plc');
36
+
}
37
+
38
+
public function test_it_throws_exception_for_unsupported_method(): void
39
+
{
40
+
$this->expectException(DidResolutionException::class);
41
+
$this->expectExceptionMessage('Unsupported DID method: web');
42
+
43
+
$resolver = new PlcDidResolver();
44
+
$resolver->resolve('did:web:example.com');
45
+
}
46
+
}
+37
tests/Unit/WebDidResolverTest.php
+37
tests/Unit/WebDidResolverTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Beacon\Tests\Unit;
4
+
5
+
use PHPUnit\Framework\TestCase;
6
+
use SocialDept\Beacon\Exceptions\DidResolutionException;
7
+
use SocialDept\Beacon\Resolvers\WebDidResolver;
8
+
9
+
class WebDidResolverTest extends TestCase
10
+
{
11
+
public function test_it_supports_web_method(): void
12
+
{
13
+
$resolver = new WebDidResolver();
14
+
15
+
$this->assertTrue($resolver->supports('web'));
16
+
$this->assertFalse($resolver->supports('plc'));
17
+
$this->assertFalse($resolver->supports('unknown'));
18
+
}
19
+
20
+
public function test_it_throws_exception_for_invalid_did_format(): void
21
+
{
22
+
$this->expectException(DidResolutionException::class);
23
+
$this->expectExceptionMessage('Invalid DID format');
24
+
25
+
$resolver = new WebDidResolver();
26
+
$resolver->resolve('invalid-did');
27
+
}
28
+
29
+
public function test_it_throws_exception_for_unsupported_method(): void
30
+
{
31
+
$this->expectException(DidResolutionException::class);
32
+
$this->expectExceptionMessage('Unsupported DID method: plc');
33
+
34
+
$resolver = new WebDidResolver();
35
+
$resolver->resolve('did:plc:abc123');
36
+
}
37
+
}