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 blob and block missing-repo reads

alice 3423427a b6b6c2b8

+31 -5
+2 -2
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=61, Tests=3119` 16 + - latest full green result in the realigned Meridian worktree: `Files=61, Tests=3128` 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` ··· 76 76 - `com.atproto.identity.resolveHandle` should treat well-formed but unresolved handles as `400 InvalidRequest` with `Unable to resolve handle`, matching the official runtime instead of returning a local `404 HandleNotFound`. 77 77 - Remote `did:web` DID docs, conservative `resolveIdentity` handle validation, and external handle adoption all need explicit coverage because small resolver-policy drifts turn into visible interop bugs quickly. 78 78 - Remote `did:plc` DID docs should resolve through the PLC directory defaults even when `plc_url` is not explicitly configured; gating that path on local config silently breaks federated identity lookups. 79 - - Missing-repo read paths now match the official runtime more closely: `describeRepo`, `sync.getLatestCommit`, `sync.getRecord`, `sync.getRepo`, `sync.getCheckout`, `sync.getHead`, and `sync.getRepoStatus` report `400 RepoNotFound`, while `listRecords` reports `400 InvalidRequest` / `Could not find repo: ...`. 79 + - Missing-repo read paths now match the official runtime more closely: `describeRepo`, `sync.getLatestCommit`, `sync.getRecord`, `sync.getRepo`, `sync.getCheckout`, `sync.getHead`, `sync.getRepoStatus`, `sync.getBlocks`, `sync.getBlob`, and `sync.listBlobs` report `400 RepoNotFound`, while `listRecords` reports `400 InvalidRequest` / `Could not find repo: ...`. 80 80 - `com.atproto.repo.getRecord` must honor `cid` when present, and `putRecord` / `deleteRecord` must actually enforce `swapRecord`; those negative edges are now covered directly. 81 81 - `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. 82 82 - 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`.
+3 -3
lib/ATProto/PDS/API/Sync.pm
··· 94 94 }); 95 95 96 96 $registry->register('com.atproto.sync.getBlocks', sub ($c, $endpoint) { 97 - my $account = _readable_repo_by_did($c); 97 + my $account = _readable_repo_by_did($c, missing_status => 400); 98 98 my @cids = flatten_params($c->every_param('cids')); 99 99 xrpc_error(400, 'InvalidRequest', 'At least one CID is required') unless @cids; 100 100 my $repo_car = $c->store->repo_car($account->{did}); ··· 112 112 }); 113 113 114 114 $registry->register('com.atproto.sync.getBlob', sub ($c, $endpoint) { 115 - my $account = _repo_by_did_or_error($c); 115 + my $account = _repo_by_did_or_error($c, missing_status => 400); 116 116 my $blob = $c->store->get_blob($c->param('cid') // q()); 117 117 xrpc_error(404, 'BlobNotFound', 'Blob was not found') 118 118 unless $blob && $c->store->blob_owned_by_did($c->param('cid') // q(), $account->{did}); ··· 156 156 }); 157 157 158 158 $registry->register('com.atproto.sync.listBlobs', sub ($c, $endpoint) { 159 - my $account = _readable_repo_by_did($c); 159 + my $account = _readable_repo_by_did($c, missing_status => 400); 160 160 my $page = $c->store->list_blobs_by_did( 161 161 $account->{did}, 162 162 since => $c->param('since'),
+14
script/differential-validate
··· 2022 2022 did => $server{$name}{did}, 2023 2023 cids => 'bafyreifakecidmismatch', 2024 2024 }); 2025 + my $missing_repo = get_form($server{$name}{origin}, 'com.atproto.sync.getBlocks', { 2026 + did => 'did:web:missing.test', 2027 + cids => $requested_cid, 2028 + }); 2025 2029 2026 2030 $server{$name}{get_blocks} = { 2027 2031 ok => $res->is_success ? 1 : 0, ··· 2030 2034 has_requested_cid => ($car && scalar(grep { $_->{cid}->to_string eq $requested_cid } @{ $car->{blocks} || [] })) ? 1 : 0, 2031 2035 missing_status => $missing->code, 2032 2036 missing_error => ($missing->json || {})->{error}, 2037 + missing_repo => normalize_xrpc_error($missing_repo), 2033 2038 }; 2034 2039 } 2035 2040 ··· 2120 2125 did => $server{$name}{did}, 2121 2126 since => $server{$name}{latest_commit_raw}{rev}, 2122 2127 }); 2128 + my $missing_repo = get_form($server{$name}{origin}, 'com.atproto.sync.listBlobs', { 2129 + did => 'did:web:missing.test', 2130 + }); 2123 2131 check($res->is_success, "$name listBlobs with since succeeds"); 2124 2132 my $json = $res->json || {}; 2125 2133 my $blob_cid = $blob->{ref}{'$link'}; ··· 2130 2138 ok => $res->is_success ? 1 : 0, 2131 2139 returns_blob => (scalar grep { $_ eq $blob_cid } @{ $json->{cids} || [] }) ? 1 : 0, 2132 2140 cursor_matches_tail => (($json->{cursor} // q()) eq (($json->{cids} || [])->[-1] // q())) ? 1 : 0, 2141 + missing_repo => normalize_xrpc_error($missing_repo), 2133 2142 }; 2134 2143 } 2135 2144 ··· 2148 2157 did => $server{$name}{did}, 2149 2158 cid => $blob_cid, 2150 2159 }); 2160 + my $missing_repo = get_form($server{$name}{origin}, 'com.atproto.sync.getBlob', { 2161 + did => 'did:web:missing.test', 2162 + cid => $blob_cid, 2163 + }); 2151 2164 check($res->is_success, "$name getBlob succeeds"); 2152 2165 next unless $res->is_success; 2153 2166 my $content_type = $res->headers->content_type // q(); ··· 2158 2171 nosniff => (($res->headers->header('X-Content-Type-Options') // q()) eq 'nosniff') ? 1 : 0, 2159 2172 csp_sandbox => (($res->headers->header('Content-Security-Policy') // q()) eq q{default-src 'none'; sandbox}) ? 1 : 0, 2160 2173 attachment_name => ($disposition =~ /\Aattachment; filename="/) ? 1 : 0, 2174 + missing_repo => normalize_xrpc_error($missing_repo), 2161 2175 }; 2162 2176 } 2163 2177
+12
t/blob-sync-surfaces.t
··· 89 89 ->status_is(200); 90 90 like($t->tx->res->headers->content_type // '', qr{application/vnd\.ipld\.car}, 'block export is a CAR'); 91 91 92 + $t->get_ok('/xrpc/com.atproto.sync.listBlobs?did=did:web:missing.test') 93 + ->status_is(400) 94 + ->json_is('/error', 'RepoNotFound'); 95 + 96 + $t->get_ok('/xrpc/com.atproto.sync.getBlob?did=did:web:missing.test&cid=' . $blob_cid) 97 + ->status_is(400) 98 + ->json_is('/error', 'RepoNotFound'); 99 + 100 + $t->get_ok('/xrpc/com.atproto.sync.getBlocks?did=did:web:missing.test&cids=' . $commit_cid) 101 + ->status_is(400) 102 + ->json_is('/error', 'RepoNotFound'); 103 + 92 104 done_testing;