perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Serve atproto handle well-known documents

alice 91878674 f6adb523

+39
+12
docs/DEPLOYMENT.md
··· 80 80 - `hostname`: the host relays should crawl 81 81 - `service_handle_domain`: the suffix used for local handles 82 82 - If you want users like `alice.pds.example.com`, set `service_handle_domain` to `pds.example.com`, not `example.com`. 83 + - Public handle resolution for `alice.pds.example.com` also requires wildcard DNS for `*.pds.example.com` and a reverse proxy/TLS setup that will answer those subdomains. 83 84 - `invite_code_required`: if true, `createAccount` requires a valid invite code 84 85 - `account_did_method`: set to `did:plc` if you want PLC-backed user DIDs 85 86 - `plc_rotation_private_key_hex`: required for `did:plc` account creation ··· 150 151 151 152 Expose `perlsky` through a TLS-capable reverse proxy to `127.0.0.1:7755`. 152 153 154 + If `service_handle_domain` is a subdomain suffix such as `pds.example.com`, your proxy must answer both: 155 + 156 + - `pds.example.com` 157 + - `*.pds.example.com` 158 + 159 + That is what allows external PDSes to resolve `https://alice.pds.example.com/.well-known/atproto-did`. 160 + 153 161 A minimal Caddy site looks like: 154 162 155 163 ```caddy ··· 158 166 reverse_proxy 127.0.0.1:7755 159 167 } 160 168 ``` 169 + 170 + For public user handles you also need a matching wildcard-capable site or on-demand TLS path for `*.pds.example.com`. 161 171 162 172 A minimal nginx site looks like: 163 173 ··· 193 203 curl https://pds.example.com/_health 194 204 curl https://pds.example.com/.well-known/did.json 195 205 curl https://pds.example.com/xrpc/com.atproto.server.describeServer 206 + curl --resolve alice.pds.example.com:443:SERVER_IP https://alice.pds.example.com/.well-known/atproto-did 196 207 ``` 197 208 198 209 For browser-hosted clients such as `https://bsky.app`, `perlsky` also answers CORS preflight requests on XRPC routes. A quick manual probe looks like: ··· 208 219 - a healthy `_health` response 209 220 - a `did:web:pds.example.com` DID document 210 221 - `describeServer.availableUserDomains` matching `service_handle_domain` 222 + - a per-handle `/.well-known/atproto-did` response returning the account DID when queried on the handle host 211 223 212 224 ## First Account 213 225
+11
lib/ATProto/PDS.pm
··· 162 162 }); 163 163 }); 164 164 165 + $routes->get('/.well-known/atproto-did')->to(cb => sub ($c) { 166 + my $host = lc($c->req->url->to_abs->host // ($c->req->headers->host // q())); 167 + $host =~ s/:\d+\z//; 168 + my $account = $c->store->get_account_by_handle($host); 169 + return $c->render(status => 404, text => 'handle not found') unless $account; 170 + 171 + $c->res->headers->content_type('text/plain; charset=utf-8'); 172 + $c->render(data => $account->{did}); 173 + }); 174 + 165 175 $routes->get('/users/:account_id/did.json')->to(cb => sub ($c) { 166 176 my $match = $c->store->get_account_by_id($c->stash('account_id')); 167 177 return $c->render(status => 404, json => { error => 'DidNotFound' }) unless $match; ··· 185 195 my $text = ref($path) ? $path->to_string : ($path // q()); 186 196 return 1 if $text =~ m{\A/xrpc(?:/|\z)}; 187 197 return 1 if $text eq '/.well-known/did.json'; 198 + return 1 if $text eq '/.well-known/atproto-did'; 188 199 return 0; 189 200 } 190 201
+16
t/app-routes.t
··· 39 39 ->json_is('/availableUserDomains/0' => 'localhost') 40 40 ->json_like('/did' => qr/\Adid:web:/); 41 41 42 + $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 43 + handle => 'routeprobe.localhost', 44 + email => 'routeprobe@example.com', 45 + password => 'hunter42', 46 + })->status_is(200); 47 + 48 + my $routeprobe_did = $t->tx->res->json->{did}; 49 + 50 + $t->get_ok('/.well-known/atproto-did' => { Host => 'routeprobe.localhost' }) 51 + ->status_is(200) 52 + ->content_type_like(qr{text/plain}) 53 + ->content_is($routeprobe_did); 54 + 55 + $t->get_ok('/.well-known/atproto-did' => { Host => 'missing.localhost' }) 56 + ->status_is(404); 57 + 42 58 $t->post_ok('/xrpc/com.atproto.repo.createRecord' => json => {}) 43 59 ->status_is(404) 44 60 ->json_is('/error' => 'RepoNotFound');