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 mutation no-op semantics

alice b5733573 51301c34

+77 -14
+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=3000` 16 + - latest full green result in the realigned Meridian worktree: `Files=48, Tests=3009` 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` ··· 62 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`. 63 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 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`. 65 + - `com.atproto.admin.updateAccountPassword` and `com.atproto.admin.disableAccountInvites` / `enableAccountInvites` now follow the reference runtime’s missing-account behavior: they are empty-body `200` no-ops instead of returning local `404 AccountNotFound` or JSON `{}` success bodies. 65 66 - `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. 66 67 - `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`. 67 68 - `com.atproto.admin.getInviteCodes` now matches the official runtime on sort validation, always-emitted cursor behavior, total `available` counts, and newest-first `uses` ordering.
+8 -9
lib/ATProto/PDS/API/Admin.pm
··· 162 162 my $body = $c->req->json || {}; 163 163 xrpc_error(400, 'InvalidRequest', 'Invalid password length.') 164 164 if length($body->{password} // q()) > $NEW_PASSWORD_MAX_LENGTH; 165 - my $account = $c->store->get_account_by_did($body->{did} // q()); 166 - xrpc_error(404, 'AccountNotFound', 'Account was not found') unless $account; 167 165 my $password_record = hash_password($body->{password}); 168 166 $c->store->txn(sub ($dbh) { 167 + my $did = $body->{did} // q(); 169 168 $c->store->update_account( 170 - $account->{did}, 169 + $did, 171 170 password_hash => $password_record->{hash}, 172 171 password_salt => $password_record->{salt}, 173 172 ); 174 - $c->store->revoke_sessions_by_did($account->{did}); 173 + $c->store->revoke_sessions_by_did($did); 175 174 }); 176 - return {}; 175 + $c->render(data => q()); 176 + return; 177 177 }); 178 178 179 179 $registry->register('com.atproto.admin.updateAccountEmail', sub ($c, $endpoint) { ··· 401 401 } 402 402 403 403 sub _set_account_invites ($c, $identifier, $disabled, $note) { 404 - my $account = $c->store->get_account_by_did($identifier // q()); 405 - xrpc_error(404, 'AccountNotFound', 'Account was not found') unless $account; 406 404 $c->store->update_account( 407 - $account->{did}, 405 + $identifier // q(), 408 406 invites_disabled => $disabled ? 1 : 0, 409 407 invite_note => undef, 410 408 ); 411 - return {}; 409 + $c->render(data => q()); 410 + return; 412 411 } 413 412 414 413 sub _append_account_event ($c, $did, $account, $payload) {
+39
script/differential-validate
··· 2374 2374 }, 2375 2375 admin_auth_header($server{$name}{admin_password}), 2376 2376 ); 2377 + my $disable_invites_missing = post_json( 2378 + $server{$name}{origin}, 2379 + 'com.atproto.admin.disableAccountInvites', 2380 + { 2381 + account => 'did:web:missing.test', 2382 + note => 'diff disable missing', 2383 + }, 2384 + admin_auth_header($server{$name}{admin_password}), 2385 + ); 2377 2386 my $disabled_info = get_form( 2378 2387 $server{$name}{origin}, 2379 2388 'com.atproto.admin.getAccountInfo', ··· 2389 2398 }, 2390 2399 admin_auth_header($server{$name}{admin_password}), 2391 2400 ); 2401 + my $enable_invites_missing = post_json( 2402 + $server{$name}{origin}, 2403 + 'com.atproto.admin.enableAccountInvites', 2404 + { 2405 + account => 'did:web:missing.test', 2406 + note => 'diff enable missing', 2407 + }, 2408 + admin_auth_header($server{$name}{admin_password}), 2409 + ); 2392 2410 my $enabled_info = get_form( 2393 2411 $server{$name}{origin}, 2394 2412 'com.atproto.admin.getAccountInfo', ··· 2413 2431 }, 2414 2432 admin_auth_header($server{$name}{admin_password}), 2415 2433 ); 2434 + my $reset_password_missing = post_json( 2435 + $server{$name}{origin}, 2436 + 'com.atproto.admin.updateAccountPassword', 2437 + { 2438 + did => 'did:web:missing.test', 2439 + password => 'newhunter22', 2440 + }, 2441 + admin_auth_header($server{$name}{admin_password}), 2442 + ); 2416 2443 my $old_secondary_login = post_json($server{$name}{origin}, 'com.atproto.server.createSession', { 2417 2444 identifier => $server{$name}{secondary_handle}, 2418 2445 password => 'hunter22', ··· 2458 2485 invites_disabled => $disabled_json->{invitesDisabled} ? 1 : 0, 2459 2486 invite_note => $disabled_json->{inviteNote}, 2460 2487 }, 2488 + disable_account_invites_missing => { 2489 + status => $disable_invites_missing->code // 0, 2490 + body => $disable_invites_missing->body, 2491 + }, 2461 2492 enable_account_invites => { 2462 2493 status => $enable_invites->code // 0, 2463 2494 enabled_status => $enabled_info->code // 0, 2464 2495 invites_disabled => $enabled_json->{invitesDisabled} ? 1 : 0, 2465 2496 invite_note => $enabled_json->{inviteNote}, 2466 2497 }, 2498 + enable_account_invites_missing => { 2499 + status => $enable_invites_missing->code // 0, 2500 + body => $enable_invites_missing->body, 2501 + }, 2467 2502 update_password_short => normalize_xrpc_error($short_password), 2468 2503 update_password_reset => { 2469 2504 status => $reset_password->code // 0, 2470 2505 old_login => normalize_xrpc_error($old_secondary_login), 2471 2506 new_login => normalize_xrpc_error($new_secondary_login), 2507 + }, 2508 + update_password_missing => { 2509 + status => $reset_password_missing->code // 0, 2510 + body => $reset_password_missing->body, 2472 2511 }, 2473 2512 }; 2474 2513 }
+28 -4
t/uncovered-endpoints.t
··· 382 382 account => $did, 383 383 note => 'paused for audit', 384 384 })->status_is(200) 385 - ->json_is({}); 385 + ->content_is(q()); 386 + 387 + $t->post_ok('/xrpc/com.atproto.admin.disableAccountInvites' => { 388 + Authorization => $admin_auth, 389 + } => json => { 390 + account => 'did:web:missing.test', 391 + note => 'ignored', 392 + })->status_is(200) 393 + ->content_is(q()); 386 394 387 395 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getAccountInfo')->query( 388 396 did => $did, ··· 404 412 } => json => { 405 413 account => $did, 406 414 })->status_is(200) 407 - ->json_is({}); 415 + ->content_is(q()); 416 + 417 + $t->post_ok('/xrpc/com.atproto.admin.enableAccountInvites' => { 418 + Authorization => $admin_auth, 419 + } => json => { 420 + account => 'did:web:missing.test', 421 + note => 'ignored', 422 + })->status_is(200) 423 + ->content_is(q()); 408 424 409 425 $t->post_ok('/xrpc/com.atproto.server.createInviteCode' => { 410 426 Authorization => "Bearer $access", ··· 484 500 did => $did, 485 501 password => 'short', 486 502 })->status_is(200) 487 - ->json_is({}); 503 + ->content_is(q()); 488 504 489 505 $t->post_ok('/xrpc/com.atproto.server.createSession' => json => { 490 506 identifier => 'alice.external.test', ··· 498 514 did => $did, 499 515 password => 'new-hunter22', 500 516 })->status_is(200) 501 - ->json_is({}); 517 + ->content_is(q()); 518 + 519 + $t->post_ok('/xrpc/com.atproto.admin.updateAccountPassword' => { 520 + Authorization => $admin_auth, 521 + } => json => { 522 + did => 'did:web:missing.test', 523 + password => 'new-hunter22', 524 + })->status_is(200) 525 + ->content_is(q()); 502 526 503 527 $t->get_ok('/xrpc/com.atproto.server.getSession' => { 504 528 Authorization => "Bearer $access",