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 remaining void procedure responses

alice 5a151222 2b1dee8c

+41 -36
+2
docs/TEST_AUDIT.md
··· 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 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`. 82 82 - PLC identity success procedures now also match the official runtime’s empty-body `200` surface: `com.atproto.identity.requestPlcOperationSignature`, `com.atproto.identity.updateHandle`, and `com.atproto.identity.submitPlcOperation`. 83 + - Remaining lexicon-void admin/sync/temp procedures now also stay on the empty-body `200` surface instead of returning local JSON `{}` objects, including `com.atproto.admin.updateAccountHandle`, `updateAccountEmail`, `disableInviteCodes`, `updateAccountSigningKey`, `com.atproto.sync.requestCrawl`, `notifyOfUpdate`, `com.atproto.temp.requestPhoneVerification`, `revokeAccountCredentials`, and `com.atproto.repo.importRepo`. 84 + - `app.bsky.actor.putPreferences` and `/oauth/revoke` now also keep their void-style empty-body `200` surface instead of returning local JSON `{}` objects. 83 85 - 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.` 84 86 - 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. 85 87 - `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.
+6 -7
lib/ATProto/PDS/API/Admin.pm
··· 9 9 use JSON::PP (); 10 10 11 11 use ATProto::PDS::API::Helpers qw(account_view admin_account_view find_account invite_code_view require_admin subject_key update_account_email); 12 - use ATProto::PDS::API::Util qw(flatten_params xrpc_error); 12 + use ATProto::PDS::API::Util qw(flatten_params render_empty_success xrpc_error); 13 13 use ATProto::PDS::Auth::Password qw(hash_password); 14 14 use ATProto::PDS::Constants qw(EVENT_TYPE_IDENTITY); 15 15 use ATProto::PDS::Crypto::Secp256k1 qw(signing_did_to_public_key_multibase); ··· 153 153 did_doc => account_did_doc($c->app->settings, { %$account, handle => $handle }), 154 154 ); 155 155 _append_identity_event($c, $updated); 156 - return {}; 156 + return render_empty_success($c); 157 157 }); 158 158 159 159 $registry->register('com.atproto.admin.updateAccountPassword', sub ($c, $endpoint) { ··· 182 182 xrpc_error(400, 'InvalidRequest', 'Account does not exist: ' . ($body->{account} // q())) 183 183 unless $account; 184 184 update_account_email($c, $account->{did}, $body->{email}); 185 - return {}; 185 + return render_empty_success($c); 186 186 }); 187 187 188 188 $registry->register('com.atproto.admin.deleteAccount', sub ($c, $endpoint) { ··· 221 221 codes => $body->{codes}, 222 222 accounts => $body->{accounts}, 223 223 ); 224 - return {}; 224 + return render_empty_success($c); 225 225 }); 226 226 227 227 $registry->register('com.atproto.admin.getInviteCodes', sub ($c, $endpoint) { ··· 282 282 did_doc => account_did_doc($c->app->settings, $updated), 283 283 ); 284 284 _append_identity_event($c, $stored); 285 - return {}; 285 + return render_empty_success($c); 286 286 }); 287 287 } 288 288 ··· 405 405 invites_disabled => $disabled ? 1 : 0, 406 406 invite_note => undef, 407 407 ); 408 - $c->render(data => q()); 409 - return; 408 + return render_empty_success($c); 410 409 } 411 410 412 411 sub _append_account_event ($c, $did, $account, $payload) {
+7 -12
lib/ATProto/PDS/API/Misc.pm
··· 10 10 11 11 use ATProto::PDS::API::Helpers qw(find_account issue_account_action_token require_admin subject_key); 12 12 use ATProto::PDS::API::Server qw(require_auth); 13 - use ATProto::PDS::API::Util qw(flatten_params iso8601 pump_event_subscription subscription_start_seq xrpc_error); 13 + use ATProto::PDS::API::Util qw(flatten_params iso8601 pump_event_subscription render_empty_success subscription_start_seq xrpc_error); 14 14 use ATProto::PDS::Auth::OAuth qw(oauth_scope_has_atproto); 15 15 use ATProto::PDS::Auth::Password qw(hash_password random_hex); 16 16 use ATProto::PDS::Constants qw( ··· 81 81 subject => 'PLC update requested', 82 82 content => sub ($token) { "Use token $token->{token} to authorize your PLC operation." }, 83 83 ); 84 - return _render_empty_success($c); 84 + return render_empty_success($c); 85 85 }); 86 86 87 87 $registry->register('com.atproto.identity.signPlcOperation', sub ($c, $endpoint) { ··· 154 154 my $did_doc = refresh_plc_did_doc($c->app->settings, $account->{did}); 155 155 $account = $c->store->update_account($account->{did}, did_doc => $did_doc); 156 156 _append_identity_event($c, $account); 157 - return _render_empty_success($c); 157 + return render_empty_success($c); 158 158 }); 159 159 160 160 $registry->register('com.atproto.identity.updateHandle', sub ($c, $endpoint) { ··· 192 192 } 193 193 my $updated = $c->store->update_account($account->{did}, %changes); 194 194 _append_identity_event($c, $updated); 195 - return _render_empty_success($c); 195 + return render_empty_success($c); 196 196 }); 197 197 198 198 $registry->register('com.atproto.lexicon.resolveLexicon', sub ($c, $endpoint) { ··· 287 287 my $handle = normalize_handle($body->{handle}, $domain); 288 288 xrpc_error(400, 'InvalidHandle', 'Requested handle is invalid') unless defined $handle; 289 289 $c->store->reserve_handle($handle); 290 - return {}; 290 + return render_empty_success($c); 291 291 }); 292 292 293 293 $registry->register('com.atproto.temp.checkSignupQueue', sub ($c, $endpoint) { ··· 307 307 }); 308 308 309 309 $registry->register('com.atproto.temp.requestPhoneVerification', sub ($c, $endpoint) { 310 - return {}; 310 + return render_empty_success($c); 311 311 }); 312 312 313 313 $registry->register('com.atproto.temp.revokeAccountCredentials', sub ($c, $endpoint) { ··· 325 325 $c->store->revoke_sessions_by_did($account->{did}); 326 326 $c->store->revoke_app_passwords_by_did($account->{did}); 327 327 }); 328 - return {}; 328 + return render_empty_success($c); 329 329 }); 330 330 } 331 331 ··· 362 362 xrpc_error(400, 'InvalidToken', 'Bad token scope') 363 363 unless (($claims->{scope} // TOKEN_AUD_ACCESS) eq TOKEN_AUD_ACCESS); 364 364 return 1; 365 - } 366 - 367 - sub _render_empty_success ($c) { 368 - $c->render(data => q()); 369 - return; 370 365 } 371 366 372 367 sub _valid_plc_operation ($operation) {
+2 -2
lib/ATProto/PDS/API/Repo.pm
··· 12 12 use Mojo::URL; 13 13 14 14 use ATProto::PDS::API::Server qw(require_access_or_service_auth require_auth); 15 - use ATProto::PDS::API::Util qw(blob_ref resolve_repo xrpc_error); 15 + use ATProto::PDS::API::Util qw(blob_ref render_empty_success resolve_repo xrpc_error); 16 16 use ATProto::PDS::Auth::OAuth qw( 17 17 oauth_required_permission_scope 18 18 oauth_scope_allows_permission ··· 201 201 die $err if ref($err) eq 'HASH'; 202 202 xrpc_error(400, 'InvalidRequest', 'Repo import CAR was invalid'); 203 203 }; 204 - return {}; 204 + return render_empty_success($c); 205 205 }); 206 206 } 207 207
+3 -3
lib/ATProto/PDS/API/Sync.pm
··· 9 9 use JSON::PP (); 10 10 11 11 use ATProto::PDS::EventStream qw(encode_message_frame); 12 - use ATProto::PDS::API::Util qw(flatten_params iso8601 pump_event_subscription resolve_did_account subscription_start_seq xrpc_error); 12 + use ATProto::PDS::API::Util qw(flatten_params iso8601 pump_event_subscription render_empty_success resolve_did_account subscription_start_seq xrpc_error); 13 13 use ATProto::PDS::Constants qw( 14 14 EVENT_TYPE_ACCOUNT 15 15 EVENT_TYPE_COMMIT ··· 178 178 last_seq => $c->store->latest_event_seq, 179 179 status => { status => 'active' }, 180 180 ); 181 - return {}; 181 + return render_empty_success($c); 182 182 }); 183 183 184 184 $registry->register('com.atproto.sync.notifyOfUpdate', sub ($c, $endpoint) { ··· 189 189 last_seq => $c->store->latest_event_seq, 190 190 status => { status => 'active' }, 191 191 ); 192 - return {}; 192 + return render_empty_success($c); 193 193 }); 194 194 195 195 $registry->register('com.atproto.sync.listHosts', sub ($c, $endpoint) {
+6
lib/ATProto/PDS/API/Util.pm
··· 17 17 flatten_params 18 18 iso8601 19 19 pump_event_subscription 20 + render_empty_success 20 21 resolve_did_account 21 22 resolve_repo 22 23 subscription_start_seq ··· 50 51 $gmt[1], 51 52 $gmt[0], 52 53 ); 54 + } 55 + 56 + sub render_empty_success ($c) { 57 + $c->render(data => q()); 58 + return; 53 59 } 54 60 55 61 sub resolve_did_account ($c, $did) {
+2 -1
t/extended-api.t
··· 319 319 320 320 $t->post_ok('/xrpc/com.atproto.sync.requestCrawl' => json => { 321 321 hostname => 'relay.example.test', 322 - })->status_is(200); 322 + })->status_is(200) 323 + ->content_is(q()); 323 324 324 325 $t->get_ok('/xrpc/com.atproto.sync.listHosts') 325 326 ->status_is(200)
+2 -1
t/import-repo.t
··· 101 101 $t->post_ok('/xrpc/com.atproto.repo.importRepo' => { 102 102 Authorization => "Bearer $access", 103 103 'Content-Type' => 'application/vnd.ipld.car', 104 - } => $snapshot)->status_is(200); 104 + } => $snapshot)->status_is(200) 105 + ->content_is(q()); 105 106 106 107 $t->get_ok('/xrpc/com.atproto.repo.listRecords' => form => { 107 108 repo => $did,
+11 -10
t/uncovered-endpoints.t
··· 134 134 did => $did, 135 135 handle => 'alice.external.test', 136 136 })->status_is(200) 137 - ->json_is({}); 137 + ->content_is(q()); 138 138 } 139 139 140 140 my $handle_event = $app->store->list_events_from($before_admin_handle_seq + 1, limit => 1)->[0]; ··· 172 172 account => $did, 173 173 email => 'Alice+Admin@Example.Test', 174 174 })->status_is(200) 175 - ->json_is({}); 175 + ->content_is(q()); 176 176 177 177 $account = $app->store->get_account_by_did($did); 178 178 is($account->{email}, 'alice+admin@example.test', 'admin.updateAccountEmail normalizes email'); ··· 249 249 did => $did, 250 250 signingKey => $reserved_signing_key, 251 251 })->status_is(200) 252 - ->json_is({}); 252 + ->content_is(q()); 253 253 254 254 my $signing_event = $app->store->list_events_from($handle_event->{seq} + 1, limit => 1)->[0]; 255 255 is($signing_event->{type}, 'identity', 'admin.updateAccountSigningKey appends an identity event'); ··· 354 354 } => json => { 355 355 codes => [$admin_codes[0]], 356 356 })->status_is(200) 357 - ->json_is({}); 357 + ->content_is(q()); 358 358 359 359 ok($app->store->get_invite_code($admin_codes[0])->{disabled}, 'disableInviteCodes marks the requested code disabled'); 360 360 ··· 436 436 } => json => { 437 437 codes => [$user_code], 438 438 })->status_is(200) 439 - ->json_is({}); 439 + ->content_is(q()); 440 440 441 441 my ($disabled_row) = grep { $_->{code} eq $user_code } @{ $app->store->list_invite_codes_for_account($did) || [] }; 442 442 ok($disabled_row && $disabled_row->{disabled}, 'disableInviteCodes disables the requested invite code'); ··· 451 451 $t->post_ok('/xrpc/com.atproto.sync.notifyOfUpdate' => json => { 452 452 hostname => 'crawler.example.test', 453 453 })->status_is(200) 454 - ->json_is({}); 454 + ->content_is(q()); 455 455 456 456 $t->get_ok('/xrpc/com.atproto.sync.getHostStatus' => form => { 457 457 hostname => 'crawler.example.test', ··· 561 561 ->status_is(400) 562 562 ->json_is('/error' => 'InvalidScopeReference'); 563 563 564 - $t->post_ok('/xrpc/com.atproto.temp.requestPhoneVerification' => json => {}) 565 - ->status_is(200) 566 - ->json_is({}); 564 + $t->post_ok('/xrpc/com.atproto.temp.requestPhoneVerification' => json => { 565 + phoneNumber => '+441234567890', 566 + })->status_is(200) 567 + ->content_is(q()); 567 568 568 569 $t->post_ok('/xrpc/com.atproto.temp.revokeAccountCredentials' => json => { 569 570 account => $did, ··· 575 576 } => json => { 576 577 account => $did, 577 578 })->status_is(200) 578 - ->json_is({}); 579 + ->content_is(q()); 579 580 580 581 $t->get_ok('/xrpc/com.atproto.server.getSession' => { 581 582 Authorization => "Bearer $access",