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 subject status semantics

alice 51301c34 9bfa8f7b

+113 -6
+3 -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=2967` 16 + - latest full green result in the realigned Meridian worktree: `Files=48, Tests=3000` 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` ··· 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.getAccountInfo` now follows the reference runtime’s `400 NotFound` / `Account not found` shape for a missing DID instead of returning a local `404 AccountNotFound`. 61 62 - `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 63 - `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`. 64 + - `com.atproto.admin.getSubjectStatus` now follows the reference runtime more closely for repo subjects: existing repos return a synthesized active subject status even without a stored moderation row, missing repos use `400 NotFound` / `Subject not found`, blob requests without a DID use `400 InvalidRequest` / `Must provide a did to request blob state`, and entirely missing subject references use `400 InvalidRequest` / `No provided subject`. 63 65 - `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. 64 66 - `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`. 65 67 - `com.atproto.admin.getInviteCodes` now matches the official runtime on sort validation, always-emitted cursor behavior, total `available` counts, and newest-first `uses` ordering.
+5 -3
lib/ATProto/PDS/API/Admin.pm
··· 23 23 $registry->register('com.atproto.admin.getAccountInfo', sub ($c, $endpoint) { 24 24 require_admin($c); 25 25 my $account = $c->store->get_account_by_did($c->param('did') // q()); 26 - xrpc_error(404, 'AccountNotFound', 'Account was not found') unless $account; 26 + xrpc_error(400, 'NotFound', 'Account not found') unless $account; 27 27 return admin_account_view($c->store, $account, entryway => $c->config_value('entryway', 0)); 28 28 }); 29 29 ··· 57 57 require_admin($c); 58 58 my $subject = _subject_from_params($c); 59 59 my $status = current_subject_status($c, $subject); 60 - xrpc_error(404, 'NotFound', 'Subject not found') unless $status; 60 + xrpc_error(400, 'NotFound', 'Subject not found') unless $status; 61 61 return { 62 62 subject => $status->{subject}, 63 63 ($status->{takedown} ? (takedown => $status->{takedown}) : ()), ··· 303 303 sub _subject_from_params ($c) { 304 304 return { did => $c->param('did') } if defined($c->param('did')) && !defined($c->param('uri')) && !defined($c->param('blob')); 305 305 return { uri => $c->param('uri') } if defined $c->param('uri'); 306 + xrpc_error(400, 'InvalidRequest', 'Must provide a did to request blob state') 307 + if defined($c->param('blob')) && !defined($c->param('did')); 306 308 return { 307 309 did => $c->param('did'), 308 310 cid => $c->param('blob'), 309 311 } if defined $c->param('blob'); 310 - xrpc_error(400, 'InvalidRequest', 'A subject reference is required'); 312 + xrpc_error(400, 'InvalidRequest', 'No provided subject'); 311 313 } 312 314 313 315 sub _validated_subject ($c, $subject) {
+23 -2
lib/ATProto/PDS/Moderation.pm
··· 62 62 sub current_subject_status ($c, $subject) { 63 63 my $key = subject_key($subject); 64 64 my $status = $c->store->get_subject_status($key); 65 + my $current_subject = $status ? ($status->{subject} || $subject) : $subject; 66 + if (exists($subject->{did}) && !exists($subject->{uri}) && !exists($subject->{cid})) { 67 + my $account = $c->store->get_account_by_did($subject->{did}); 68 + return undef unless $account; 69 + return { 70 + ($status ? %{$status} : ()), 71 + subject => { 72 + did => $subject->{did}, 73 + '$type' => (($status && $status->{subject}{'$type'}) ? $status->{subject}{'$type'} : 'com.atproto.admin.defs#repoRef'), 74 + }, 75 + takedown => ($status && $status->{takedown}) ? $status->{takedown} : { applied => JSON::PP::false }, 76 + deactivated => defined($account->{deactivated_at}) 77 + ? { applied => JSON::PP::true } 78 + : (($status && $status->{deactivated}) ? $status->{deactivated} : { applied => JSON::PP::false }), 79 + }; 80 + } 65 81 return undef unless $status; 66 - my $current_subject = $status->{subject} || $subject; 67 82 if (exists $subject->{uri}) { 68 83 my $current = current_record_subject($c, $subject->{uri}); 69 84 return undef unless $current; 70 85 $current_subject = { 71 86 %{$current}, 72 - ($status->{subject}{'$type'} ? ('$type' => $status->{subject}{'$type'}) : ()), 87 + '$type' => (($status->{subject}{'$type'}) ? $status->{subject}{'$type'} : 'com.atproto.repo.strongRef'), 88 + }; 89 + } elsif (exists($subject->{did}) && exists($subject->{cid})) { 90 + $current_subject = { 91 + did => $subject->{did}, 92 + cid => $subject->{cid}, 93 + '$type' => (($status->{subject}{'$type'}) ? $status->{subject}{'$type'} : 'com.atproto.admin.defs#repoBlobRef'), 73 94 }; 74 95 } 75 96 return {
+42
script/differential-validate
··· 2303 2303 }, 2304 2304 admin_auth_header($server{$name}{admin_password}), 2305 2305 ); 2306 + my $info_missing = get_form( 2307 + $server{$name}{origin}, 2308 + 'com.atproto.admin.getAccountInfo', 2309 + { did => 'did:web:missing.test' }, 2310 + admin_auth_header($server{$name}{admin_password}), 2311 + ); 2306 2312 my $send_missing = post_json( 2307 2313 $server{$name}{origin}, 2308 2314 'com.atproto.admin.sendEmail', ··· 2329 2335 }, 2330 2336 admin_auth_header($server{$name}{admin_password}), 2331 2337 ); 2338 + my $subject_active = get_form( 2339 + $server{$name}{origin}, 2340 + 'com.atproto.admin.getSubjectStatus', 2341 + { did => $server{$name}{did} }, 2342 + admin_auth_header($server{$name}{admin_password}), 2343 + ); 2344 + my $subject_missing = get_form( 2345 + $server{$name}{origin}, 2346 + 'com.atproto.admin.getSubjectStatus', 2347 + { did => 'did:web:missing.test' }, 2348 + admin_auth_header($server{$name}{admin_password}), 2349 + ); 2350 + my $subject_missing_blob_did = get_form( 2351 + $server{$name}{origin}, 2352 + 'com.atproto.admin.getSubjectStatus', 2353 + { blob => 'bafkqaaa' }, 2354 + admin_auth_header($server{$name}{admin_password}), 2355 + ); 2356 + my $subject_missing_ref = get_form( 2357 + $server{$name}{origin}, 2358 + 'com.atproto.admin.getSubjectStatus', 2359 + {}, 2360 + admin_auth_header($server{$name}{admin_password}), 2361 + ); 2332 2362 my $disable_admin = post_json( 2333 2363 $server{$name}{origin}, 2334 2364 'com.atproto.admin.disableInviteCodes', ··· 2394 2424 2395 2425 my $info_json = $info->json || {}; 2396 2426 my @infos = @{ $info_json->{infos} || [] }; 2427 + my $subject_active_json = $subject_active->json || {}; 2397 2428 my $disabled_json = $disabled_info->json || {}; 2398 2429 my $enabled_json = $enabled_info->json || {}; 2399 2430 ··· 2403 2434 returned_count => scalar @infos, 2404 2435 first_is_primary => (@infos && (($infos[0]{did} // q()) eq $server{$name}{did})) ? 1 : 0, 2405 2436 }, 2437 + get_info_missing => normalize_xrpc_error($info_missing), 2406 2438 send_missing => normalize_xrpc_error($send_missing), 2407 2439 update_email_missing => normalize_xrpc_error($update_email_missing), 2408 2440 delete_missing => { 2409 2441 status => $delete_missing->code // 0, 2410 2442 body => $delete_missing->body, 2411 2443 }, 2444 + get_subject_active => { 2445 + status => $subject_active->code // 0, 2446 + subject_is_primary => (($subject_active_json->{subject}{did} // q()) eq $server{$name}{did}) ? 1 : 0, 2447 + subject_type => $subject_active_json->{subject}{'$type'}, 2448 + takedown_applied => $subject_active_json->{takedown}{applied} ? 1 : 0, 2449 + deactivated_applied => $subject_active_json->{deactivated}{applied} ? 1 : 0, 2450 + }, 2451 + get_subject_missing => normalize_xrpc_error($subject_missing), 2452 + get_subject_missing_blob_did => normalize_xrpc_error($subject_missing_blob_did), 2453 + get_subject_missing_ref => normalize_xrpc_error($subject_missing_ref), 2412 2454 disable_admin_codes => normalize_xrpc_error($disable_admin), 2413 2455 disable_account_invites => { 2414 2456 status => $disable_invites->code // 0,
+40
t/uncovered-endpoints.t
··· 151 151 152 152 is(scalar @{ $t->tx->res->json->{infos} || [] }, 1, 'getAccountInfos returns only existing accounts'); 153 153 154 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getAccountInfo')->query( 155 + did => 'did:web:missing.test', 156 + ) => { 157 + Authorization => $admin_auth, 158 + })->status_is(400) 159 + ->json_is('/error' => 'NotFound') 160 + ->json_is('/message' => 'Account not found'); 161 + 154 162 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.searchAccounts')->query( 155 163 email => 'ALICE@EXAMPLE.TEST', 156 164 ) => { ··· 185 193 Authorization => $admin_auth, 186 194 })->status_is(200) 187 195 ->json_is('/email' => 'alice+admin@example.test'); 196 + 197 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getSubjectStatus')->query( 198 + did => $did, 199 + ) => { 200 + Authorization => $admin_auth, 201 + })->status_is(200) 202 + ->json_is('/subject/did' => $did) 203 + ->json_is('/subject/$type' => 'com.atproto.admin.defs#repoRef') 204 + ->json_is('/takedown/applied' => JSON::PP::false) 205 + ->json_is('/deactivated/applied' => JSON::PP::false); 206 + 207 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getSubjectStatus')->query( 208 + did => 'did:web:missing.test', 209 + ) => { 210 + Authorization => $admin_auth, 211 + })->status_is(400) 212 + ->json_is('/error' => 'NotFound') 213 + ->json_is('/message' => 'Subject not found'); 214 + 215 + $t->get_ok('/xrpc/com.atproto.admin.getSubjectStatus' => { 216 + Authorization => $admin_auth, 217 + })->status_is(400) 218 + ->json_is('/error' => 'InvalidRequest') 219 + ->json_is('/message' => 'No provided subject'); 220 + 221 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getSubjectStatus')->query( 222 + blob => 'bafkqaaa', 223 + ) => { 224 + Authorization => $admin_auth, 225 + })->status_is(400) 226 + ->json_is('/error' => 'InvalidRequest') 227 + ->json_is('/message' => 'Must provide a did to request blob state'); 188 228 189 229 $t->post_ok('/xrpc/com.atproto.admin.sendEmail' => { 190 230 Authorization => $admin_auth,