atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

Select the types of activity you want to include in your feed.

Align space listRecords value shape

zzstoatzz (Jul 9, 2026, 1:26 AM -0500) fd0c1af0 5bc7ff54

+39 -8
+1 -1
bench/README.md
··· 84 84 - `listSpaces` 85 85 - `createRecord` into a space writer repo 86 86 - `getRecord` by `(space, repo, collection, rkey)` 87 - - `listRecords`, limit 50, index-only response shape 87 + - `listRecords`, limit 50, with values included by default 88 88 - `getBlob` storage readback 89 89 - `listRepoOps` catch-up reads 90 90
+2 -1
bench/main.zig
··· 737 737 const start = nowNs(); 738 738 for (0..iterations) |_| { 739 739 _ = arena.reset(.retain_capacity); 740 - const found = try zds.storage.store.listSpaceRecords(arena.allocator(), space, account.did, space_collection, null, false, 50); 740 + const found = try zds.storage.store.listSpaceRecords(arena.allocator(), space, account.did, space_collection, null, false, 50, true); 741 741 if (records > 0 and found.len == 0) return error.MissingRecords; 742 + if (records > 0 and found[0].value_json == null) return error.MissingRecord; 742 743 } 743 744 return .{ .name = "space listRecords", .ops = iterations, .elapsed_ns = nowNs() - start }; 744 745 }
+5
docs/permissioned-data.md
··· 124 124 Permissioned records and blobs are not public repo records. They must not be 125 125 squeezed into `records`, `repo_blocks`, `commits`, or `seq_events`. 126 126 127 + `com.atproto.space.listRecords` returns current record values by default, in 128 + line with the permissioned-data proposal. Consumers that only need the 129 + collection, rkey, and CID listing can pass `excludeValues=true` to avoid 130 + materializing record JSON. 131 + 127 132 ## Zat first 128 133 129 134 Before implementing local primitives, check Zat's current public API. ZDS uses
+11 -1
src/atproto/space.zig
··· 555 555 std.mem.eql(u8, value, "true") 556 556 else 557 557 false; 558 + var exclude_values_buf: [8]u8 = undefined; 559 + const exclude_values = if (http_api.queryParam(request.url.raw, "excludeValues", &exclude_values_buf)) |value| 560 + std.mem.eql(u8, value, "true") 561 + else 562 + false; 558 563 559 564 const records = try store.listSpaceRecords( 560 565 allocator, ··· 564 569 maybe_cursor, 565 570 reverse, 566 571 http_api.queryLimit(request.url.raw, 50), 572 + !exclude_values, 567 573 ); 568 574 var out: std.Io.Writer.Allocating = .init(allocator); 569 575 defer out.deinit(); ··· 571 577 for (records, 0..) |record, idx| { 572 578 if (idx != 0) try out.writer.writeByte(','); 573 579 try out.writer.print( 574 - "{{\"collection\":{f},\"rkey\":{f},\"cid\":{f}}}", 580 + "{{\"collection\":{f},\"rkey\":{f},\"cid\":{f}", 575 581 .{ std.json.fmt(record.collection, .{}), std.json.fmt(record.rkey, .{}), std.json.fmt(record.cid, .{}) }, 576 582 ); 583 + if (record.value_json) |value_json| { 584 + try out.writer.print(",\"value\":{s}", .{value_json}); 585 + } 586 + try out.writer.writeByte('}'); 577 587 } 578 588 try out.writer.writeByte(']'); 579 589 if (records.len > 0) {
+1 -1
src/http/router.zig
··· 224 224 .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.deleteRecord", .group = "space", .auth = "experimental bearer", .summary = "Delete a record inside a permissioned data space.", .body = &.{ "space", "repo", "collection", "rkey" }, .notes = permissioned_data_note }, 225 225 .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.applyWrites", .group = "space", .auth = "experimental bearer", .summary = "Apply a batch of writes inside a permissioned data space.", .body = &.{ "space", "repo", "validate", "writes" }, .notes = permissioned_data_note }, 226 226 .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.getRecord", .group = "space", .auth = "experimental bearer or space credential", .summary = "Read a record from a permissioned data space.", .params = &.{ "space", "repo", "collection", "rkey" }, .notes = permissioned_data_note }, 227 - .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.listRecords", .group = "space", .auth = "experimental bearer or space credential", .summary = "List record keys and CIDs in a permissioned data space.", .params = &.{ "space", "repo", "collection", "limit", "cursor", "reverse" }, .notes = permissioned_data_note }, 227 + .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.listRecords", .group = "space", .auth = "experimental bearer or space credential", .summary = "List records in a permissioned data space. Values are included unless excludeValues=true.", .params = &.{ "space", "repo", "collection", "limit", "cursor", "reverse", "excludeValues" }, .notes = permissioned_data_note }, 228 228 .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.getBlob", .group = "space", .auth = "experimental bearer or space credential", .summary = "Read a blob referenced from a permissioned data record.", .params = &.{ "space", "repo", "cid" }, .notes = permissioned_data_note }, 229 229 .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.getRepoState", .group = "space", .auth = "experimental bearer or space credential", .summary = "Read current record-set commitment state for a writer repo in a space.", .params = &.{ "space", "repo" }, .notes = permissioned_data_note }, 230 230 .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.listRepoOps", .group = "space", .auth = "experimental bearer or space credential", .summary = "Read incremental record operations for a writer repo in a space.", .params = &.{ "space", "repo", "since", "limit" }, .notes = permissioned_data_note },
+1 -1
src/internal/account.zig
··· 160 160 \\function renderRecords(records,append=false){const box=$('records');if(!append)box.innerHTML='';if(!records.length&&!append)box.innerHTML='<div class="empty">No records for that repo.</div>';for(const rec of records){const btn=document.createElement('button');btn.type='button';btn.className='item';btn.innerHTML='<span class="item-title">'+esc(rec.collection)+' / '+esc(rec.rkey)+'</span><span class="item-meta">'+esc(rec.cid)+'</span>';btn.onclick=()=>openRecord(rec);box.appendChild(btn)}} 161 161 \\async function loadRecords(append=false){if(!selectedSpace)throw new Error('choose a space first');const repo=$('repo').value.trim(),collection=$('collection').value.trim();if(!repo)throw new Error('enter a writer repo DID');setStatus('loading records...');const params={space:selectedSpace,repo,limit:'50'};if(collection)params.collection=collection;if(append&&cursor)params.cursor=cursor;const out=await xrpc('/xrpc/com.atproto.space.listRecords',params);cursor=out.cursor||null;$('next-records').disabled=!cursor;renderRecords(out.records||[],append);setStatus('records loaded','ok')} 162 162 \\function blobChips(value){const out=[],seen=new Set();function walk(v){if(!v||typeof v!=='object')return;if(v.ref&&v.ref.$link&&!seen.has(v.ref.$link)){seen.add(v.ref.$link);out.push(v.ref.$link)}for(const child of Array.isArray(v)?v:Object.values(v))walk(child)}walk(value);return out.map(cid=>'<span class="blob-chip">blob '+esc(cid)+'</span>').join(' ')} 163 - \\async function openRecord(rec){const detail=$('record-detail');detail.className='';detail.innerHTML='<p class="hint">loading record...</p>';const out=await xrpc('/xrpc/com.atproto.space.getRecord',{space:selectedSpace,repo:$('repo').value.trim(),collection:rec.collection,rkey:rec.rkey});detail.innerHTML='<div class="record-head"><div><strong>'+esc(rec.collection)+' / '+esc(rec.rkey)+'</strong><span class="item-meta">'+esc(out.cid)+'</span></div><button class="secondary" type="button" id="copy-json">copy JSON</button></div>'+blobChips(out.value)+'<pre>'+esc(JSON.stringify(out.value,null,2))+'</pre>';$('copy-json').onclick=()=>navigator.clipboard.writeText(JSON.stringify(out.value,null,2))} 163 + \\async function openRecord(rec){const detail=$('record-detail');detail.className='';detail.innerHTML='<p class="hint">loading record...</p>';const out=rec.value?rec:await xrpc('/xrpc/com.atproto.space.getRecord',{space:selectedSpace,repo:$('repo').value.trim(),collection:rec.collection,rkey:rec.rkey});detail.innerHTML='<div class="record-head"><div><strong>'+esc(rec.collection)+' / '+esc(rec.rkey)+'</strong><span class="item-meta">'+esc(out.cid)+'</span></div><button class="secondary" type="button" id="copy-json">copy JSON</button></div>'+blobChips(out.value)+'<pre>'+esc(JSON.stringify(out.value,null,2))+'</pre>';$('copy-json').onclick=()=>navigator.clipboard.writeText(JSON.stringify(out.value,null,2))} 164 164 \\async function loadAccountData(){setStatus('loading account...');const [session,passkeys,passwords,sessions,spaceOut]=await Promise.all([xrpc('/xrpc/com.atproto.server.getSession'),xrpc('/xrpc/com.atproto.server.listPasskeys'),xrpc('/xrpc/com.atproto.server.listAppPasswords'),xrpc('/xrpc/dev.zat.account.listSessions',{active:'false',limit:'200'}),xrpc('/xrpc/com.atproto.space.listSpaces',{limit:'100'}).catch(()=>({spaces:[]}))]);account=session;spaces=spaceOut.spaces||[];updateHeader();showPasskeys(passkeys.passkeys||[]);showAppPasswords(passwords.passwords||[]);showSessions(sessions);renderSpaces();quickChecks(sessions);setStatus('signed in','ok')} 165 165 \\loginForm.addEventListener('submit',async(e)=>{e.preventDefault();try{setStatus('signing in...');const f=e.currentTarget;const session=await fail(await fetch('/xrpc/com.atproto.server.createSession',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({identifier:f.identifier.value,password:f.password.value})}),'sign in failed');accessJwt=session.accessJwt;account=session;loginForm.classList.add('off');resident.classList.add('on');showView();await loadAccountData()}catch(err){setStatus(err.message||String(err),'error')}}) 166 166 \\$('add-passkey').onclick=async()=>{try{setStatus('opening browser passkey prompt...');const friendlyName=$('friendly').value;const start=await xrpc('/xrpc/com.atproto.server.startPasskeyRegistration',{}, {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({friendlyName})});const credential=await navigator.credentials.create(prepCreate(start.options));await xrpc('/xrpc/com.atproto.server.finishPasskeyRegistration',{}, {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({friendlyName,credential:serialize(credential)})});setStatus('passkey saved','ok');await loadAccountData()}catch(err){setStatus((err.name?err.name+': ':'')+(err.message||String(err)),'error')}}
+12 -3
src/storage/store.zig
··· 333 333 collection: []const u8, 334 334 rkey: []const u8, 335 335 cid: []const u8, 336 + value_json: ?[]const u8 = null, 336 337 }; 337 338 338 339 pub const SpaceRecordOplogEntry = struct { ··· 3511 3512 maybe_cursor: ?[]const u8, 3512 3513 reverse: bool, 3513 3514 limit: usize, 3515 + include_values: bool, 3514 3516 ) ![]SpaceRecordRef { 3515 3517 db_mutex.lockUncancelable(store_io); 3516 3518 defer db_mutex.unlock(store_io); ··· 3527 3529 var query = std.Io.Writer.Allocating.init(allocator); 3528 3530 defer query.deinit(); 3529 3531 try query.writer.print( 3530 - "SELECT collection, rkey, cid FROM permissioned_space_records WHERE space = ? AND repo_did = ?", 3531 - .{}, 3532 + "SELECT collection, rkey, cid, {s} FROM permissioned_space_records WHERE space = ? AND repo_did = ?", 3533 + .{if (include_values) "value_json" else "NULL"}, 3532 3534 ); 3533 3535 if (maybe_collection != null) try query.writer.writeAll(" AND collection = ?"); 3534 3536 if (maybe_cursor != null and cursor_collection.len > 0 and cursor_rkey.len > 0) { ··· 3553 3555 .collection = try allocator.dupe(u8, row.text(0)), 3554 3556 .rkey = try allocator.dupe(u8, row.text(1)), 3555 3557 .cid = try allocator.dupe(u8, row.text(2)), 3558 + .value_json = if (row.nullableText(3)) |value| try allocator.dupe(u8, value) else null, 3556 3559 }); 3557 3560 } 3558 3561 if (rows.err) |err| return err; ··· 6308 6311 try std.testing.expectEqualStrings(stored.cid, found.cid); 6309 6312 try std.testing.expect(std.mem.indexOf(u8, found.value_json, "secret track") != null); 6310 6313 6311 - const listed = try listSpaceRecords(allocator, space.uri, account.did, "fm.plyr.track", null, false, 50); 6314 + const listed = try listSpaceRecords(allocator, space.uri, account.did, "fm.plyr.track", null, false, 50, true); 6312 6315 try std.testing.expectEqual(@as(usize, 1), listed.len); 6313 6316 try std.testing.expectEqualStrings("fm.plyr.track", listed[0].collection); 6314 6317 try std.testing.expectEqualStrings("track-one", listed[0].rkey); 6318 + try std.testing.expect(listed[0].value_json != null); 6319 + try std.testing.expect(std.mem.indexOf(u8, listed[0].value_json.?, "secret track") != null); 6320 + 6321 + const listed_refs = try listSpaceRecords(allocator, space.uri, account.did, "fm.plyr.track", null, false, 50, false); 6322 + try std.testing.expectEqual(@as(usize, 1), listed_refs.len); 6323 + try std.testing.expect(listed_refs[0].value_json == null); 6315 6324 6316 6325 const updated_parsed = try std.json.parseFromSlice( 6317 6326 std.json.Value,
+6
tools/smoke-permissioned.sh
··· 111 111 112 112 space_records=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.listRecords?space=$encoded_space&repo=did:plc:permissionsmoke&collection=fm.plyr.track&limit=1") 113 113 printf '%s' "$space_records" | grep -q '"collection":"fm.plyr.track"' 114 + printf '%s' "$space_records" | grep -q '"value":' 115 + printf '%s' "$space_records" | grep -q '"title":"private smoke"' 114 116 printf '%s' "$space_records" | grep -q '"cursor":"fm.plyr.track/track-one"' 117 + 118 + space_record_refs=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.listRecords?space=$encoded_space&repo=did:plc:permissionsmoke&collection=fm.plyr.track&limit=1&excludeValues=true") 119 + printf '%s' "$space_record_refs" | grep -q '"collection":"fm.plyr.track"' 120 + ! printf '%s' "$space_record_refs" | grep -q '"value":' 115 121 116 122 space_blob_status=$(curl -sS -o /tmp/zds-space-blob-range.bin -w '%{http_code}' \ 117 123 -H "authorization: Bearer $token" \