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.

Align handle conflict error semantics

alice 70188765 bc8691d8

+50 -4
+2 -1
docs/TEST_AUDIT.md
··· 13 13 The current baseline for saying "the audited suite is green" is: 14 14 15 15 - `prove -lr t` 16 - - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2926` 16 + - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2943` 17 17 - `prove -lv t/server-auth.t` 18 18 - `perl -c script/differential-validate` 19 19 - `PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential.t` ··· 52 52 - `com.atproto.server.requestEmailConfirmation`, `requestEmailUpdate`, and `requestAccountDelete` should reject accounts with no stored email using the official `400 InvalidRequest` / `account does not have an email address` shape, and `updateEmail` should reject unsupported syntax with the official `This email address is not supported, please use a different email.` message. 53 53 - perlsky intentionally still allows test-friendly no-email local accounts, but once an email is supplied `com.atproto.server.createAccount` now follows the same unsupported-syntax rejection shape as `updateEmail` and the official runtime. 54 54 - `com.atproto.server.createAccount` must not turn duplicate-email requests into a `500`; it now follows the official client-visible `400 InvalidRequest` / `Email already taken: ...` shape instead. 55 + - Local handle-conflict flows now use the reference runtime’s client-visible `400 InvalidRequest` / `Handle already taken: ...` shape on `createAccount`, `com.atproto.identity.updateHandle`, and `com.atproto.admin.updateAccountHandle`, instead of the older local `HandleNotAvailable` variant. 55 56 - `app.bsky.actor.putPreferences` and `app.bsky.notification.putPreferencesV2` now have explicit shape validation plus focused regression coverage, turning an earlier hardening concern into a pinned contract. 56 57 - `com.atproto.identity.resolveHandle` should reject malformed handles with `400 InvalidRequest`, not quietly treat them as misses or return a local `InvalidHandle` variant. 57 58 - `com.atproto.identity.resolveHandle` should treat well-formed but unresolved handles as `400 InvalidRequest` with `Unable to resolve handle`, matching the official runtime instead of returning a local `404 HandleNotFound`.
+1 -1
lib/ATProto/PDS/API/Admin.pm
··· 145 145 unless defined $resolved_did && lc($resolved_did) eq lc($account->{did}); 146 146 } 147 147 my $existing = $c->store->get_account_by_handle($handle); 148 - xrpc_error(400, 'HandleNotAvailable', 'That handle is already registered') 148 + xrpc_error(400, 'InvalidRequest', "Handle already taken: $handle") 149 149 if $existing && ($existing->{did} // q()) ne $account->{did}; 150 150 my $updated = $c->store->update_account( 151 151 $account->{did},
+1 -1
lib/ATProto/PDS/API/Misc.pm
··· 180 180 unless defined $resolved_did && lc($resolved_did) eq lc($account->{did}); 181 181 } 182 182 my $existing = $c->store->get_account_by_handle($handle); 183 - xrpc_error(400, 'HandleNotAvailable', 'That handle is already registered') 183 + xrpc_error(400, 'InvalidRequest', "Handle already taken: $handle") 184 184 if $existing && ($existing->{did} // q()) ne $account->{did}; 185 185 xrpc_error(400, 'HandleNotAvailable', 'That handle is reserved') 186 186 if $c->store->get_reserved_handle($handle);
+1 -1
lib/ATProto/PDS/API/Server.pm
··· 67 67 my $domain = $c->config_value('service_handle_domain', 'localhost'); 68 68 my $handle = normalize_handle($body->{handle}, $domain); 69 69 xrpc_error(400, 'InvalidHandle', 'Requested handle is invalid') unless defined $handle; 70 - xrpc_error(400, 'HandleNotAvailable', 'That handle is already registered') 70 + xrpc_error(400, 'InvalidRequest', "Handle already taken: $handle") 71 71 if $c->store->get_account_by_handle($handle); 72 72 xrpc_error(400, 'HandleNotAvailable', 'That handle is reserved') 73 73 if $c->store->get_reserved_handle($handle);
+6
script/differential-validate
··· 577 577 email => uc($server{$name}{email}), 578 578 password => 'hunter22', 579 579 }); 580 + my $duplicate_handle_create = post_json($server{$name}{origin}, 'com.atproto.server.createAccount', { 581 + handle => uc($server{$name}{handle}), 582 + email => "duphandle-$name-" . substr(random_hex(3), 0, 6) . '@test.com', 583 + password => 'hunter22', 584 + }); 580 585 my $too_long_session = post_json($server{$name}{origin}, 'com.atproto.server.createSession', { 581 586 identifier => $server{$name}{handle}, 582 587 password => ('x' x 513), ··· 589 594 create_too_long => normalize_xrpc_error($too_long_create), 590 595 create_bad_email => normalize_xrpc_error($invalid_email_create), 591 596 create_dup_email => normalize_xrpc_error($duplicate_email_create), 597 + create_dup_handle => normalize_xrpc_error($duplicate_handle_create), 592 598 session_too_long => normalize_xrpc_error($too_long_session), 593 599 reset_uppercase => { 594 600 status => $password_reset_upper->code // 0,
+8
t/app.t
··· 106 106 ->json_is('/error' => 'InvalidRequest') 107 107 ->json_is('/message' => 'Email already taken: ALICE@example.test'); 108 108 109 + $fresh->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 110 + handle => 'ALICE.localhost', 111 + email => 'another@example.test', 112 + password => 'hunter22', 113 + })->status_is(400) 114 + ->json_is('/error' => 'InvalidRequest') 115 + ->json_is('/message' => 'Handle already taken: alice.localhost'); 116 + 109 117 $fresh->get_ok("/xrpc/com.atproto.identity.resolveHandle?handle=alice.localhost") 110 118 ->status_is(200) 111 119 ->json_is('/did' => $user_did);
+15
t/external-handle-update.t
··· 47 47 my $did = $session->{did}; 48 48 my $access = $session->{accessJwt}; 49 49 50 + $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 51 + handle => 'bob', 52 + email => 'bob@example.com', 53 + password => 'password123', 54 + })->status_is(200) 55 + ->json_is('/handle' => 'bob.localhost'); 56 + 57 + $t->post_ok('/xrpc/com.atproto.identity.updateHandle' => { 58 + Authorization => "Bearer $access", 59 + } => json => { 60 + handle => 'bob.localhost', 61 + })->status_is(400) 62 + ->json_is('/error' => 'InvalidRequest') 63 + ->json_is('/message' => 'Handle already taken: bob.localhost'); 64 + 50 65 { 51 66 no warnings 'redefine'; 52 67 local *ATProto::PDS::Identity::_resolve_handle_dns = sub {
+16
t/uncovered-endpoints.t
··· 52 52 my $did = $created->{did}; 53 53 my $access = $created->{accessJwt}; 54 54 55 + $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 56 + handle => 'bob.example.test', 57 + email => 'bob@example.test', 58 + password => 'hunter22', 59 + })->status_is(200); 60 + my $bob = $t->tx->res->json; 61 + 55 62 $t->post_ok('/xrpc/com.atproto.server.reserveSigningKey' => json => {}) 56 63 ->status_is(200) 57 64 ->json_like('/signingKey' => qr/\Adid:key:/); ··· 111 118 my ($config, $handle) = @_; 112 119 return $handle eq 'alice.external.test' ? $did : undef; 113 120 }; 121 + 122 + $t->post_ok('/xrpc/com.atproto.admin.updateAccountHandle' => { 123 + Authorization => $admin_auth, 124 + } => json => { 125 + did => $did, 126 + handle => $bob->{handle}, 127 + })->status_is(400) 128 + ->json_is('/error' => 'InvalidRequest') 129 + ->json_is('/message' => 'Handle already taken: bob.example.test'); 114 130 115 131 $t->post_ok('/xrpc/com.atproto.admin.updateAccountHandle' => { 116 132 Authorization => $admin_auth,