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 server lifecycle empty responses

alice 10be6e03 194666bf

+31 -20
+1
docs/TEST_AUDIT.md
··· 78 78 - `com.atproto.repo.createRecord` follows the reference runtime by ignoring a stray `swapRecord` field, and direct reference coverage now pins `putRecord` / `deleteRecord` `swapCommit` and `swapRecord` mismatch semantics explicitly. 79 79 - App-password sessions follow the official runtime more closely than the older local assumptions did: access-token scopes use the `com.atproto.appPass` / `com.atproto.appPassPrivileged` names, standard app-password sessions may list app passwords, privileged-only `getServiceAuth` failures report `InvalidRequest`, and revoked refresh tokens on `refreshSession` fail with `400 ExpiredToken`. 80 80 - `com.atproto.server.requestPasswordReset` and `com.atproto.server.deleteAccount` now follow the reference form-token flow, with focused regression coverage for missing-account and bearerless deletion semantics. 81 + - Core no-output account lifecycle procedures now match the official runtime’s empty-body `200` surface instead of returning local JSON `{}` objects: `deleteSession`, `revokeAppPassword`, `deactivateAccount`, `activateAccount`, `requestPasswordReset`, `resetPassword`, `requestEmailConfirmation`, `confirmEmail`, `updateEmail`, `requestAccountDelete`, and `deleteAccount`. 81 82 - Password-bearing account endpoints need the same bounded-length behavior as the official runtime: `createAccount` rejects passwords longer than 256 characters, `createSession` rejects passwords longer than 512 characters with the reset hint, and `resetPassword` / `deleteAccount` reject overlong password inputs with `Invalid password length.` 82 83 - The executable reference harness now also pins those password-boundary semantics directly, including the official `AuthenticationRequired` error shape for overlong `createSession` requests and case-insensitive `requestPasswordReset` email lookup. 83 84 - `com.atproto.server.createAccount` with an explicit `did` must behave like an authenticated migration flow: require auth from that same DID, keep the existing DID document, and start the new account deactivated until activation catches the DID document up to the new PDS.
+16 -11
lib/ATProto/PDS/API/Server.pm
··· 287 287 my ($claims) = require_auth($c, audience => TOKEN_AUD_REFRESH, allow_refresh => 1); 288 288 my $session = $c->store->get_session($claims->{jti}); 289 289 $c->store->revoke_session($session->{id}) if $session; 290 - return {}; 290 + return _render_empty_success($c); 291 291 }); 292 292 293 293 $registry->register('com.atproto.server.checkAccountStatus', sub ($c, $endpoint) { ··· 381 381 next if defined $session->{revoked_at}; 382 382 $c->store->revoke_session($session->{id}); 383 383 } 384 - return {}; 384 + return _render_empty_success($c); 385 385 }); 386 386 387 387 $registry->register('com.atproto.server.deactivateAccount', sub ($c, $endpoint) { ··· 401 401 status => 'deactivated', 402 402 }, 403 403 ); 404 - return {}; 404 + return _render_empty_success($c); 405 405 }); 406 406 407 407 $registry->register('com.atproto.server.activateAccount', sub ($c, $endpoint) { ··· 443 443 }, 444 444 ) if defined $sync_car; 445 445 } 446 - return {}; 446 + return _render_empty_success($c); 447 447 }); 448 448 449 449 $registry->register('com.atproto.server.requestPasswordReset', sub ($c, $endpoint) { ··· 458 458 subject => 'perlsky password reset', 459 459 content => sub ($token) { "Use token $token->{token} to reset your password." }, 460 460 ); 461 - return {}; 461 + return _render_empty_success($c); 462 462 }); 463 463 464 464 $registry->register('com.atproto.server.resetPassword', sub ($c, $endpoint) { ··· 484 484 $c->store->revoke_app_passwords_by_did($account->{did}); 485 485 $c->store->consume_action_token($token->{token}); 486 486 }); 487 - return {}; 487 + return _render_empty_success($c); 488 488 }); 489 489 490 490 $registry->register('com.atproto.server.requestEmailConfirmation', sub ($c, $endpoint) { ··· 507 507 subject => 'perlsky email confirmation', 508 508 content => sub ($token) { "Use token $token->{token} to confirm your email address." }, 509 509 ); 510 - return {}; 510 + return _render_empty_success($c); 511 511 }); 512 512 513 513 $registry->register('com.atproto.server.confirmEmail', sub ($c, $endpoint) { ··· 539 539 $c->store->update_account($account->{did}, email_confirmed_at => time); 540 540 $c->store->consume_action_token($token->{token}); 541 541 }); 542 - return {}; 542 + return _render_empty_success($c); 543 543 }); 544 544 545 545 $registry->register('com.atproto.server.requestEmailUpdate', sub ($c, $endpoint) { ··· 593 593 $c->store->consume_action_token($token->{token}); 594 594 } 595 595 update_account_email($c, $account->{did}, $email); 596 - return {}; 596 + return _render_empty_success($c); 597 597 }); 598 598 599 599 $registry->register('com.atproto.server.requestAccountDelete', sub ($c, $endpoint) { ··· 612 612 subject => 'perlsky account deletion', 613 613 content => sub ($token) { "Use token $token->{token} to delete your account." }, 614 614 ); 615 - return {}; 615 + return _render_empty_success($c); 616 616 }); 617 617 618 618 $registry->register('com.atproto.server.deleteAccount', sub ($c, $endpoint) { ··· 650 650 }, 651 651 ); 652 652 }); 653 - return {}; 653 + return _render_empty_success($c); 654 654 }); 655 655 656 656 $registry->register('com.atproto.server.getServiceAuth', sub ($c, $endpoint) { ··· 1205 1205 1206 1206 sub _new_invite_code { 1207 1207 return 'perlsky-' . substr(random_hex(8), 0, 12); 1208 + } 1209 + 1210 + sub _render_empty_success ($c) { 1211 + $c->render(data => q()); 1212 + return; 1208 1213 } 1209 1214 1210 1215 1;
+6 -1
script/differential-validate
··· 694 694 reset_uppercase => { 695 695 status => $password_reset_upper->code // 0, 696 696 success => $password_reset_upper->is_success ? 1 : 0, 697 - empty_body => same_hash($password_reset_upper->json || {}, {}) ? 1 : 0, 697 + empty_body => (($password_reset_upper->body // q()) eq q()) ? 1 : 0, 698 698 }, 699 699 }; 700 700 } ··· 2873 2873 token_required => (($initial_update->json || {})->{tokenRequired} ? 1 : 0), 2874 2874 }, 2875 2875 request_status => $request_confirm->code // 0, 2876 + request_empty_body => (($request_confirm->body // q()) eq q()) ? 1 : 0, 2876 2877 token_present => $confirm_token && length($confirm_token->{token} // q()) ? 1 : 0, 2877 2878 wrong_email_error => { 2878 2879 status => $wrong_confirm->code // 0, ··· 2880 2881 message => ($wrong_confirm->json || {})->{message}, 2881 2882 }, 2882 2883 confirm_status => $confirm_ok->code // 0, 2884 + confirm_empty_body => (($confirm_ok->body // q()) eq q()) ? 1 : 0, 2883 2885 session_confirmed => ($session_after_confirm->json || {})->{emailConfirmed} ? 1 : 0, 2884 2886 }, 2885 2887 update => { ··· 2899 2901 renew_status => $renew_update->code // 0, 2900 2902 fresh_token_present => $fresh_update_token && length($fresh_update_token->{token} // q()) ? 1 : 0, 2901 2903 update_status => $update_ok->code // 0, 2904 + update_empty_body => (($update_ok->body // q()) eq q()) ? 1 : 0, 2902 2905 session_email_matches => (($session_after_update->json || {})->{email} // q()) eq $updated_email ? 1 : 0, 2903 2906 session_confirmed => ($session_after_update->json || {})->{emailConfirmed} ? 1 : 0, 2904 2907 }, 2905 2908 delete => { 2906 2909 request_status => $request_delete->code // 0, 2910 + request_empty_body => (($request_delete->body // q()) eq q()) ? 1 : 0, 2907 2911 token_present => $delete_token && length($delete_token->{token} // q()) ? 1 : 0, 2908 2912 expired_token_error => { 2909 2913 status => $expired_delete->code // 0, ··· 2917 2921 error => ($delete_ok->json || {})->{error}, 2918 2922 message => ($delete_ok->json || {})->{message}, 2919 2923 }, 2924 + delete_empty_body => (($delete_ok->body // q()) eq q()) ? 1 : 0, 2920 2925 post_delete_login => normalize_xrpc_error($post_delete_login), 2921 2926 }, 2922 2927 };
+2 -2
t/delete-account.t
··· 51 51 $t->post_ok('/xrpc/com.atproto.server.requestAccountDelete' => { 52 52 Authorization => "Bearer $access", 53 53 } => json => {})->status_is(200) 54 - ->json_is({}); 54 + ->content_is(q()); 55 55 56 56 my $token = $app->store->latest_action_token( 57 57 did => $did, ··· 88 88 password => 'hunter22', 89 89 token => $token->{token}, 90 90 })->status_is(200) 91 - ->json_is({}); 91 + ->content_is(q()); 92 92 93 93 ok(defined $app->store->get_account_by_did($did)->{deleted_at}, 'deleteAccount marks the account deleted'); 94 94
+2 -2
t/email-confirmation.t
··· 155 155 email => 'BOB@example.test', 156 156 token => $bob_token->{token}, 157 157 })->status_is(200) 158 - ->json_is({}); 158 + ->content_is(q()); 159 159 160 160 ok( 161 161 defined $app->store->get_account_by_did($bob->{did})->{email_confirmed_at}, ··· 197 197 email => 'carol@example.test', 198 198 token => $carol_token->{token}, 199 199 })->status_is(200) 200 - ->json_is({}); 200 + ->content_is(q()); 201 201 202 202 $bypass_t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 203 203 handle => 'noemail.example.test',
+1 -1
t/oauth-permissions.t
··· 118 118 my $email_manage = _oauth_tokens_for_scope($t, $did, 'atproto account:email?action=manage'); 119 119 $t->post_ok('/xrpc/com.atproto.server.requestEmailConfirmation' => _oauth_headers($email_manage->{access_token}, 'POST', $config->{base_url} . '/xrpc/com.atproto.server.requestEmailConfirmation') => json => {}) 120 120 ->status_is(200) 121 - ->json_is({}); 121 + ->content_is(q()); 122 122 $t->post_ok('/xrpc/com.atproto.server.requestEmailUpdate' => _oauth_headers($email_manage->{access_token}, 'POST', $config->{base_url} . '/xrpc/com.atproto.server.requestEmailUpdate') => json => {}) 123 123 ->status_is(200) 124 124 ->json_is('/tokenRequired' => JSON::PP::true);
+3 -3
t/password-reset.t
··· 53 53 $t->post_ok('/xrpc/com.atproto.server.requestPasswordReset' => json => { 54 54 email => 'alice@example.test', 55 55 })->status_is(200) 56 - ->json_is({}); 56 + ->content_is(q()); 57 57 58 58 my $token = $app->store->latest_action_token( 59 59 purpose => 'password_reset', ··· 64 64 token => $token->{token}, 65 65 password => 'new-hunter22', 66 66 })->status_is(200) 67 - ->json_is({}); 67 + ->content_is(q()); 68 68 69 69 $t->post_ok('/xrpc/com.atproto.server.requestPasswordReset' => json => { 70 70 email => 'ALICE@example.test', 71 71 })->status_is(200) 72 - ->json_is({}); 72 + ->content_is(q()); 73 73 74 74 $t->post_ok('/xrpc/com.atproto.server.resetPassword' => json => { 75 75 token => $token->{token},