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 admin email and delete semantics

alice 9bfa8f7b 09bf1c30

+69 -12
+2
docs/TEST_AUDIT.md
··· 58 58 - The executable differential harness now proves that handle-conflict shape directly for both user and admin handle-update flows, not just local regression tests. 59 59 - `com.atproto.server.createSession` invalid-credential failures now use the reference runtime’s `401 AuthenticationRequired` shape instead of the older local `AuthRequired` variant. 60 60 - `com.atproto.admin.sendEmail` now follows the reference runtime’s `400 InvalidRequest` / `Recipient not found` shape for a missing recipient instead of returning a local `404 AccountNotFound`. 61 + - `com.atproto.admin.updateAccountEmail` now follows the reference runtime’s `400 InvalidRequest` / `Account does not exist: ...` shape for a missing account identifier instead of a local `404 AccountNotFound`. 62 + - `com.atproto.admin.deleteAccount` is now reference-style idempotent for missing DIDs: it succeeds and emits the same deleted account event shape instead of failing locally with `404 AccountNotFound`. 61 63 - `com.atproto.admin.updateAccountPassword` follows the reference runtime’s looser admin policy: it rejects overlong passwords with `400 InvalidRequest` / `Invalid password length.`, but does not impose the normal user-facing minimum-length gate. 62 64 - `com.atproto.admin.disableAccountInvites` / `enableAccountInvites` now ignore the local `note` field so the visible account state matches the official runtime instead of carrying an extra stored `inviteNote`. 63 65 - `com.atproto.admin.getInviteCodes` now matches the official runtime on sort validation, always-emitted cursor behavior, total `available` counts, and newest-first `uses` ordering.
+21 -11
lib/ATProto/PDS/API/Admin.pm
··· 180 180 require_admin($c); 181 181 my $body = $c->req->json || {}; 182 182 my $account = find_account($c, $body->{account} // q()); 183 - xrpc_error(404, 'AccountNotFound', 'Account was not found') unless $account; 183 + xrpc_error(400, 'InvalidRequest', 'Account does not exist: ' . ($body->{account} // q())) 184 + unless $account; 184 185 update_account_email($c, $account->{did}, $body->{email}); 185 186 return {}; 186 187 }); ··· 189 190 require_admin($c); 190 191 my $body = $c->req->json || {}; 191 192 my $account = $c->store->get_account_by_did($body->{did} // q()); 192 - xrpc_error(404, 'AccountNotFound', 'Account was not found') unless $account; 193 193 $c->store->txn(sub ($dbh) { 194 - my $deleted = $c->store->update_account( 195 - $account->{did}, 196 - deactivated_at => time, 197 - deleted_at => time, 198 - ); 199 - $c->store->revoke_sessions_by_did($account->{did}); 200 - $c->store->revoke_app_passwords_by_did($account->{did}); 201 - _append_account_event($c, $account->{did}, $deleted, _repo_account_event_payload($deleted, undef)); 194 + my $did = $body->{did} // q(); 195 + my $deleted = $account 196 + ? $c->store->update_account( 197 + $did, 198 + deactivated_at => time, 199 + deleted_at => time, 200 + ) 201 + : undef; 202 + $c->store->revoke_sessions_by_did($did); 203 + $c->store->revoke_app_passwords_by_did($did); 204 + my $payload = $deleted 205 + ? _repo_account_event_payload($deleted, undef) 206 + : { 207 + active => JSON::PP::false, 208 + status => 'deleted', 209 + }; 210 + _append_account_event($c, $did, $deleted, $payload); 202 211 }); 203 - return {}; 212 + $c->render(data => q()); 213 + return; 204 214 }); 205 215 206 216 $registry->register('com.atproto.admin.disableInviteCodes', sub ($c, $endpoint) {
+23 -1
script/differential-validate
··· 2312 2312 }, 2313 2313 admin_auth_header($server{$name}{admin_password}), 2314 2314 ); 2315 + my $update_email_missing = post_json( 2316 + $server{$name}{origin}, 2317 + 'com.atproto.admin.updateAccountEmail', 2318 + { 2319 + account => 'did:web:missing.test', 2320 + email => 'missing@test.invalid', 2321 + }, 2322 + admin_auth_header($server{$name}{admin_password}), 2323 + ); 2324 + my $delete_missing = post_json( 2325 + $server{$name}{origin}, 2326 + 'com.atproto.admin.deleteAccount', 2327 + { 2328 + did => 'did:web:missing.test', 2329 + }, 2330 + admin_auth_header($server{$name}{admin_password}), 2331 + ); 2315 2332 my $disable_admin = post_json( 2316 2333 $server{$name}{origin}, 2317 2334 'com.atproto.admin.disableInviteCodes', ··· 2386 2403 returned_count => scalar @infos, 2387 2404 first_is_primary => (@infos && (($infos[0]{did} // q()) eq $server{$name}{did})) ? 1 : 0, 2388 2405 }, 2389 - send_missing => normalize_xrpc_error($send_missing), 2406 + send_missing => normalize_xrpc_error($send_missing), 2407 + update_email_missing => normalize_xrpc_error($update_email_missing), 2408 + delete_missing => { 2409 + status => $delete_missing->code // 0, 2410 + body => $delete_missing->body, 2411 + }, 2390 2412 disable_admin_codes => normalize_xrpc_error($disable_admin), 2391 2413 disable_account_invites => { 2392 2414 status => $disable_invites->code // 0,
+23
t/uncovered-endpoints.t
··· 170 170 is($account->{email}, 'alice+admin@example.test', 'admin.updateAccountEmail normalizes email'); 171 171 ok(!defined($account->{email_confirmed_at}), 'admin.updateAccountEmail clears email confirmation state'); 172 172 173 + $t->post_ok('/xrpc/com.atproto.admin.updateAccountEmail' => { 174 + Authorization => $admin_auth, 175 + } => json => { 176 + account => 'did:web:missing.test', 177 + email => 'missing@example.test', 178 + })->status_is(400) 179 + ->json_is('/error' => 'InvalidRequest') 180 + ->json_is('/message' => 'Account does not exist: did:web:missing.test'); 181 + 173 182 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getAccountInfo')->query( 174 183 did => $did, 175 184 ) => { ··· 214 223 })->status_is(400) 215 224 ->json_is('/error' => 'InvalidRequest') 216 225 ->json_is('/message' => 'signingKey must be a valid secp256k1 did:key'); 226 + 227 + my $before_missing_delete_seq = $app->store->latest_event_seq; 228 + $t->post_ok('/xrpc/com.atproto.admin.deleteAccount' => { 229 + Authorization => $admin_auth, 230 + } => json => { 231 + did => 'did:web:missing.test', 232 + })->status_is(200) 233 + ->content_is(''); 234 + 235 + my $missing_delete_event = $app->store->list_events_from($before_missing_delete_seq + 1, limit => 1)->[0]; 236 + is($missing_delete_event->{type}, 'account', 'admin.deleteAccount missing DID still appends an account event'); 237 + is($missing_delete_event->{did}, 'did:web:missing.test', 'missing delete event identifies the requested DID'); 238 + ok(!$missing_delete_event->{payload}{active}, 'missing delete event marks the account inactive'); 239 + is($missing_delete_event->{payload}{status}, 'deleted', 'missing delete event reports deleted status'); 217 240 218 241 $t->post_ok('/xrpc/com.atproto.server.createInviteCodes' => { 219 242 Authorization => $admin_auth,