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 permissioned space authority surface

zzstoatzz (Jul 1, 2026, 9:58 AM -0500) 32d801cf edfc6164

+268 -233
+1 -1
bench/README.md
··· 82 82 - `getRecord` by `(space, repo, collection, rkey)` 83 83 - `listRecords`, limit 50, index-only response shape 84 84 - `getBlob` storage readback 85 - - `getRepoOplog` catch-up reads 85 + - `listRepoOps` catch-up reads 86 86 87 87 Comparison against Daniel's permissioned-data branch should live in this 88 88 section once we have a repeatable way to run that PDS locally. Until then, keep
+3 -3
bench/main.zig
··· 571 571 ) !zds.storage.store.SpaceConfig { 572 572 const space = try zds.storage.store.createSpace(allocator, .{ 573 573 .actor_did = account.did, 574 - .owner_did = account.did, 574 + .authority_did = account.did, 575 575 .space_type = space_type, 576 576 .skey = "self", 577 - .is_owner = true, 577 + .is_authority = true, 578 578 .managing_app = "https://api-stg.plyr.fm", 579 579 .is_public = false, 580 580 .app_access_mode = "allow", ··· 668 668 const ops = try zds.storage.store.listSpaceRecordOplog(arena.allocator(), space, account.did, null, 100); 669 669 if (ops.len == 0) return error.MissingRecords; 670 670 } 671 - return .{ .name = "space getRepoOplog", .ops = iterations, .elapsed_ns = nowNs() - start }; 671 + return .{ .name = "space listRepoOps", .ops = iterations, .elapsed_ns = nowNs() - start }; 672 672 } 673 673 674 674 fn benchSpaceBlob(allocator: std.mem.Allocator, account: zds.auth.tokens.Account, cid: []const u8) !BenchResult {
+1 -1
docs/account-takedown-runbook.md
··· 139 139 union all select 'repo_blocks', count(*) from repo_blocks where did='$did' 140 140 union all select 'commits', count(*) from commits where did='$did' 141 141 union all select 'blobs', count(*) from blobs where did='$did' 142 - union all select 'spaces_owned', count(*) from permissioned_spaces where owner_did='$did' 142 + union all select 'spaces_owned', count(*) from permissioned_spaces where authority_did='$did' 143 143 union all select 'space_actor_state', count(*) from permissioned_space_actor_state where actor_did='$did' 144 144 union all select 'space_records', count(*) from permissioned_space_records where repo_did='$did' 145 145 union all select 'space_repos', count(*) from permissioned_space_repos where repo_did='$did'
+10 -10
docs/permissioned-data-proposal-94.md
··· 19 19 - isolate prototype code under the existing `space` and permissioned-data 20 20 modules 21 21 - add tests around durable semantics that are likely to survive 22 - - avoid adding new member-list or grant behavior in the old vocabulary 22 + - avoid adding new member-list behavior or resurrecting old credential names 23 23 24 24 ZDS's immediate goal is modest: get ahead of the likely direction enough to 25 25 store private data on the user's PDS, while keeping the implementation easy to ··· 95 95 break existing spaces that depend on the declaration, which makes lexicon 96 96 resolution override behavior appropriate in a way that would be surprising 97 97 for normal AT records. 98 - - `com.atproto.space.getMemberGrant` is older vocabulary. The proposal uses 99 - `com.atproto.space.getDelegationToken` as a PDS-issued OAuth query and then 100 - exchanges that with the space host through `getSpaceCredential`. 101 - - `getRepoOplog` is older vocabulary. The proposal uses `listRepoOps`. 98 + - ZDS should use the proposal names directly: `getDelegationToken` for the 99 + PDS-issued OAuth query, and `listRepoOps` for incremental permissioned-repo 100 + sync. 102 101 - The proposal adds `com.atproto.space.listRepos` so a space host can list known 103 102 writer repos in a space without enumerating readers. 104 103 - The proposal adds `registerNotify` as an explicit syncer registration method. 105 104 ZDS currently records credential recipients and supports write/deletion 106 105 notifications, but does not expose this method shape. 107 - - Space credentials and delegation tokens have more specific JWT `typ`, `sub`, 108 - `aud`, `client_id`, lifetime, and verification expectations than ZDS's 109 - prototype helper names imply. 106 + - Space credentials and delegation tokens have specific JWT `typ`, `sub`, 107 + `aud`, `client_id`, lifetime, and verification expectations. Keep ZDS helper 108 + names and token shapes aligned with those names instead of inventing a local 109 + credential vocabulary. 110 110 - The proposal introduces optional client attestation for app-bound space 111 111 credentials. 112 112 - Space-authority DID documents are expected to publish `#atproto_space` and ··· 200 200 201 201 - per-space and per-writer repo isolation 202 202 - `ats://` parsing and formatting 203 - - OAuth scope gating for owner reads, self reads, writes, and management 203 + - OAuth scope gating for authority reads, self reads, writes, and management 204 204 - ranged blob reads through permissioned-data auth 205 205 - write oplog ordering and cursor behavior 206 206 - record-set commitment changes across create, update, and delete 207 - - deletion behavior for owner spaces versus writer repos 207 + - deletion behavior for authority spaces versus writer repos 208 208 209 209 These tests should stay separate from public-repo conformance tests so the 210 210 experimental surface can move without muddying stable PDS behavior.
+8 -17
docs/permissioned-data.md
··· 56 56 `updateSpaceConfig`, `deleteSpace` 57 57 - records and blobs: `createRecord`, `putRecord`, `deleteRecord`, 58 58 `applyWrites`, `getRecord`, `listRecords`, `getBlob` 59 - - writer state: `getRepoState`, `getRepoOplog`, `notifyWrite` 60 - - credentials and deletion fanout: `getMemberGrant`, `getSpaceCredential`, 59 + - writer state: `getRepoState`, `listRepoOps`, `notifyWrite` 60 + - credentials and deletion fanout: `getDelegationToken`, `getSpaceCredential`, 61 61 `notifySpaceDeleted` 62 62 63 - The proposal draft has since moved some names and responsibilities. In 64 - particular, `getMemberGrant` maps only loosely to the newer 65 - `getDelegationToken`, and `getRepoOplog` maps only loosely to `listRepoOps`. 66 - Keep that vocabulary difference visible when changing this area. 63 + ZDS uses the proposal names for the credential and sync-read surface: 64 + `getDelegationToken` and `listRepoOps`. 67 65 68 66 ZDS deliberately does not expose the older protocol member-list routes: 69 67 ··· 93 91 experiment. 94 92 95 93 For private spaces, ZDS currently mints space credentials only to the space 96 - owner DID unless the space is public. That is the conservative default until 94 + authority DID unless the space is public. That is the conservative default until 97 95 the protocol settles on an application or space-host policy hook for broader 98 96 credential issuance. 99 97 ··· 106 104 ZDS stores many actor repos in one SQLite database, so permissioned data uses 107 105 explicit space-scoped tables instead of the public repo tables: 108 106 109 - - `permissioned_spaces`: canonical owner-space config keyed by space URI 110 - - `permissioned_space_actor_state`: actor-local owner/deleted state keyed by 107 + - `permissioned_spaces`: canonical authority-space config keyed by space URI 108 + - `permissioned_space_actor_state`: actor-local authority/deleted state keyed by 111 109 `(space, actor_did)` 112 110 - `permissioned_space_records`: current records keyed by 113 111 `(space, repo_did, collection, rkey)` with CID, DAG-CBOR value, repo revision, ··· 115 113 - `permissioned_space_repos`: writer repo state and current record-set hash 116 114 - `permissioned_space_record_oplog`: incremental record changes by 117 115 `(space, repo_did, rev, idx)` 118 - - `permissioned_space_credentials`: short-lived prototype grants and 116 + - `permissioned_space_credentials`: short-lived prototype credentials and 119 117 credentials 120 118 - `permissioned_space_credential_recipients`: services to notify for writes and 121 119 space deletion 122 120 123 121 Permissioned records and blobs are not public repo records. They must not be 124 122 squeezed into `records`, `repo_blocks`, `commits`, or `seq_events`. 125 - 126 - Existing databases from the earlier experiment drop the dedicated 127 - permissioned-space member tables during migration: 128 - 129 - - `permissioned_space_members` 130 - - `permissioned_space_member_state` 131 - - `permissioned_space_member_oplog` 132 123 133 124 ## Zat first 134 125
+51 -51
src/atproto/space.zig
··· 34 34 if (std.mem.eql(u8, method, "com.atproto.space.listSpaces")) return listSpaces(request); 35 35 if (std.mem.eql(u8, method, "com.atproto.space.updateSpaceConfig")) return updateSpaceConfig(request); 36 36 if (std.mem.eql(u8, method, "com.atproto.space.deleteSpace")) return deleteSpace(request); 37 - if (std.mem.eql(u8, method, "com.atproto.space.getMemberGrant")) return getMemberGrant(request); 37 + if (std.mem.eql(u8, method, "com.atproto.space.getDelegationToken")) return getDelegationToken(request); 38 38 if (std.mem.eql(u8, method, "com.atproto.space.createRecord")) return createRecord(request); 39 39 if (std.mem.eql(u8, method, "com.atproto.space.putRecord")) return putRecord(request); 40 40 if (std.mem.eql(u8, method, "com.atproto.space.deleteRecord")) return deleteRecord(request); ··· 43 43 if (std.mem.eql(u8, method, "com.atproto.space.listRecords")) return listRecords(request); 44 44 if (std.mem.eql(u8, method, "com.atproto.space.getBlob")) return getBlob(request); 45 45 if (std.mem.eql(u8, method, "com.atproto.space.getRepoState")) return getRepoState(request); 46 - if (std.mem.eql(u8, method, "com.atproto.space.getRepoOplog")) return getRepoOplog(request); 46 + if (std.mem.eql(u8, method, "com.atproto.space.listRepoOps")) return listRepoOps(request); 47 47 if (std.mem.eql(u8, method, "com.atproto.space.getSpaceCredential")) return getSpaceCredential(request); 48 48 if (std.mem.eql(u8, method, "com.atproto.space.notifyWrite")) return notifyWrite(request); 49 49 if (std.mem.eql(u8, method, "com.atproto.space.notifySpaceDeleted")) return notifySpaceDeleted(request); ··· 67 67 else => return err, 68 68 }; 69 69 const input = parsed.value; 70 - const did = zat.json.getString(input, "did") orelse { 71 - return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing did"); 70 + const authority = zat.json.getString(input, "authority") orelse { 71 + return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing authority"); 72 72 }; 73 73 const space_type = zat.json.getString(input, "type") orelse { 74 74 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing type"); 75 75 }; 76 76 const skey = zat.json.getString(input, "skey") orelse try store.generateRkey(allocator); 77 - try requireSpaceScope(request, auth_ctx.oauth_scope, space_type, did, skey, .manage, null); 77 + try requireSpaceScope(request, auth_ctx.oauth_scope, space_type, authority, skey, .manage, null); 78 78 const managing_app = zat.json.getString(input, "managingApp"); 79 79 const is_public = valueBool(input, "isPublic") orelse false; 80 80 const app_access_mode = zat.json.getString(input, "appAccessMode") orelse "allow"; ··· 85 85 86 86 const space = store.createSpace(allocator, .{ 87 87 .actor_did = auth_ctx.account.did, 88 - .owner_did = did, 88 + .authority_did = authority, 89 89 .space_type = space_type, 90 90 .skey = skey, 91 - .is_owner = std.mem.eql(u8, did, auth_ctx.account.did), 91 + .is_authority = std.mem.eql(u8, authority, auth_ctx.account.did), 92 92 .managing_app = managing_app, 93 93 .is_public = is_public, 94 94 .app_access_mode = app_access_mode, 95 95 .app_exceptions_json = app_exceptions_json, 96 96 }) catch |err| switch (err) { 97 - error.InvalidRepoPath => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid did"), 97 + error.InvalidRepoPath => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid authority"), 98 98 error.InvalidCollection => return http_api.xrpcError(request, .bad_request, "InvalidType", "Invalid space type"), 99 99 error.InvalidRecordKey => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid skey"), 100 100 error.InvalidRecordType => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid appAccessMode"), ··· 120 120 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); 121 121 }; 122 122 if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) { 123 - return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); 123 + return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); 124 124 } 125 125 try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .manage, null); 126 126 const space = (try store.getSpace(allocator, auth_ctx.account.did, space_uri)) orelse { 127 127 return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); 128 128 }; 129 - if (!space.is_owner) { 130 - return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); 129 + if (!space.is_authority) { 130 + return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); 131 131 } 132 132 if (space.deleted_at != null) { 133 133 return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); 134 134 } 135 - return http_api.json(request, .ok, try ownerSpaceJson(allocator, space)); 135 + return http_api.json(request, .ok, try authoritySpaceJson(allocator, space)); 136 136 } 137 137 138 138 fn listSpaces(request: *http_api.Request) !void { ··· 141 141 const allocator = arena.allocator(); 142 142 143 143 const auth_ctx = requireAccount(request, allocator) catch return; 144 - var did_buf: [256]u8 = undefined; 145 - const maybe_did = http_api.queryParam(request.url.raw, "did", &did_buf); 144 + var authority_buf: [256]u8 = undefined; 145 + const maybe_authority = http_api.queryParam(request.url.raw, "authority", &authority_buf); 146 146 var type_buf: [320]u8 = undefined; 147 147 const maybe_type = http_api.queryParam(request.url.raw, "type", &type_buf); 148 148 if (maybe_type) |space_type| { 149 149 if (zat.Nsid.parse(space_type) == null) return http_api.xrpcError(request, .bad_request, "InvalidType", "Invalid space type"); 150 150 } 151 151 if (auth_ctx.oauth_scope) |scope_text| { 152 - if (!scopes.spaceAllows(scope_text, .read, maybe_type orelse "*", maybe_did orelse "*", "*", null)) { 152 + if (!scopes.spaceAllows(scope_text, .read, maybe_type orelse "*", maybe_authority orelse "*", "*", null)) { 153 153 return http_api.xrpcError(request, .forbidden, "InsufficientScope", "OAuth token does not grant the requested space operation"); 154 154 } 155 155 } 156 156 var cursor_buf: [1024]u8 = undefined; 157 157 const maybe_cursor = http_api.queryParam(request.url.raw, "cursor", &cursor_buf); 158 - const spaces = try store.listSpaces(allocator, auth_ctx.account.did, maybe_did, maybe_type, maybe_cursor, http_api.queryLimit(request.url.raw, 50)); 158 + const spaces = try store.listSpaces(allocator, auth_ctx.account.did, maybe_authority, maybe_type, maybe_cursor, http_api.queryLimit(request.url.raw, 50)); 159 159 160 160 var out: std.Io.Writer.Allocating = .init(allocator); 161 161 defer out.deinit(); 162 162 try out.writer.writeAll("{\"spaces\":["); 163 163 for (spaces, 0..) |space, idx| { 164 164 if (idx != 0) try out.writer.writeByte(','); 165 - try out.writer.print("{{\"uri\":{f},\"isOwner\":{}}}", .{ std.json.fmt(space.uri, .{}), space.is_owner }); 165 + try out.writer.print("{{\"uri\":{f},\"isAuthority\":{}}}", .{ std.json.fmt(space.uri, .{}), space.is_authority }); 166 166 } 167 167 try out.writer.writeByte(']'); 168 168 if (spaces.len > 0) { ··· 184 184 const input = parsed_body.value; 185 185 const space = zat.json.getString(input, "space") orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing space"); 186 186 const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); 187 - if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); 187 + if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); 188 188 try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .manage, null); 189 189 const existing = (try store.getSpace(allocator, auth_ctx.account.did, space)) orelse return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); 190 - if (!existing.is_owner) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); 190 + if (!existing.is_authority) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); 191 191 if (existing.deleted_at != null) return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); 192 192 const managing_app = zat.json.getString(input, "managingApp"); 193 193 const clear_managing_app = switch (input) { ··· 228 228 }; 229 229 const space = zat.json.getString(parsed_body.value, "space") orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing space"); 230 230 const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); 231 - if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); 231 + if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); 232 232 try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .manage, null); 233 233 const existing = try store.getSpace(allocator, auth_ctx.account.did, space); 234 234 if (existing == null) return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); 235 - if (!existing.?.is_owner) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); 235 + if (!existing.?.is_authority) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); 236 236 if (existing.?.deleted_at != null) return http_api.json(request, .ok, "{}"); 237 237 const recipients = try store.listCredentialRecipients(allocator, space); 238 238 try store.markSpaceDeleted(auth_ctx.account.did, space); 239 - try store.purgeOwnerSpaceData(space); 239 + try store.purgeAuthoritySpaceData(space); 240 240 fireNotifySpaceDeleted(allocator, auth_ctx.account, space, recipients) catch {}; 241 241 return http_api.json(request, .ok, "{}"); 242 242 } 243 243 244 - fn getMemberGrant(request: *http_api.Request) !void { 244 + fn getDelegationToken(request: *http_api.Request) !void { 245 245 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 246 246 defer arena.deinit(); 247 247 const allocator = arena.allocator(); ··· 252 252 const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); 253 253 try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .read, null); 254 254 var keypair = store.signingKeypair(auth_ctx.account.did) catch return http_api.xrpcError(request, .not_found, "RepoNotFound", "Signing key not found"); 255 - const grant = try permissioned.createMemberGrant(allocator, store.currentIo(), auth_ctx.account.did, parsed.did, space, client_id, &keypair); 256 - return http_api.json(request, .ok, try std.fmt.allocPrint(allocator, "{{\"grant\":{f}}}", .{std.json.fmt(grant, .{})})); 255 + const token = try permissioned.createDelegationToken(allocator, store.currentIo(), auth_ctx.account.did, parsed.did, space, client_id, &keypair); 256 + return http_api.json(request, .ok, try std.fmt.allocPrint(allocator, "{{\"token\":{f}}}", .{std.json.fmt(token, .{})})); 257 257 } 258 258 259 259 fn createRecord(request: *http_api.Request) !void { ··· 285 285 store.deleteSpaceRecord(allocator, space, repo, collection, rkey) catch |err| switch (err) { 286 286 error.RepoNotFound => return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"), 287 287 error.MissingRecord => return http_api.xrpcError(request, .not_found, "RecordNotFound", "Record not found"), 288 - error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotAMember", "Not a member of the space"), 288 + error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotPermitted", "Not permitted to write in this space"), 289 289 else => return err, 290 290 }; 291 291 return http_api.json(request, .ok, "{}"); ··· 342 342 const results = store.applySpaceWrites(allocator, space, repo, ops.items) catch |err| switch (err) { 343 343 error.RepoNotFound => return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"), 344 344 error.MissingRecord => return http_api.xrpcError(request, .not_found, "RecordNotFound", "Record not found"), 345 - error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotAMember", "Not a member of the space"), 345 + error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotPermitted", "Not permitted to write in this space"), 346 346 else => return err, 347 347 }; 348 348 const state = try store.getSpaceRepoState(allocator, space, repo); ··· 411 411 .put => store.putSpaceRecord(allocator, space, repo, collection, rkey, prepared), 412 412 } catch |err| switch (err) { 413 413 error.RepoNotFound => return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"), 414 - error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotAMember", "Not a member of the space"), 414 + error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotPermitted", "Not permitted to write in this space"), 415 415 error.MissingRecord => return http_api.xrpcError(request, .bad_request, "RecordNotFound", "Record not found"), 416 416 else => return err, 417 417 }; ··· 536 536 return writeSignedState(request, allocator, space, repo, repo, .records, state); 537 537 } 538 538 539 - fn getRepoOplog(request: *http_api.Request) !void { 539 + fn listRepoOps(request: *http_api.Request) !void { 540 540 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 541 541 defer arena.deinit(); 542 542 const allocator = arena.allocator(); ··· 579 579 defer arena.deinit(); 580 580 const allocator = arena.allocator(); 581 581 const token = bearerToken(request) orelse return http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"); 582 - const member_did = permissioned.unverifiedStringClaim(allocator, token, "iss") catch { 583 - return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid member grant"); 582 + const requester_did = permissioned.unverifiedStringClaim(allocator, token, "iss") catch { 583 + return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid delegation token"); 584 584 }; 585 - const public_key = signingKeyForDid(allocator, member_did) catch { 586 - return http_api.xrpcError(request, .bad_gateway, "DidResolutionFailed", "could not resolve member grant issuer did"); 585 + const public_key = signingKeyForDid(allocator, requester_did) catch { 586 + return http_api.xrpcError(request, .bad_gateway, "DidResolutionFailed", "could not resolve delegation token issuer did"); 587 587 }; 588 - const grant = permissioned.verifyMemberGrant(allocator, token, public_key) catch { 589 - return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid member grant"); 588 + const delegation = permissioned.verifyDelegationToken(allocator, token, public_key) catch { 589 + return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid delegation token"); 590 590 }; 591 591 const parsed_body = parseBody(request, allocator, 32 * 1024) catch |err| switch (err) { 592 592 error.HandledResponse => return, ··· 594 594 }; 595 595 const input = parsed_body.value; 596 596 const space = zat.json.getString(input, "space") orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing space"); 597 - if (!std.mem.eql(u8, grant.space, space)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Grant space mismatch"); 597 + if (!std.mem.eql(u8, delegation.space, space)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Delegation token space mismatch"); 598 598 const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); 599 - if (!std.mem.eql(u8, grant.owner_did, parsed.did)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Grant owner mismatch"); 599 + if (!std.mem.eql(u8, delegation.authority_did, parsed.did)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Delegation token authority mismatch"); 600 600 const config_row = (try store.getSpace(allocator, parsed.did, space)) orelse return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); 601 601 if (config_row.deleted_at != null) { 602 602 return http_api.xrpcError(request, .bad_request, "SpaceDeleted", "Space has been deleted"); 603 603 } 604 - if (!config_row.is_public and !std.mem.eql(u8, grant.member_did, parsed.did)) { 604 + if (!config_row.is_public and !std.mem.eql(u8, delegation.requester_did, parsed.did)) { 605 605 return http_api.xrpcError(request, .forbidden, "NotPermitted", "The space host did not grant access to this requester"); 606 606 } 607 - if (!spaceAllowsClient(allocator, config_row, grant.client_id)) { 607 + if (!spaceAllowsClient(allocator, config_row, delegation.client_id)) { 608 608 return http_api.xrpcError(request, .forbidden, "AppNotPermitted", "OAuth client is not allowed for this space"); 609 609 } 610 - var keypair = store.signingKeypair(parsed.did) catch return http_api.xrpcError(request, .not_found, "RepoNotFound", "Owner signing key not found"); 611 - const credential = try permissioned.createSpaceCredential(allocator, store.currentIo(), parsed.did, space, grant.client_id, &keypair); 610 + var keypair = store.signingKeypair(parsed.did) catch return http_api.xrpcError(request, .not_found, "RepoNotFound", "Authority signing key not found"); 611 + const credential = try permissioned.createSpaceCredential(allocator, store.currentIo(), parsed.did, space, delegation.client_id, &keypair); 612 612 if (zat.json.getString(input, "notifyEndpoint")) |endpoint| { 613 - try store.recordCredentialRecipient(space, grant.member_did, endpoint); 613 + try store.recordCredentialRecipient(space, delegation.requester_did, endpoint); 614 614 } 615 615 return http_api.json(request, .ok, try std.fmt.allocPrint(allocator, "{{\"credential\":{f}}}", .{std.json.fmt(credential, .{})})); 616 616 } ··· 631 631 const service = requireServiceAuth(request, allocator, "com.atproto.space.notifyWrite") catch return; 632 632 if (!std.mem.eql(u8, service.issuer_did, repo)) return http_api.xrpcError(request, .unauthorized, "InvalidToken", "JWT issuer must be the writer repo DID"); 633 633 if (!std.mem.eql(u8, service.audience, parsed.did)) return http_api.xrpcError(request, .unauthorized, "BadJwtAudience", "JWT audience must be the space DID"); 634 - const owner = (store.findAccount(allocator, parsed.did) catch null) orelse return http_api.json(request, .ok, "{}"); 635 - try fanoutNotifyWriteToRecipients(allocator, owner, space, repo, rev); 634 + const authority = (store.findAccount(allocator, parsed.did) catch null) orelse return http_api.json(request, .ok, "{}"); 635 + try fanoutNotifyWriteToRecipients(allocator, authority, space, repo, rev); 636 636 return http_api.json(request, .ok, "{}"); 637 637 } 638 638 ··· 935 935 return error.HandledResponse; 936 936 }; 937 937 if (!std.mem.eql(u8, issuer, parsed.did)) { 938 - try http_api.xrpcError(request, .unauthorized, "InvalidToken", "Space credential issuer must be the space owner"); 938 + try http_api.xrpcError(request, .unauthorized, "InvalidToken", "Space credential issuer must be the space authority"); 939 939 return error.HandledResponse; 940 940 } 941 941 const public_key = signingKeyForDid(allocator, issuer) catch { ··· 1077 1077 var out: std.Io.Writer.Allocating = .init(allocator); 1078 1078 defer out.deinit(); 1079 1079 try out.writer.print( 1080 - "{{\"uri\":{f},\"did\":{f},\"type\":{f},\"skey\":{f},\"isOwner\":{},\"isPublic\":{},\"appAccessMode\":{f}", 1080 + "{{\"uri\":{f},\"authority\":{f},\"type\":{f},\"skey\":{f},\"isAuthority\":{},\"isPublic\":{},\"appAccessMode\":{f}", 1081 1081 .{ 1082 1082 std.json.fmt(space.uri, .{}), 1083 - std.json.fmt(space.owner_did, .{}), 1083 + std.json.fmt(space.authority_did, .{}), 1084 1084 std.json.fmt(space.space_type, .{}), 1085 1085 std.json.fmt(space.skey, .{}), 1086 - space.is_owner, 1086 + space.is_authority, 1087 1087 space.is_public, 1088 1088 std.json.fmt(space.app_access_mode, .{}), 1089 1089 }, ··· 1095 1095 return out.toOwnedSlice(); 1096 1096 } 1097 1097 1098 - fn ownerSpaceJson(allocator: std.mem.Allocator, space: store.SpaceConfig) ![]const u8 { 1098 + fn authoritySpaceJson(allocator: std.mem.Allocator, space: store.SpaceConfig) ![]const u8 { 1099 1099 var out: std.Io.Writer.Allocating = .init(allocator); 1100 1100 defer out.deinit(); 1101 1101 try out.writer.print( 1102 - "{{\"uri\":{f},\"isOwner\":{},\"isPublic\":{},\"appAccessMode\":{f}", 1102 + "{{\"uri\":{f},\"isAuthority\":{},\"isPublic\":{},\"appAccessMode\":{f}", 1103 1103 .{ 1104 1104 std.json.fmt(space.uri, .{}), 1105 - space.is_owner, 1105 + space.is_authority, 1106 1106 space.is_public, 1107 1107 std.json.fmt(space.app_access_mode, .{}), 1108 1108 },
+7 -7
src/http/router.zig
··· 213 213 .{ .route = .identity_submit_plc_operation, .method = "POST", .path = "/xrpc/com.atproto.identity.submitPlcOperation", .group = "identity", .auth = "bearer", .summary = "Submit a PLC operation." }, 214 214 .{ .route = .identity_resolve_handle, .method = "GET", .path = "/xrpc/com.atproto.identity.resolveHandle", .group = "identity", .auth = "public", .summary = "Resolve a handle to a DID.", .params = &.{"handle"} }, 215 215 216 - .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.createSpace", .group = "space", .auth = "experimental bearer", .summary = "Create a permissioned data space.", .body = &.{ "did", "type", "skey", "managingApp", "isPublic", "appAccessMode", "appExceptions" }, .notes = permissioned_data_note }, 216 + .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.createSpace", .group = "space", .auth = "experimental bearer", .summary = "Create a permissioned data space.", .body = &.{ "authority", "type", "skey", "managingApp", "isPublic", "appAccessMode", "appExceptions" }, .notes = permissioned_data_note }, 217 217 .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.getSpace", .group = "space", .auth = "experimental bearer", .summary = "Read permissioned data space configuration.", .params = &.{"space"}, .notes = permissioned_data_note }, 218 - .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.listSpaces", .group = "space", .auth = "experimental bearer", .summary = "List spaces the authenticated user participates in.", .params = &.{ "did", "type", "limit", "cursor" }, .notes = permissioned_data_note }, 218 + .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.listSpaces", .group = "space", .auth = "experimental bearer", .summary = "List spaces the authenticated user participates in.", .params = &.{ "authority", "type", "limit", "cursor" }, .notes = permissioned_data_note }, 219 219 .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.updateSpaceConfig", .group = "space", .auth = "experimental bearer", .summary = "Update permissioned data space configuration.", .body = &.{ "space", "managingApp", "isPublic", "appAccessMode", "appExceptions" }, .notes = permissioned_data_note }, 220 220 .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.deleteSpace", .group = "space", .auth = "experimental bearer", .summary = "Tombstone a permissioned data space.", .body = &.{"space"}, .notes = permissioned_data_note }, 221 221 222 - .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.getMemberGrant", .group = "space", .auth = "experimental OAuth", .summary = "Create a member grant for exchange with a space owner.", .params = &.{"space"}, .notes = permissioned_data_note }, 222 + .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.getDelegationToken", .group = "space", .auth = "experimental OAuth", .summary = "Create a delegation token for exchange with a space authority.", .params = &.{"space"}, .notes = permissioned_data_note }, 223 223 224 224 .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.createRecord", .group = "space", .auth = "experimental bearer", .summary = "Create a record inside a permissioned data space.", .body = &.{ "space", "repo", "collection", "rkey", "validate", "record" }, .notes = permissioned_data_note }, 225 225 .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.putRecord", .group = "space", .auth = "experimental bearer", .summary = "Create or update a record inside a permissioned data space.", .body = &.{ "space", "repo", "collection", "rkey", "validate", "record" }, .notes = permissioned_data_note }, ··· 229 229 .{ .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 }, 230 230 .{ .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 }, 231 231 .{ .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 }, 232 - .{ .route = .permissioned_data, .method = "GET", .path = "/xrpc/com.atproto.space.getRepoOplog", .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 }, 233 - .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.notifyWrite", .group = "space", .auth = "experimental service", .summary = "Notify a space owner or syncing service of a permissioned data write.", .body = &.{ "space", "repo", "rev" }, .notes = permissioned_data_note }, 232 + .{ .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 }, 233 + .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.notifyWrite", .group = "space", .auth = "experimental service", .summary = "Notify a space authority or syncing service of a permissioned data write.", .body = &.{ "space", "repo", "rev" }, .notes = permissioned_data_note }, 234 234 235 - .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.getSpaceCredential", .group = "space", .auth = "experimental member grant", .summary = "Exchange a member grant for a space credential.", .body = &.{ "space", "notifyEndpoint" }, .notes = permissioned_data_note }, 236 - .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.notifySpaceDeleted", .group = "space", .auth = "experimental service", .summary = "Notify a member PDS or syncing service that a space was deleted.", .body = &.{"space"}, .notes = permissioned_data_note }, 235 + .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.getSpaceCredential", .group = "space", .auth = "experimental delegation token", .summary = "Exchange a delegation token for a space credential.", .body = &.{ "space", "notifyEndpoint" }, .notes = permissioned_data_note }, 236 + .{ .route = .permissioned_data, .method = "POST", .path = "/xrpc/com.atproto.space.notifySpaceDeleted", .group = "space", .auth = "experimental service", .summary = "Notify a repo host or syncing service that a space was deleted.", .body = &.{"space"}, .notes = permissioned_data_note }, 237 237 }; 238 238 239 239 pub fn route(method: httpz.Method, target: []const u8) Route {
+56 -46
src/internal/permissioned_data.zig
··· 1 1 //! Experimental permissioned-data primitives for `com.atproto.space.*`. 2 2 //! 3 3 //! Keep protocol mechanics here so the experimental LtHash, signed-commit, 4 - //! member-grant, and space-credential code does not sprawl through stable PDS 4 + //! delegation-token, and space-credential code does not sprawl through stable PDS 5 5 //! auth, repo, or sync modules. 6 6 7 7 const std = @import("std"); ··· 89 89 rev: []const u8, 90 90 }; 91 91 92 - pub const MemberGrant = struct { 93 - member_did: []const u8, 94 - owner_did: []const u8, 92 + pub const DelegationToken = struct { 93 + requester_did: []const u8, 94 + authority_did: []const u8, 95 95 space: []const u8, 96 96 client_id: []const u8, 97 97 exp: i64, 98 98 }; 99 99 100 100 pub const SpaceCredential = struct { 101 - owner_did: []const u8, 101 + authority_did: []const u8, 102 102 space: []const u8, 103 103 client_id: []const u8, 104 104 exp: i64, ··· 145 145 ); 146 146 } 147 147 148 - pub fn createMemberGrant( 148 + pub fn createDelegationToken( 149 149 allocator: std.mem.Allocator, 150 150 io: std.Io, 151 - member_did: []const u8, 152 - owner_did: []const u8, 151 + requester_did: []const u8, 152 + authority_did: []const u8, 153 153 space: []const u8, 154 154 client_id: []const u8, 155 155 keypair: *const zat.Keypair, 156 156 ) ![]const u8 { 157 157 const iat = unixNow(); 158 - const exp = iat + 300; 158 + const exp = iat + 60; 159 159 const jti = try randomTokenId(allocator, io); 160 160 defer allocator.free(jti); 161 - const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"space_member_grant\",\"alg\":\"{s}\"}}", .{@tagName(keypair.algorithm())}); 161 + const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"atproto-space-delegation+jwt\",\"alg\":\"{s}\",\"kid\":\"#atproto\"}}", .{@tagName(keypair.algorithm())}); 162 162 defer allocator.free(header); 163 + const audience = try std.fmt.allocPrint(allocator, "{s}#atproto_space_host", .{authority_did}); 164 + defer allocator.free(audience); 163 165 const payload = try std.fmt.allocPrint( 164 166 allocator, 165 - "{{\"iss\":{f},\"aud\":{f},\"space\":{f},\"clientId\":{f},\"lxm\":\"com.atproto.space.getSpaceCredential\",\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", 166 - .{ std.json.fmt(member_did, .{}), std.json.fmt(owner_did, .{}), std.json.fmt(space, .{}), std.json.fmt(client_id, .{}), iat, exp, std.json.fmt(jti, .{}) }, 167 + "{{\"iss\":{f},\"aud\":{f},\"sub\":{f},\"client_id\":{f},\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", 168 + .{ std.json.fmt(requester_did, .{}), std.json.fmt(audience, .{}), std.json.fmt(space, .{}), std.json.fmt(client_id, .{}), iat, exp, std.json.fmt(jti, .{}) }, 167 169 ); 168 170 defer allocator.free(payload); 169 171 return zat.oauth.createJwt(allocator, header, payload, keypair); ··· 172 174 pub fn createSpaceCredential( 173 175 allocator: std.mem.Allocator, 174 176 io: std.Io, 175 - owner_did: []const u8, 177 + authority_did: []const u8, 176 178 space: []const u8, 177 179 client_id: []const u8, 178 180 keypair: *const zat.Keypair, ··· 181 183 const exp = iat + 7200; 182 184 const jti = try randomTokenId(allocator, io); 183 185 defer allocator.free(jti); 184 - const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"space_credential\",\"alg\":\"{s}\"}}", .{@tagName(keypair.algorithm())}); 186 + const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"atproto-space-credential+jwt\",\"alg\":\"{s}\",\"kid\":\"#atproto_space\"}}", .{@tagName(keypair.algorithm())}); 185 187 defer allocator.free(header); 186 188 const payload = try std.fmt.allocPrint( 187 189 allocator, 188 - "{{\"iss\":{f},\"space\":{f},\"clientId\":{f},\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", 189 - .{ std.json.fmt(owner_did, .{}), std.json.fmt(space, .{}), std.json.fmt(client_id, .{}), iat, exp, std.json.fmt(jti, .{}) }, 190 + "{{\"iss\":{f},\"sub\":{f},\"client_id\":{f},\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", 191 + .{ std.json.fmt(authority_did, .{}), std.json.fmt(space, .{}), std.json.fmt(client_id, .{}), iat, exp, std.json.fmt(jti, .{}) }, 190 192 ); 191 193 defer allocator.free(payload); 192 194 return zat.oauth.createJwt(allocator, header, payload, keypair); 193 195 } 194 196 195 - pub fn verifyMemberGrant(allocator: std.mem.Allocator, token: []const u8, public_key_multibase: []const u8) !MemberGrant { 196 - const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "space_member_grant"); 197 + pub fn verifyDelegationToken(allocator: std.mem.Allocator, token: []const u8, public_key_multibase: []const u8) !DelegationToken { 198 + const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "atproto-space-delegation+jwt"); 197 199 defer parsed.deinit(); 198 - const lxm = zat.json.getString(parsed.payload.value, "lxm") orelse return error.InvalidJwt; 199 - if (!std.mem.eql(u8, lxm, "com.atproto.space.getSpaceCredential")) return error.InvalidJwt; 200 200 const exp = zat.json.getInt(parsed.payload.value, "exp") orelse return error.InvalidJwt; 201 201 if (exp < unixNow()) return error.ExpiredJwt; 202 + const audience = zat.json.getString(parsed.payload.value, "aud") orelse return error.InvalidJwt; 203 + const authority_did = authorityDidFromAudience(audience) orelse return error.InvalidJwt; 202 204 return .{ 203 - .member_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), 204 - .owner_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "aud") orelse return error.InvalidJwt), 205 - .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "space") orelse return error.InvalidJwt), 206 - .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "clientId") orelse return error.InvalidJwt), 205 + .requester_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), 206 + .authority_did = try allocator.dupe(u8, authority_did), 207 + .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "sub") orelse return error.InvalidJwt), 208 + .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "client_id") orelse return error.InvalidJwt), 207 209 .exp = exp, 208 210 }; 209 211 } 210 212 211 213 pub fn verifySpaceCredential(allocator: std.mem.Allocator, token: []const u8, public_key_multibase: []const u8) !SpaceCredential { 212 - const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "space_credential"); 214 + const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "atproto-space-credential+jwt"); 213 215 defer parsed.deinit(); 214 216 const exp = zat.json.getInt(parsed.payload.value, "exp") orelse return error.InvalidJwt; 215 217 if (exp < unixNow()) return error.ExpiredJwt; 216 218 return .{ 217 - .owner_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), 218 - .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "space") orelse return error.InvalidJwt), 219 - .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "clientId") orelse return error.InvalidJwt), 219 + .authority_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), 220 + .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "sub") orelse return error.InvalidJwt), 221 + .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "client_id") orelse return error.InvalidJwt), 220 222 .exp = exp, 221 223 }; 224 + } 225 + 226 + fn authorityDidFromAudience(audience: []const u8) ?[]const u8 { 227 + const suffix = "#atproto_space_host"; 228 + if (!std.mem.endsWith(u8, audience, suffix)) return null; 229 + const authority_did = audience[0 .. audience.len - suffix.len]; 230 + if (zat.Did.parse(authority_did) == null) return null; 231 + return authority_did; 222 232 } 223 233 224 234 pub fn unverifiedStringClaim(allocator: std.mem.Allocator, token: []const u8, claim: []const u8) ![]const u8 { ··· 375 385 try std.testing.expectEqualStrings(expected, &std.fmt.bytesToHex(hash.bytes, .lower)); 376 386 } 377 387 378 - test "member grant and space credential round trip" { 388 + test "delegation token and space credential round trip" { 379 389 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 380 390 defer arena.deinit(); 381 391 const allocator = arena.allocator(); ··· 389 399 const did_key = try keypair.did(allocator); 390 400 const public_key_multibase = did_key["did:key:".len..]; 391 401 392 - const grant_token = try createMemberGrant( 402 + const delegation_token = try createDelegationToken( 393 403 allocator, 394 404 std.Options.debug_io, 395 - "did:plc:membergrant", 396 - "did:plc:spaceowner", 397 - "ats://did:plc:spaceowner/fm.plyr.privateMedia/self", 405 + "did:plc:requester", 406 + "did:plc:spaceauthority", 407 + "ats://did:plc:spaceauthority/fm.plyr.privateMedia/self", 398 408 "https://plyr.fm/oauth-client.json", 399 409 &keypair, 400 410 ); 401 - const grant = try verifyMemberGrant(allocator, grant_token, public_key_multibase); 402 - try std.testing.expectEqualStrings("did:plc:membergrant", grant.member_did); 403 - try std.testing.expectEqualStrings("did:plc:spaceowner", grant.owner_did); 404 - try std.testing.expectEqualStrings("ats://did:plc:spaceowner/fm.plyr.privateMedia/self", grant.space); 405 - try std.testing.expectEqualStrings("https://plyr.fm/oauth-client.json", grant.client_id); 411 + const delegation = try verifyDelegationToken(allocator, delegation_token, public_key_multibase); 412 + try std.testing.expectEqualStrings("did:plc:requester", delegation.requester_did); 413 + try std.testing.expectEqualStrings("did:plc:spaceauthority", delegation.authority_did); 414 + try std.testing.expectEqualStrings("ats://did:plc:spaceauthority/fm.plyr.privateMedia/self", delegation.space); 415 + try std.testing.expectEqualStrings("https://plyr.fm/oauth-client.json", delegation.client_id); 406 416 407 417 const credential_token = try createSpaceCredential( 408 418 allocator, 409 419 std.Options.debug_io, 410 - "did:plc:spaceowner", 411 - grant.space, 412 - grant.client_id, 420 + "did:plc:spaceauthority", 421 + delegation.space, 422 + delegation.client_id, 413 423 &keypair, 414 424 ); 415 425 const credential = try verifySpaceCredential(allocator, credential_token, public_key_multibase); 416 - try std.testing.expectEqualStrings("did:plc:spaceowner", credential.owner_did); 417 - try std.testing.expectEqualStrings(grant.space, credential.space); 418 - try std.testing.expectEqualStrings(grant.client_id, credential.client_id); 426 + try std.testing.expectEqualStrings("did:plc:spaceauthority", credential.authority_did); 427 + try std.testing.expectEqualStrings(delegation.space, credential.space); 428 + try std.testing.expectEqualStrings(delegation.client_id, credential.client_id); 419 429 420 - try std.testing.expectError(error.InvalidJwt, verifySpaceCredential(allocator, grant_token, public_key_multibase)); 421 - try std.testing.expectError(error.InvalidJwt, verifyMemberGrant(allocator, credential_token, public_key_multibase)); 430 + try std.testing.expectError(error.InvalidJwt, verifySpaceCredential(allocator, delegation_token, public_key_multibase)); 431 + try std.testing.expectError(error.InvalidJwt, verifyDelegationToken(allocator, credential_token, public_key_multibase)); 422 432 }
+126 -87
src/storage/store.zig
··· 298 298 299 299 pub const SpaceConfig = struct { 300 300 uri: []const u8, 301 - owner_did: []const u8, 301 + authority_did: []const u8, 302 302 space_type: []const u8, 303 303 skey: []const u8, 304 304 managing_app: ?[]const u8, 305 305 is_public: bool, 306 306 app_access_mode: []const u8, 307 307 app_exceptions_json: []const u8, 308 - is_owner: bool, 308 + is_authority: bool, 309 309 deleted_at: ?i64, 310 310 }; 311 311 ··· 3045 3045 3046 3046 pub const CreateSpaceInput = struct { 3047 3047 actor_did: []const u8, 3048 - owner_did: []const u8, 3048 + authority_did: []const u8, 3049 3049 space_type: []const u8, 3050 3050 skey: []const u8, 3051 - is_owner: bool, 3051 + is_authority: bool, 3052 3052 managing_app: ?[]const u8, 3053 3053 is_public: bool, 3054 3054 app_access_mode: []const u8, ··· 3057 3057 3058 3058 pub fn createSpace(allocator: std.mem.Allocator, input: CreateSpaceInput) !SpaceConfig { 3059 3059 if (zat.Did.parse(input.actor_did) == null) return Error.InvalidRepoPath; 3060 - if (zat.Did.parse(input.owner_did) == null) return Error.InvalidRepoPath; 3060 + if (zat.Did.parse(input.authority_did) == null) return Error.InvalidRepoPath; 3061 3061 if (zat.Nsid.parse(input.space_type) == null) return Error.InvalidCollection; 3062 3062 if (zat.Rkey.parse(input.skey) == null) return Error.InvalidRecordKey; 3063 3063 if (!std.mem.eql(u8, input.app_access_mode, "allow") and !std.mem.eql(u8, input.app_access_mode, "deny")) { 3064 3064 return Error.InvalidRecordType; 3065 3065 } 3066 3066 3067 - const uri = try std.fmt.allocPrint(allocator, "ats://{s}/{s}/{s}", .{ input.owner_did, input.space_type, input.skey }); 3067 + const uri = try std.fmt.allocPrint(allocator, "ats://{s}/{s}/{s}", .{ input.authority_did, input.space_type, input.skey }); 3068 3068 3069 3069 db_mutex.lockUncancelable(store_io); 3070 3070 defer db_mutex.unlock(store_io); ··· 3076 3076 errdefer conn.rollback(); 3077 3077 try conn.exec( 3078 3078 \\INSERT INTO permissioned_spaces ( 3079 - \\ uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json 3079 + \\ uri, authority_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json 3080 3080 \\) VALUES (?, ?, ?, ?, ?, ?, ?, ?) 3081 3081 \\ON CONFLICT(uri) DO NOTHING 3082 3082 , .{ 3083 3083 uri, 3084 - input.owner_did, 3084 + input.authority_did, 3085 3085 input.space_type, 3086 3086 input.skey, 3087 3087 input.managing_app, ··· 3089 3089 input.app_access_mode, 3090 3090 input.app_exceptions_json, 3091 3091 }); 3092 - if (input.is_owner) { 3092 + if (input.is_authority) { 3093 3093 try conn.exec( 3094 3094 \\UPDATE permissioned_spaces 3095 3095 \\SET managing_app = ?, ··· 3109 3109 \\INSERT INTO permissioned_space_repos (space, repo_did, set_hash, rev) 3110 3110 \\VALUES (?, ?, NULL, NULL) 3111 3111 \\ON CONFLICT(space, repo_did) DO NOTHING 3112 - , .{ uri, input.owner_did }); 3112 + , .{ uri, input.authority_did }); 3113 3113 try conn.exec( 3114 - \\INSERT INTO permissioned_space_actor_state (space, actor_did, is_owner) 3114 + \\INSERT INTO permissioned_space_actor_state (space, actor_did, is_authority) 3115 3115 \\VALUES (?, ?, ?) 3116 - , .{ uri, input.actor_did, @as(i64, if (input.is_owner) 1 else 0) }); 3116 + , .{ uri, input.actor_did, @as(i64, if (input.is_authority) 1 else 0) }); 3117 3117 try conn.commit(); 3118 3118 3119 3119 return (try getSpaceLocked(allocator, input.actor_did, uri)) orelse Error.RepoNotFound; ··· 3146 3146 \\SELECT s.uri 3147 3147 \\FROM permissioned_spaces s 3148 3148 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3149 - \\WHERE a.actor_did = ? AND s.owner_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_owner = 1 3149 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_authority = 1 3150 3150 \\ORDER BY s.uri ASC 3151 3151 \\LIMIT ? 3152 3152 , .{ actor_did, did, space_type, cursor, capped_limit }) ··· 3155 3155 \\SELECT s.uri 3156 3156 \\FROM permissioned_spaces s 3157 3157 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3158 - \\WHERE a.actor_did = ? AND s.owner_did = ? AND s.space_type = ? AND a.is_owner = 1 3158 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND s.space_type = ? AND a.is_authority = 1 3159 3159 \\ORDER BY s.uri ASC 3160 3160 \\LIMIT ? 3161 3161 , .{ actor_did, did, space_type, capped_limit }) ··· 3164 3164 \\SELECT s.uri 3165 3165 \\FROM permissioned_spaces s 3166 3166 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3167 - \\WHERE a.actor_did = ? AND s.owner_did = ? AND s.uri > ? AND a.is_owner = 1 3167 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND s.uri > ? AND a.is_authority = 1 3168 3168 \\ORDER BY s.uri ASC 3169 3169 \\LIMIT ? 3170 3170 , .{ actor_did, did, cursor, capped_limit }) ··· 3173 3173 \\SELECT s.uri 3174 3174 \\FROM permissioned_spaces s 3175 3175 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3176 - \\WHERE a.actor_did = ? AND s.owner_did = ? AND a.is_owner = 1 3176 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND a.is_authority = 1 3177 3177 \\ORDER BY s.uri ASC 3178 3178 \\LIMIT ? 3179 3179 , .{ actor_did, did, capped_limit }) ··· 3183 3183 \\SELECT s.uri 3184 3184 \\FROM permissioned_spaces s 3185 3185 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3186 - \\WHERE a.actor_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_owner = 1 3186 + \\WHERE a.actor_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_authority = 1 3187 3187 \\ORDER BY s.uri ASC 3188 3188 \\LIMIT ? 3189 3189 , .{ actor_did, space_type, cursor, capped_limit }) ··· 3192 3192 \\SELECT s.uri 3193 3193 \\FROM permissioned_spaces s 3194 3194 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3195 - \\WHERE a.actor_did = ? AND s.space_type = ? AND a.is_owner = 1 3195 + \\WHERE a.actor_did = ? AND s.space_type = ? AND a.is_authority = 1 3196 3196 \\ORDER BY s.uri ASC 3197 3197 \\LIMIT ? 3198 3198 , .{ actor_did, space_type, capped_limit }) ··· 3201 3201 \\SELECT s.uri 3202 3202 \\FROM permissioned_spaces s 3203 3203 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3204 - \\WHERE a.actor_did = ? AND s.uri > ? AND a.is_owner = 1 3204 + \\WHERE a.actor_did = ? AND s.uri > ? AND a.is_authority = 1 3205 3205 \\ORDER BY s.uri ASC 3206 3206 \\LIMIT ? 3207 3207 , .{ actor_did, cursor, capped_limit }) ··· 3210 3210 \\SELECT s.uri 3211 3211 \\FROM permissioned_spaces s 3212 3212 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3213 - \\WHERE a.actor_did = ? AND a.is_owner = 1 3213 + \\WHERE a.actor_did = ? AND a.is_authority = 1 3214 3214 \\ORDER BY s.uri ASC 3215 3215 \\LIMIT ? 3216 3216 , .{ actor_did, capped_limit }); ··· 3264 3264 try conn.exec("UPDATE permissioned_space_actor_state SET deleted_at = unixepoch() WHERE space = ? AND actor_did = ?", .{ space, actor_did }); 3265 3265 } 3266 3266 3267 - pub fn purgeOwnerSpaceData(space: []const u8) !void { 3267 + pub fn purgeAuthoritySpaceData(space: []const u8) !void { 3268 3268 db_mutex.lockUncancelable(store_io); 3269 3269 defer db_mutex.unlock(store_io); 3270 3270 try requireInitialized(); ··· 3621 3621 3622 3622 fn getSpaceConfigLocked(allocator: std.mem.Allocator, uri: []const u8) !?SpaceConfig { 3623 3623 const row = try conn.row( 3624 - \\SELECT uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json 3624 + \\SELECT uri, authority_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json 3625 3625 \\FROM permissioned_spaces 3626 3626 \\WHERE uri = ? 3627 3627 , .{uri}); ··· 3630 3630 3631 3631 return .{ 3632 3632 .uri = try allocator.dupe(u8, row.?.text(0)), 3633 - .owner_did = try allocator.dupe(u8, row.?.text(1)), 3633 + .authority_did = try allocator.dupe(u8, row.?.text(1)), 3634 3634 .space_type = try allocator.dupe(u8, row.?.text(2)), 3635 3635 .skey = try allocator.dupe(u8, row.?.text(3)), 3636 3636 .managing_app = if (row.?.nullableText(4)) |value| try allocator.dupe(u8, value) else null, 3637 3637 .is_public = row.?.int(5) != 0, 3638 3638 .app_access_mode = try allocator.dupe(u8, row.?.text(6)), 3639 3639 .app_exceptions_json = try allocator.dupe(u8, row.?.text(7)), 3640 - .is_owner = false, 3640 + .is_authority = false, 3641 3641 .deleted_at = null, 3642 3642 }; 3643 3643 } 3644 3644 3645 3645 fn getSpaceLocked(allocator: std.mem.Allocator, actor_did: []const u8, uri: []const u8) !?SpaceConfig { 3646 3646 const row = try conn.row( 3647 - \\SELECT s.uri, s.owner_did, s.space_type, s.skey, s.managing_app, s.is_public, s.app_access_mode, s.app_exceptions_json, 3648 - \\ a.is_owner, a.deleted_at 3647 + \\SELECT s.uri, s.authority_did, s.space_type, s.skey, s.managing_app, s.is_public, s.app_access_mode, s.app_exceptions_json, 3648 + \\ a.is_authority, a.deleted_at 3649 3649 \\FROM permissioned_spaces s 3650 3650 \\JOIN permissioned_space_actor_state a ON a.space = s.uri 3651 3651 \\WHERE s.uri = ? AND a.actor_did = ? ··· 3655 3655 3656 3656 return .{ 3657 3657 .uri = try allocator.dupe(u8, row.?.text(0)), 3658 - .owner_did = try allocator.dupe(u8, row.?.text(1)), 3658 + .authority_did = try allocator.dupe(u8, row.?.text(1)), 3659 3659 .space_type = try allocator.dupe(u8, row.?.text(2)), 3660 3660 .skey = try allocator.dupe(u8, row.?.text(3)), 3661 3661 .managing_app = if (row.?.nullableText(4)) |value| try allocator.dupe(u8, value) else null, 3662 3662 .is_public = row.?.int(5) != 0, 3663 3663 .app_access_mode = try allocator.dupe(u8, row.?.text(6)), 3664 3664 .app_exceptions_json = try allocator.dupe(u8, row.?.text(7)), 3665 - .is_owner = row.?.int(8) != 0, 3665 + .is_authority = row.?.int(8) != 0, 3666 3666 .deleted_at = if (row.?.nullableInt(9)) |value| value else null, 3667 3667 }; 3668 3668 } ··· 3701 3701 } 3702 3702 3703 3703 const SpaceParts = struct { 3704 - owner_did: []const u8, 3704 + authority_did: []const u8, 3705 3705 space_type: []const u8, 3706 3706 skey: []const u8, 3707 3707 }; ··· 3711 3711 if (!std.mem.startsWith(u8, uri, prefix)) return null; 3712 3712 const rest = uri[prefix.len..]; 3713 3713 const first = std.mem.indexOfScalar(u8, rest, '/') orelse return null; 3714 - const owner_did = rest[0..first]; 3715 - if (zat.Did.parse(owner_did) == null) return null; 3714 + const authority_did = rest[0..first]; 3715 + if (zat.Did.parse(authority_did) == null) return null; 3716 3716 const after_did = rest[first + 1 ..]; 3717 3717 const second = std.mem.indexOfScalar(u8, after_did, '/') orelse return null; 3718 3718 const space_type = after_did[0..second]; 3719 3719 if (zat.Nsid.parse(space_type) == null) return null; 3720 3720 const skey = after_did[second + 1 ..]; 3721 3721 if (zat.Rkey.parse(skey) == null) return null; 3722 - return .{ .owner_did = owner_did, .space_type = space_type, .skey = skey }; 3722 + return .{ .authority_did = authority_did, .space_type = space_type, .skey = skey }; 3723 3723 } 3724 3724 3725 3725 fn migrate() !void { ··· 3769 3769 3770 3770 fn migratePermissionedDataTables() !void { 3771 3771 try ensureMigrationTable(); 3772 - try migratePermissionedSpacesOwnerFk(); 3773 - try migratePermissionedSpaceActorState(); 3772 + try migratePermissionedSpacesAuthorityColumns(); 3773 + try migratePermissionedSpaceAuthorityFlag(); 3774 3774 try migratePermissionedRecordOplogRepo(); 3775 - try migratePermissionedDropMemberLists(); 3776 3775 } 3777 3776 3778 3777 fn ensureMigrationTable() !void { ··· 3794 3793 return false; 3795 3794 } 3796 3795 3796 + fn permissionedSpacesColumnExistsLocked(column: []const u8) !bool { 3797 + var rows = try conn.rows("PRAGMA table_info(permissioned_spaces)", .{}); 3798 + defer rows.deinit(); 3799 + while (rows.next()) |row| { 3800 + if (std.mem.eql(u8, row.text(1), column)) return true; 3801 + } 3802 + if (rows.err) |err| return err; 3803 + return false; 3804 + } 3805 + 3806 + fn permissionedSpaceActorStateColumnExistsLocked(column: []const u8) !bool { 3807 + var rows = try conn.rows("PRAGMA table_info(permissioned_space_actor_state)", .{}); 3808 + defer rows.deinit(); 3809 + while (rows.next()) |row| { 3810 + if (std.mem.eql(u8, row.text(1), column)) return true; 3811 + } 3812 + if (rows.err) |err| return err; 3813 + return false; 3814 + } 3815 + 3797 3816 fn migrationApplied(name: []const u8) !bool { 3798 3817 const row = try conn.row("SELECT 1 FROM zds_migrations WHERE name = ?", .{name}); 3799 3818 if (row == null) return false; ··· 3805 3824 try conn.exec("INSERT OR IGNORE INTO zds_migrations (name) VALUES (?)", .{name}); 3806 3825 } 3807 3826 3808 - fn migratePermissionedSpacesOwnerFk() !void { 3809 - const name = "permissioned-spaces-owner-fk"; 3827 + fn migratePermissionedSpacesAuthorityColumns() !void { 3828 + const name = "permissioned-spaces-authority-columns"; 3810 3829 if (try migrationApplied(name)) return; 3830 + if (!try permissionedSpacesColumnExistsLocked("owner_did")) { 3831 + try markMigrationApplied(name); 3832 + return; 3833 + } 3811 3834 try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_spaces_next"); 3812 3835 try conn.execNoArgs( 3813 3836 \\CREATE TABLE permissioned_spaces_next ( 3814 3837 \\ uri TEXT PRIMARY KEY, 3815 - \\ owner_did TEXT NOT NULL, 3838 + \\ authority_did TEXT NOT NULL, 3816 3839 \\ space_type TEXT NOT NULL, 3817 3840 \\ skey TEXT NOT NULL, 3818 3841 \\ managing_app TEXT, 3819 3842 \\ is_public INTEGER NOT NULL DEFAULT 0, 3820 3843 \\ app_access_mode TEXT NOT NULL DEFAULT 'allow', 3821 3844 \\ app_exceptions_json TEXT NOT NULL DEFAULT '[]', 3822 - \\ is_owner INTEGER NOT NULL DEFAULT 1, 3845 + \\ is_authority INTEGER NOT NULL DEFAULT 1, 3823 3846 \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), 3824 3847 \\ deleted_at INTEGER, 3825 - \\ UNIQUE(owner_did, space_type, skey) 3848 + \\ UNIQUE(authority_did, space_type, skey) 3826 3849 \\) 3827 3850 ); 3828 3851 try conn.execNoArgs( 3829 3852 \\INSERT INTO permissioned_spaces_next ( 3830 - \\ uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, 3831 - \\ app_exceptions_json, is_owner, created_at, deleted_at 3853 + \\ uri, authority_did, space_type, skey, managing_app, is_public, app_access_mode, 3854 + \\ app_exceptions_json, is_authority, created_at, deleted_at 3832 3855 \\) 3833 3856 \\SELECT uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, 3834 3857 \\ app_exceptions_json, is_owner, created_at, deleted_at ··· 3839 3862 try conn.execNoArgs("DROP TABLE permissioned_spaces"); 3840 3863 try conn.execNoArgs("ALTER TABLE permissioned_spaces_next RENAME TO permissioned_spaces"); 3841 3864 try conn.execNoArgs("PRAGMA foreign_keys = ON"); 3842 - try conn.execNoArgs("CREATE INDEX IF NOT EXISTS permissioned_spaces_owner_idx ON permissioned_spaces (owner_did, space_type, uri)"); 3865 + try conn.execNoArgs("CREATE INDEX IF NOT EXISTS permissioned_spaces_authority_idx ON permissioned_spaces (authority_did, space_type, uri)"); 3843 3866 try markMigrationApplied(name); 3844 3867 } 3845 3868 3846 - fn migratePermissionedSpaceActorState() !void { 3847 - const name = "permissioned-space-actor-state"; 3869 + fn migratePermissionedSpaceAuthorityFlag() !void { 3870 + const name = "permissioned-space-authority-flag"; 3848 3871 if (try migrationApplied(name)) return; 3872 + if (try permissionedSpaceActorStateColumnExistsLocked("is_owner")) { 3873 + try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_actor_state_next"); 3874 + try conn.execNoArgs( 3875 + \\CREATE TABLE permissioned_space_actor_state_next ( 3876 + \\ space TEXT NOT NULL REFERENCES permissioned_spaces(uri) ON DELETE CASCADE, 3877 + \\ actor_did TEXT NOT NULL, 3878 + \\ is_authority INTEGER NOT NULL DEFAULT 0, 3879 + \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), 3880 + \\ deleted_at INTEGER, 3881 + \\ PRIMARY KEY (space, actor_did) 3882 + \\) 3883 + ); 3884 + try conn.execNoArgs( 3885 + \\INSERT INTO permissioned_space_actor_state_next ( 3886 + \\ space, actor_did, is_authority, created_at, deleted_at 3887 + \\) 3888 + \\SELECT space, actor_did, is_owner, created_at, deleted_at 3889 + \\FROM permissioned_space_actor_state 3890 + ); 3891 + try conn.execNoArgs("PRAGMA foreign_keys = OFF"); 3892 + errdefer conn.execNoArgs("PRAGMA foreign_keys = ON") catch {}; 3893 + try conn.execNoArgs("DROP TABLE permissioned_space_actor_state"); 3894 + try conn.execNoArgs("ALTER TABLE permissioned_space_actor_state_next RENAME TO permissioned_space_actor_state"); 3895 + try conn.execNoArgs("PRAGMA foreign_keys = ON"); 3896 + } 3849 3897 try conn.execNoArgs( 3850 - \\INSERT OR IGNORE INTO permissioned_space_actor_state (space, actor_did, is_owner, deleted_at) 3898 + \\INSERT OR IGNORE INTO permissioned_space_actor_state (space, actor_did, is_authority, deleted_at) 3851 3899 \\SELECT s.uri, 3852 - \\ CASE WHEN s.is_owner = 1 THEN s.owner_did ELSE COALESCE(r.repo_did, s.owner_did) END, 3853 - \\ s.is_owner, 3900 + \\ CASE WHEN s.is_authority = 1 THEN s.authority_did ELSE COALESCE(r.repo_did, s.authority_did) END, 3901 + \\ s.is_authority, 3854 3902 \\ s.deleted_at 3855 3903 \\FROM permissioned_spaces s 3856 3904 \\LEFT JOIN permissioned_space_repos r ON r.space = s.uri 3857 - \\WHERE s.is_owner = 1 3905 + \\WHERE s.is_authority = 1 3858 3906 ); 3859 3907 try markMigrationApplied(name); 3860 3908 } ··· 3886 3934 ); 3887 3935 try conn.execNoArgs("DROP TABLE permissioned_space_record_oplog"); 3888 3936 try conn.execNoArgs("ALTER TABLE permissioned_space_record_oplog_next RENAME TO permissioned_space_record_oplog"); 3889 - try markMigrationApplied(name); 3890 - } 3891 - 3892 - fn migratePermissionedDropMemberLists() !void { 3893 - const name = "permissioned-drop-member-lists"; 3894 - if (try migrationApplied(name)) return; 3895 - try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_members"); 3896 - try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_member_state"); 3897 - try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_member_oplog"); 3898 3937 try markMigrationApplied(name); 3899 3938 } 3900 3939 ··· 5540 5579 "CREATE INDEX IF NOT EXISTS reserved_signing_keys_expires_idx ON reserved_signing_keys (expires_at) WHERE used_at IS NULL", 5541 5580 \\CREATE TABLE IF NOT EXISTS permissioned_spaces ( 5542 5581 \\ uri TEXT PRIMARY KEY, 5543 - \\ owner_did TEXT NOT NULL, 5582 + \\ authority_did TEXT NOT NULL, 5544 5583 \\ space_type TEXT NOT NULL, 5545 5584 \\ skey TEXT NOT NULL, 5546 5585 \\ managing_app TEXT, 5547 5586 \\ is_public INTEGER NOT NULL DEFAULT 0, 5548 5587 \\ app_access_mode TEXT NOT NULL DEFAULT 'allow', 5549 5588 \\ app_exceptions_json TEXT NOT NULL DEFAULT '[]', 5550 - \\ is_owner INTEGER NOT NULL DEFAULT 1, 5589 + \\ is_authority INTEGER NOT NULL DEFAULT 1, 5551 5590 \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), 5552 5591 \\ deleted_at INTEGER, 5553 - \\ UNIQUE(owner_did, space_type, skey) 5592 + \\ UNIQUE(authority_did, space_type, skey) 5554 5593 \\) 5555 5594 , 5556 - "CREATE INDEX IF NOT EXISTS permissioned_spaces_owner_idx ON permissioned_spaces (owner_did, space_type, uri)", 5595 + "CREATE INDEX IF NOT EXISTS permissioned_spaces_authority_idx ON permissioned_spaces (authority_did, space_type, uri)", 5557 5596 \\CREATE TABLE IF NOT EXISTS permissioned_space_actor_state ( 5558 5597 \\ space TEXT NOT NULL REFERENCES permissioned_spaces(uri) ON DELETE CASCADE, 5559 5598 \\ actor_did TEXT NOT NULL, 5560 - \\ is_owner INTEGER NOT NULL DEFAULT 0, 5599 + \\ is_authority INTEGER NOT NULL DEFAULT 0, 5561 5600 \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), 5562 5601 \\ deleted_at INTEGER, 5563 5602 \\ PRIMARY KEY (space, actor_did) ··· 5982 6021 "space.test", 5983 6022 "space@test.com", 5984 6023 "password", 5985 - "did:plc:spaceowneralice", 6024 + "did:plc:spaceauthorityalice", 5986 6025 true, 5987 6026 ); 5988 6027 5989 6028 const space = try createSpace(allocator, .{ 5990 6029 .actor_did = account.did, 5991 - .owner_did = account.did, 6030 + .authority_did = account.did, 5992 6031 .space_type = "fm.plyr.privateMedia", 5993 6032 .skey = "self", 5994 - .is_owner = true, 6033 + .is_authority = true, 5995 6034 .managing_app = "did:web:plyr.fm", 5996 6035 .is_public = false, 5997 6036 .app_access_mode = "allow", 5998 6037 .app_exceptions_json = "[]", 5999 6038 }); 6000 - try std.testing.expectEqualStrings("ats://did:plc:spaceowneralice/fm.plyr.privateMedia/self", space.uri); 6001 - try std.testing.expect(space.is_owner); 6039 + try std.testing.expectEqualStrings("ats://did:plc:spaceauthorityalice/fm.plyr.privateMedia/self", space.uri); 6040 + try std.testing.expect(space.is_authority); 6002 6041 6003 6042 try std.testing.expectError(Error.InvalidRefreshSession, createSpace(allocator, .{ 6004 6043 .actor_did = account.did, 6005 - .owner_did = account.did, 6044 + .authority_did = account.did, 6006 6045 .space_type = "fm.plyr.privateMedia", 6007 6046 .skey = "self", 6008 - .is_owner = true, 6047 + .is_authority = true, 6009 6048 .managing_app = null, 6010 6049 .is_public = false, 6011 6050 .app_access_mode = "allow", ··· 6036 6075 try std.testing.expectEqualStrings("track-one", listed[0].rkey); 6037 6076 } 6038 6077 6039 - test "permissioned spaces keep owner-local state per space URI" { 6078 + test "permissioned spaces keep authority-local state per space URI" { 6040 6079 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 6041 6080 defer arena.deinit(); 6042 6081 const allocator = arena.allocator(); ··· 6046 6085 6047 6086 const owner = try createAccount( 6048 6087 allocator, 6049 - "owner-space.test", 6050 - "owner-space@test.com", 6088 + "authority-space.test", 6089 + "authority-space@test.com", 6051 6090 "password", 6052 - "did:plc:spaceownerlocal", 6091 + "did:plc:spaceauthoritylocal", 6053 6092 true, 6054 6093 ); 6055 6094 const created = try createSpace(allocator, .{ 6056 6095 .actor_did = owner.did, 6057 - .owner_did = owner.did, 6096 + .authority_did = owner.did, 6058 6097 .space_type = "fm.plyr.privateMedia", 6059 6098 .skey = "self", 6060 - .is_owner = true, 6099 + .is_authority = true, 6061 6100 .managing_app = null, 6062 6101 .is_public = false, 6063 6102 .app_access_mode = "allow", ··· 6065 6104 }); 6066 6105 6067 6106 const owner_view = (try getSpace(allocator, owner.did, created.uri)).?; 6068 - try std.testing.expect(owner_view.is_owner); 6107 + try std.testing.expect(owner_view.is_authority); 6069 6108 try std.testing.expect(owner_view.deleted_at == null); 6070 6109 6071 6110 const owner_spaces = try listSpaces(allocator, owner.did, owner.did, "fm.plyr.privateMedia", null, 50); 6072 6111 try std.testing.expectEqual(@as(usize, 1), owner_spaces.len); 6073 - try std.testing.expect(owner_spaces[0].is_owner); 6112 + try std.testing.expect(owner_spaces[0].is_authority); 6074 6113 6075 6114 try markSpaceDeleted(owner.did, created.uri); 6076 6115 const deleted_owner_view = (try getSpace(allocator, owner.did, created.uri)).?; ··· 6085 6124 try init(std.Options.debug_io, ":memory:"); 6086 6125 defer close(); 6087 6126 6088 - const owner = try createAccount(allocator, "owner-first.test", "owner-first@test.com", "password", "did:plc:ownerfirst", true); 6127 + const owner = try createAccount(allocator, "authority-first.test", "authority-first@test.com", "password", "did:plc:ownerfirst", true); 6089 6128 const viewer = try createAccount(allocator, "viewer-first.test", "viewer-first@test.com", "password", "did:plc:viewerfirst", true); 6090 6129 6091 6130 const viewer_row = try createSpace(allocator, .{ 6092 6131 .actor_did = viewer.did, 6093 - .owner_did = owner.did, 6132 + .authority_did = owner.did, 6094 6133 .space_type = "fm.plyr.privateMedia", 6095 6134 .skey = "self", 6096 - .is_owner = false, 6135 + .is_authority = false, 6097 6136 .managing_app = null, 6098 6137 .is_public = false, 6099 6138 .app_access_mode = "allow", 6100 6139 .app_exceptions_json = "[]", 6101 6140 }); 6102 - try std.testing.expect(!viewer_row.is_owner); 6141 + try std.testing.expect(!viewer_row.is_authority); 6103 6142 6104 6143 const owner_row = try createSpace(allocator, .{ 6105 6144 .actor_did = owner.did, 6106 - .owner_did = owner.did, 6145 + .authority_did = owner.did, 6107 6146 .space_type = "fm.plyr.privateMedia", 6108 6147 .skey = "self", 6109 - .is_owner = true, 6148 + .is_authority = true, 6110 6149 .managing_app = "did:web:plyr.fm", 6111 6150 .is_public = true, 6112 6151 .app_access_mode = "deny", 6113 6152 .app_exceptions_json = "[\"did:web:allowed.example\"]", 6114 6153 }); 6115 - try std.testing.expect(owner_row.is_owner); 6154 + try std.testing.expect(owner_row.is_authority); 6116 6155 try std.testing.expect(owner_row.is_public); 6117 6156 try std.testing.expectEqualStrings("did:web:plyr.fm", owner_row.managing_app.?); 6118 6157 6119 6158 try std.testing.expectError(Error.InvalidRefreshSession, createSpace(allocator, .{ 6120 6159 .actor_did = viewer.did, 6121 - .owner_did = owner.did, 6160 + .authority_did = owner.did, 6122 6161 .space_type = "fm.plyr.privateMedia", 6123 6162 .skey = "self", 6124 - .is_owner = false, 6163 + .is_authority = false, 6125 6164 .managing_app = null, 6126 6165 .is_public = false, 6127 6166 .app_access_mode = "allow",
+5 -10
tools/smoke-permissioned.sh
··· 69 69 space_create=$(curl -fsS -X POST "$base/xrpc/com.atproto.space.createSpace" \ 70 70 -H "authorization: Bearer $token" \ 71 71 -H 'content-type: application/json' \ 72 - --data '{"did":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","managingApp":"did:web:plyr.fm","isPublic":false,"appAccessMode":"allow","appExceptions":[]}') 72 + --data '{"authority":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","managingApp":"did:web:plyr.fm","isPublic":false,"appAccessMode":"allow","appExceptions":[]}') 73 73 printf '%s' "$space_create" | grep -q '"uri":"ats://did:plc:permissionsmoke/fm.plyr.privateMedia/self"' 74 74 ! printf '%s' "$space_create" | grep -q '"config":' 75 75 76 76 space_duplicate_status=$(curl -sS -o /tmp/zds-space-duplicate.json -w '%{http_code}' -X POST "$base/xrpc/com.atproto.space.createSpace" \ 77 77 -H "authorization: Bearer $token" \ 78 78 -H 'content-type: application/json' \ 79 - --data '{"did":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","isPublic":false}') 79 + --data '{"authority":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","isPublic":false}') 80 80 test "$space_duplicate_status" = "400" 81 81 grep -q '"error":"SpaceAlreadyExists"' /tmp/zds-space-duplicate.json 82 82 83 83 space_get=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.getSpace?space=$encoded_space") 84 - printf '%s' "$space_get" | grep -q '"isOwner":true' 84 + printf '%s' "$space_get" | grep -q '"isAuthority":true' 85 85 printf '%s' "$space_get" | grep -q '"managingApp":"did:web:plyr.fm"' 86 86 ! printf '%s' "$space_get" | grep -q '"members":' 87 87 88 - space_list=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.listSpaces?did=did:plc:permissionsmoke&type=fm.plyr.privateMedia") 88 + space_list=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.listSpaces?authority=did:plc:permissionsmoke&type=fm.plyr.privateMedia") 89 89 printf '%s' "$space_list" | grep -q '"uri":"ats://did:plc:permissionsmoke/fm.plyr.privateMedia/self"' 90 - printf '%s' "$space_list" | grep -q '"isOwner":true' 90 + printf '%s' "$space_list" | grep -q '"isAuthority":true' 91 91 printf '%s' "$space_list" | grep -q '"cursor":"ats://did:plc:permissionsmoke/fm.plyr.privateMedia/self"' 92 92 ! printf '%s' "$space_list" | grep -q '"isMember":' 93 - 94 - space_members_status=$(curl -sS -o /tmp/zds-space-members.json -w '%{http_code}' \ 95 - -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.getMembers?space=$encoded_space&limit=1") 96 - test "$space_members_status" = "404" 97 - grep -q '"error":"UnknownMethod"' /tmp/zds-space-members.json 98 93 99 94 space_record=$(curl -fsS -X POST "$base/xrpc/com.atproto.space.createRecord" \ 100 95 -H "authorization: Bearer $token" \