diff --git a/bench/README.md b/bench/README.md index 9ad31e1..5a0087b 100644 --- a/bench/README.md +++ b/bench/README.md @@ -82,7 +82,7 @@ blob, then measures these paths as distinct units of work: - `getRecord` by `(space, repo, collection, rkey)` - `listRecords`, limit 50, index-only response shape - `getBlob` storage readback -- `getRepoOplog` catch-up reads +- `listRepoOps` catch-up reads Comparison against Daniel's permissioned-data branch should live in this section once we have a repeatable way to run that PDS locally. Until then, keep diff --git a/bench/main.zig b/bench/main.zig index 44d19ac..0f201ff 100644 --- a/bench/main.zig +++ b/bench/main.zig @@ -571,10 +571,10 @@ fn seedSpaceRecords( ) !zds.storage.store.SpaceConfig { const space = try zds.storage.store.createSpace(allocator, .{ .actor_did = account.did, - .owner_did = account.did, + .authority_did = account.did, .space_type = space_type, .skey = "self", - .is_owner = true, + .is_authority = true, .managing_app = "https://api-stg.plyr.fm", .is_public = false, .app_access_mode = "allow", @@ -668,7 +668,7 @@ fn benchSpaceOplog(allocator: std.mem.Allocator, account: zds.auth.tokens.Accoun const ops = try zds.storage.store.listSpaceRecordOplog(arena.allocator(), space, account.did, null, 100); if (ops.len == 0) return error.MissingRecords; } - return .{ .name = "space getRepoOplog", .ops = iterations, .elapsed_ns = nowNs() - start }; + return .{ .name = "space listRepoOps", .ops = iterations, .elapsed_ns = nowNs() - start }; } fn benchSpaceBlob(allocator: std.mem.Allocator, account: zds.auth.tokens.Account, cid: []const u8) !BenchResult { diff --git a/docs/account-takedown-runbook.md b/docs/account-takedown-runbook.md index 71a3d95..c917657 100644 --- a/docs/account-takedown-runbook.md +++ b/docs/account-takedown-runbook.md @@ -139,7 +139,7 @@ select 'records', count(*) from records where did='$did' union all select 'repo_blocks', count(*) from repo_blocks where did='$did' union all select 'commits', count(*) from commits where did='$did' union all select 'blobs', count(*) from blobs where did='$did' -union all select 'spaces_owned', count(*) from permissioned_spaces where owner_did='$did' +union all select 'spaces_owned', count(*) from permissioned_spaces where authority_did='$did' union all select 'space_actor_state', count(*) from permissioned_space_actor_state where actor_did='$did' union all select 'space_records', count(*) from permissioned_space_records where repo_did='$did' union all select 'space_repos', count(*) from permissioned_space_repos where repo_did='$did' diff --git a/docs/permissioned-data-proposal-94.md b/docs/permissioned-data-proposal-94.md index 1eab2f5..91a9c88 100644 --- a/docs/permissioned-data-proposal-94.md +++ b/docs/permissioned-data-proposal-94.md @@ -19,7 +19,7 @@ Prefer small alignment work that preserves optionality: - isolate prototype code under the existing `space` and permissioned-data modules - add tests around durable semantics that are likely to survive -- avoid adding new member-list or grant behavior in the old vocabulary +- avoid adding new member-list behavior or resurrecting old credential names ZDS's immediate goal is modest: get ahead of the likely direction enough to store private data on the user's PDS, while keeping the implementation easy to @@ -95,18 +95,18 @@ These are the main known gaps between ZDS's current prototype and PR #94: break existing spaces that depend on the declaration, which makes lexicon resolution override behavior appropriate in a way that would be surprising for normal AT records. -- `com.atproto.space.getMemberGrant` is older vocabulary. The proposal uses - `com.atproto.space.getDelegationToken` as a PDS-issued OAuth query and then - exchanges that with the space host through `getSpaceCredential`. -- `getRepoOplog` is older vocabulary. The proposal uses `listRepoOps`. +- ZDS should use the proposal names directly: `getDelegationToken` for the + PDS-issued OAuth query, and `listRepoOps` for incremental permissioned-repo + sync. - The proposal adds `com.atproto.space.listRepos` so a space host can list known writer repos in a space without enumerating readers. - The proposal adds `registerNotify` as an explicit syncer registration method. ZDS currently records credential recipients and supports write/deletion notifications, but does not expose this method shape. -- Space credentials and delegation tokens have more specific JWT `typ`, `sub`, - `aud`, `client_id`, lifetime, and verification expectations than ZDS's - prototype helper names imply. +- Space credentials and delegation tokens have specific JWT `typ`, `sub`, + `aud`, `client_id`, lifetime, and verification expectations. Keep ZDS helper + names and token shapes aligned with those names instead of inventing a local + credential vocabulary. - The proposal introduces optional client attestation for app-bound space credentials. - Space-authority DID documents are expected to publish `#atproto_space` and @@ -200,11 +200,11 @@ Before adding more endpoints, prefer tests that exercise durable semantics: - per-space and per-writer repo isolation - `ats://` parsing and formatting -- OAuth scope gating for owner reads, self reads, writes, and management +- OAuth scope gating for authority reads, self reads, writes, and management - ranged blob reads through permissioned-data auth - write oplog ordering and cursor behavior - record-set commitment changes across create, update, and delete -- deletion behavior for owner spaces versus writer repos +- deletion behavior for authority spaces versus writer repos These tests should stay separate from public-repo conformance tests so the experimental surface can move without muddying stable PDS behavior. diff --git a/docs/permissioned-data.md b/docs/permissioned-data.md index 169ccc0..7ac770c 100644 --- a/docs/permissioned-data.md +++ b/docs/permissioned-data.md @@ -56,14 +56,12 @@ ZDS currently keeps a pre-proposal-94 permissioned-data substrate: `updateSpaceConfig`, `deleteSpace` - records and blobs: `createRecord`, `putRecord`, `deleteRecord`, `applyWrites`, `getRecord`, `listRecords`, `getBlob` -- writer state: `getRepoState`, `getRepoOplog`, `notifyWrite` -- credentials and deletion fanout: `getMemberGrant`, `getSpaceCredential`, +- writer state: `getRepoState`, `listRepoOps`, `notifyWrite` +- credentials and deletion fanout: `getDelegationToken`, `getSpaceCredential`, `notifySpaceDeleted` -The proposal draft has since moved some names and responsibilities. In -particular, `getMemberGrant` maps only loosely to the newer -`getDelegationToken`, and `getRepoOplog` maps only loosely to `listRepoOps`. -Keep that vocabulary difference visible when changing this area. +ZDS uses the proposal names for the credential and sync-read surface: +`getDelegationToken` and `listRepoOps`. ZDS deliberately does not expose the older protocol member-list routes: @@ -93,7 +91,7 @@ target to evaluate, not permission to rebuild the older generic member-list experiment. For private spaces, ZDS currently mints space credentials only to the space -owner DID unless the space is public. That is the conservative default until +authority DID unless the space is public. That is the conservative default until the protocol settles on an application or space-host policy hook for broader credential issuance. @@ -106,8 +104,8 @@ credential recipients. ZDS stores many actor repos in one SQLite database, so permissioned data uses explicit space-scoped tables instead of the public repo tables: -- `permissioned_spaces`: canonical owner-space config keyed by space URI -- `permissioned_space_actor_state`: actor-local owner/deleted state keyed by +- `permissioned_spaces`: canonical authority-space config keyed by space URI +- `permissioned_space_actor_state`: actor-local authority/deleted state keyed by `(space, actor_did)` - `permissioned_space_records`: current records keyed by `(space, repo_did, collection, rkey)` with CID, DAG-CBOR value, repo revision, @@ -115,7 +113,7 @@ explicit space-scoped tables instead of the public repo tables: - `permissioned_space_repos`: writer repo state and current record-set hash - `permissioned_space_record_oplog`: incremental record changes by `(space, repo_did, rev, idx)` -- `permissioned_space_credentials`: short-lived prototype grants and +- `permissioned_space_credentials`: short-lived prototype credentials and credentials - `permissioned_space_credential_recipients`: services to notify for writes and space deletion @@ -123,13 +121,6 @@ explicit space-scoped tables instead of the public repo tables: Permissioned records and blobs are not public repo records. They must not be squeezed into `records`, `repo_blocks`, `commits`, or `seq_events`. -Existing databases from the earlier experiment drop the dedicated -permissioned-space member tables during migration: - -- `permissioned_space_members` -- `permissioned_space_member_state` -- `permissioned_space_member_oplog` - ## Zat first Before implementing local primitives, check Zat's current public API. ZDS uses diff --git a/src/atproto/space.zig b/src/atproto/space.zig index a77fbfa..280ec38 100644 --- a/src/atproto/space.zig +++ b/src/atproto/space.zig @@ -34,7 +34,7 @@ pub fn dispatch(request: *http_api.Request) !void { if (std.mem.eql(u8, method, "com.atproto.space.listSpaces")) return listSpaces(request); if (std.mem.eql(u8, method, "com.atproto.space.updateSpaceConfig")) return updateSpaceConfig(request); if (std.mem.eql(u8, method, "com.atproto.space.deleteSpace")) return deleteSpace(request); - if (std.mem.eql(u8, method, "com.atproto.space.getMemberGrant")) return getMemberGrant(request); + if (std.mem.eql(u8, method, "com.atproto.space.getDelegationToken")) return getDelegationToken(request); if (std.mem.eql(u8, method, "com.atproto.space.createRecord")) return createRecord(request); if (std.mem.eql(u8, method, "com.atproto.space.putRecord")) return putRecord(request); if (std.mem.eql(u8, method, "com.atproto.space.deleteRecord")) return deleteRecord(request); @@ -43,7 +43,7 @@ pub fn dispatch(request: *http_api.Request) !void { if (std.mem.eql(u8, method, "com.atproto.space.listRecords")) return listRecords(request); if (std.mem.eql(u8, method, "com.atproto.space.getBlob")) return getBlob(request); if (std.mem.eql(u8, method, "com.atproto.space.getRepoState")) return getRepoState(request); - if (std.mem.eql(u8, method, "com.atproto.space.getRepoOplog")) return getRepoOplog(request); + if (std.mem.eql(u8, method, "com.atproto.space.listRepoOps")) return listRepoOps(request); if (std.mem.eql(u8, method, "com.atproto.space.getSpaceCredential")) return getSpaceCredential(request); if (std.mem.eql(u8, method, "com.atproto.space.notifyWrite")) return notifyWrite(request); if (std.mem.eql(u8, method, "com.atproto.space.notifySpaceDeleted")) return notifySpaceDeleted(request); @@ -67,14 +67,14 @@ fn createSpace(request: *http_api.Request) !void { else => return err, }; const input = parsed.value; - const did = zat.json.getString(input, "did") orelse { - return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing did"); + const authority = zat.json.getString(input, "authority") orelse { + return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing authority"); }; const space_type = zat.json.getString(input, "type") orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing type"); }; const skey = zat.json.getString(input, "skey") orelse try store.generateRkey(allocator); - try requireSpaceScope(request, auth_ctx.oauth_scope, space_type, did, skey, .manage, null); + try requireSpaceScope(request, auth_ctx.oauth_scope, space_type, authority, skey, .manage, null); const managing_app = zat.json.getString(input, "managingApp"); const is_public = valueBool(input, "isPublic") orelse false; const app_access_mode = zat.json.getString(input, "appAccessMode") orelse "allow"; @@ -85,16 +85,16 @@ fn createSpace(request: *http_api.Request) !void { const space = store.createSpace(allocator, .{ .actor_did = auth_ctx.account.did, - .owner_did = did, + .authority_did = authority, .space_type = space_type, .skey = skey, - .is_owner = std.mem.eql(u8, did, auth_ctx.account.did), + .is_authority = std.mem.eql(u8, authority, auth_ctx.account.did), .managing_app = managing_app, .is_public = is_public, .app_access_mode = app_access_mode, .app_exceptions_json = app_exceptions_json, }) catch |err| switch (err) { - error.InvalidRepoPath => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid did"), + error.InvalidRepoPath => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid authority"), error.InvalidCollection => return http_api.xrpcError(request, .bad_request, "InvalidType", "Invalid space type"), error.InvalidRecordKey => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid skey"), error.InvalidRecordType => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid appAccessMode"), @@ -120,19 +120,19 @@ fn getSpace(request: *http_api.Request) !void { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); }; if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) { - return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); + return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); } try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .manage, null); const space = (try store.getSpace(allocator, auth_ctx.account.did, space_uri)) orelse { return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); }; - if (!space.is_owner) { - return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); + if (!space.is_authority) { + return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); } if (space.deleted_at != null) { return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); } - return http_api.json(request, .ok, try ownerSpaceJson(allocator, space)); + return http_api.json(request, .ok, try authoritySpaceJson(allocator, space)); } fn listSpaces(request: *http_api.Request) !void { @@ -141,28 +141,28 @@ fn listSpaces(request: *http_api.Request) !void { const allocator = arena.allocator(); const auth_ctx = requireAccount(request, allocator) catch return; - var did_buf: [256]u8 = undefined; - const maybe_did = http_api.queryParam(request.url.raw, "did", &did_buf); + var authority_buf: [256]u8 = undefined; + const maybe_authority = http_api.queryParam(request.url.raw, "authority", &authority_buf); var type_buf: [320]u8 = undefined; const maybe_type = http_api.queryParam(request.url.raw, "type", &type_buf); if (maybe_type) |space_type| { if (zat.Nsid.parse(space_type) == null) return http_api.xrpcError(request, .bad_request, "InvalidType", "Invalid space type"); } if (auth_ctx.oauth_scope) |scope_text| { - if (!scopes.spaceAllows(scope_text, .read, maybe_type orelse "*", maybe_did orelse "*", "*", null)) { + if (!scopes.spaceAllows(scope_text, .read, maybe_type orelse "*", maybe_authority orelse "*", "*", null)) { return http_api.xrpcError(request, .forbidden, "InsufficientScope", "OAuth token does not grant the requested space operation"); } } var cursor_buf: [1024]u8 = undefined; const maybe_cursor = http_api.queryParam(request.url.raw, "cursor", &cursor_buf); - const spaces = try store.listSpaces(allocator, auth_ctx.account.did, maybe_did, maybe_type, maybe_cursor, http_api.queryLimit(request.url.raw, 50)); + const spaces = try store.listSpaces(allocator, auth_ctx.account.did, maybe_authority, maybe_type, maybe_cursor, http_api.queryLimit(request.url.raw, 50)); var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); try out.writer.writeAll("{\"spaces\":["); for (spaces, 0..) |space, idx| { if (idx != 0) try out.writer.writeByte(','); - try out.writer.print("{{\"uri\":{f},\"isOwner\":{}}}", .{ std.json.fmt(space.uri, .{}), space.is_owner }); + try out.writer.print("{{\"uri\":{f},\"isAuthority\":{}}}", .{ std.json.fmt(space.uri, .{}), space.is_authority }); } try out.writer.writeByte(']'); if (spaces.len > 0) { @@ -184,10 +184,10 @@ fn updateSpaceConfig(request: *http_api.Request) !void { const input = parsed_body.value; const space = zat.json.getString(input, "space") orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing space"); const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); - if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); + if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .manage, null); const existing = (try store.getSpace(allocator, auth_ctx.account.did, space)) orelse return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); - if (!existing.is_owner) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); + if (!existing.is_authority) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); if (existing.deleted_at != null) return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); const managing_app = zat.json.getString(input, "managingApp"); const clear_managing_app = switch (input) { @@ -228,20 +228,20 @@ fn deleteSpace(request: *http_api.Request) !void { }; const space = zat.json.getString(parsed_body.value, "space") orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing space"); const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); - if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); + if (!std.mem.eql(u8, parsed.did, auth_ctx.account.did)) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .manage, null); const existing = try store.getSpace(allocator, auth_ctx.account.did, space); if (existing == null) return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); - if (!existing.?.is_owner) return http_api.xrpcError(request, .forbidden, "NotSpaceOwner", "Not the space owner"); + if (!existing.?.is_authority) return http_api.xrpcError(request, .forbidden, "NotSpaceAuthority", "Not the space authority"); if (existing.?.deleted_at != null) return http_api.json(request, .ok, "{}"); const recipients = try store.listCredentialRecipients(allocator, space); try store.markSpaceDeleted(auth_ctx.account.did, space); - try store.purgeOwnerSpaceData(space); + try store.purgeAuthoritySpaceData(space); fireNotifySpaceDeleted(allocator, auth_ctx.account, space, recipients) catch {}; return http_api.json(request, .ok, "{}"); } -fn getMemberGrant(request: *http_api.Request) !void { +fn getDelegationToken(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -252,8 +252,8 @@ fn getMemberGrant(request: *http_api.Request) !void { const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); try requireSpaceScope(request, auth_ctx.oauth_scope, parsed.space_type, parsed.did, parsed.skey, .read, null); var keypair = store.signingKeypair(auth_ctx.account.did) catch return http_api.xrpcError(request, .not_found, "RepoNotFound", "Signing key not found"); - const grant = try permissioned.createMemberGrant(allocator, store.currentIo(), auth_ctx.account.did, parsed.did, space, client_id, &keypair); - return http_api.json(request, .ok, try std.fmt.allocPrint(allocator, "{{\"grant\":{f}}}", .{std.json.fmt(grant, .{})})); + const token = try permissioned.createDelegationToken(allocator, store.currentIo(), auth_ctx.account.did, parsed.did, space, client_id, &keypair); + return http_api.json(request, .ok, try std.fmt.allocPrint(allocator, "{{\"token\":{f}}}", .{std.json.fmt(token, .{})})); } fn createRecord(request: *http_api.Request) !void { @@ -285,7 +285,7 @@ fn deleteRecord(request: *http_api.Request) !void { store.deleteSpaceRecord(allocator, space, repo, collection, rkey) catch |err| switch (err) { error.RepoNotFound => return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"), error.MissingRecord => return http_api.xrpcError(request, .not_found, "RecordNotFound", "Record not found"), - error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotAMember", "Not a member of the space"), + error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotPermitted", "Not permitted to write in this space"), else => return err, }; return http_api.json(request, .ok, "{}"); @@ -342,7 +342,7 @@ fn applyWrites(request: *http_api.Request) !void { const results = store.applySpaceWrites(allocator, space, repo, ops.items) catch |err| switch (err) { error.RepoNotFound => return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"), error.MissingRecord => return http_api.xrpcError(request, .not_found, "RecordNotFound", "Record not found"), - error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotAMember", "Not a member of the space"), + error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotPermitted", "Not permitted to write in this space"), else => return err, }; const state = try store.getSpaceRepoState(allocator, space, repo); @@ -411,7 +411,7 @@ fn writeSpaceRecord(request: *http_api.Request, mode: WriteMode) !void { .put => store.putSpaceRecord(allocator, space, repo, collection, rkey, prepared), } catch |err| switch (err) { error.RepoNotFound => return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"), - error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotAMember", "Not a member of the space"), + error.InvalidRefreshSession => return http_api.xrpcError(request, .forbidden, "NotPermitted", "Not permitted to write in this space"), error.MissingRecord => return http_api.xrpcError(request, .bad_request, "RecordNotFound", "Record not found"), else => return err, }; @@ -536,7 +536,7 @@ fn getRepoState(request: *http_api.Request) !void { return writeSignedState(request, allocator, space, repo, repo, .records, state); } -fn getRepoOplog(request: *http_api.Request) !void { +fn listRepoOps(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -579,14 +579,14 @@ fn getSpaceCredential(request: *http_api.Request) !void { defer arena.deinit(); const allocator = arena.allocator(); const token = bearerToken(request) orelse return http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"); - const member_did = permissioned.unverifiedStringClaim(allocator, token, "iss") catch { - return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid member grant"); + const requester_did = permissioned.unverifiedStringClaim(allocator, token, "iss") catch { + return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid delegation token"); }; - const public_key = signingKeyForDid(allocator, member_did) catch { - return http_api.xrpcError(request, .bad_gateway, "DidResolutionFailed", "could not resolve member grant issuer did"); + const public_key = signingKeyForDid(allocator, requester_did) catch { + return http_api.xrpcError(request, .bad_gateway, "DidResolutionFailed", "could not resolve delegation token issuer did"); }; - const grant = permissioned.verifyMemberGrant(allocator, token, public_key) catch { - return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid member grant"); + const delegation = permissioned.verifyDelegationToken(allocator, token, public_key) catch { + return http_api.xrpcError(request, .unauthorized, "InvalidToken", "Invalid delegation token"); }; const parsed_body = parseBody(request, allocator, 32 * 1024) catch |err| switch (err) { error.HandledResponse => return, @@ -594,23 +594,23 @@ fn getSpaceCredential(request: *http_api.Request) !void { }; const input = parsed_body.value; const space = zat.json.getString(input, "space") orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing space"); - if (!std.mem.eql(u8, grant.space, space)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Grant space mismatch"); + if (!std.mem.eql(u8, delegation.space, space)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Delegation token space mismatch"); const parsed = parseSpaceUri(space) orelse return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid space URI"); - if (!std.mem.eql(u8, grant.owner_did, parsed.did)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Grant owner mismatch"); + if (!std.mem.eql(u8, delegation.authority_did, parsed.did)) return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Delegation token authority mismatch"); const config_row = (try store.getSpace(allocator, parsed.did, space)) orelse return http_api.xrpcError(request, .not_found, "SpaceNotFound", "Space not found"); if (config_row.deleted_at != null) { return http_api.xrpcError(request, .bad_request, "SpaceDeleted", "Space has been deleted"); } - if (!config_row.is_public and !std.mem.eql(u8, grant.member_did, parsed.did)) { + if (!config_row.is_public and !std.mem.eql(u8, delegation.requester_did, parsed.did)) { return http_api.xrpcError(request, .forbidden, "NotPermitted", "The space host did not grant access to this requester"); } - if (!spaceAllowsClient(allocator, config_row, grant.client_id)) { + if (!spaceAllowsClient(allocator, config_row, delegation.client_id)) { return http_api.xrpcError(request, .forbidden, "AppNotPermitted", "OAuth client is not allowed for this space"); } - var keypair = store.signingKeypair(parsed.did) catch return http_api.xrpcError(request, .not_found, "RepoNotFound", "Owner signing key not found"); - const credential = try permissioned.createSpaceCredential(allocator, store.currentIo(), parsed.did, space, grant.client_id, &keypair); + var keypair = store.signingKeypair(parsed.did) catch return http_api.xrpcError(request, .not_found, "RepoNotFound", "Authority signing key not found"); + const credential = try permissioned.createSpaceCredential(allocator, store.currentIo(), parsed.did, space, delegation.client_id, &keypair); if (zat.json.getString(input, "notifyEndpoint")) |endpoint| { - try store.recordCredentialRecipient(space, grant.member_did, endpoint); + try store.recordCredentialRecipient(space, delegation.requester_did, endpoint); } return http_api.json(request, .ok, try std.fmt.allocPrint(allocator, "{{\"credential\":{f}}}", .{std.json.fmt(credential, .{})})); } @@ -631,8 +631,8 @@ fn notifyWrite(request: *http_api.Request) !void { const service = requireServiceAuth(request, allocator, "com.atproto.space.notifyWrite") catch return; if (!std.mem.eql(u8, service.issuer_did, repo)) return http_api.xrpcError(request, .unauthorized, "InvalidToken", "JWT issuer must be the writer repo DID"); if (!std.mem.eql(u8, service.audience, parsed.did)) return http_api.xrpcError(request, .unauthorized, "BadJwtAudience", "JWT audience must be the space DID"); - const owner = (store.findAccount(allocator, parsed.did) catch null) orelse return http_api.json(request, .ok, "{}"); - try fanoutNotifyWriteToRecipients(allocator, owner, space, repo, rev); + const authority = (store.findAccount(allocator, parsed.did) catch null) orelse return http_api.json(request, .ok, "{}"); + try fanoutNotifyWriteToRecipients(allocator, authority, space, repo, rev); return http_api.json(request, .ok, "{}"); } @@ -935,7 +935,7 @@ fn requireReadAccess(request: *http_api.Request, allocator: std.mem.Allocator, s return error.HandledResponse; }; if (!std.mem.eql(u8, issuer, parsed.did)) { - try http_api.xrpcError(request, .unauthorized, "InvalidToken", "Space credential issuer must be the space owner"); + try http_api.xrpcError(request, .unauthorized, "InvalidToken", "Space credential issuer must be the space authority"); return error.HandledResponse; } const public_key = signingKeyForDid(allocator, issuer) catch { @@ -1077,13 +1077,13 @@ fn spaceConfigJson(allocator: std.mem.Allocator, space: store.SpaceConfig) ![]co var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); try out.writer.print( - "{{\"uri\":{f},\"did\":{f},\"type\":{f},\"skey\":{f},\"isOwner\":{},\"isPublic\":{},\"appAccessMode\":{f}", + "{{\"uri\":{f},\"authority\":{f},\"type\":{f},\"skey\":{f},\"isAuthority\":{},\"isPublic\":{},\"appAccessMode\":{f}", .{ std.json.fmt(space.uri, .{}), - std.json.fmt(space.owner_did, .{}), + std.json.fmt(space.authority_did, .{}), std.json.fmt(space.space_type, .{}), std.json.fmt(space.skey, .{}), - space.is_owner, + space.is_authority, space.is_public, std.json.fmt(space.app_access_mode, .{}), }, @@ -1095,14 +1095,14 @@ fn spaceConfigJson(allocator: std.mem.Allocator, space: store.SpaceConfig) ![]co return out.toOwnedSlice(); } -fn ownerSpaceJson(allocator: std.mem.Allocator, space: store.SpaceConfig) ![]const u8 { +fn authoritySpaceJson(allocator: std.mem.Allocator, space: store.SpaceConfig) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); try out.writer.print( - "{{\"uri\":{f},\"isOwner\":{},\"isPublic\":{},\"appAccessMode\":{f}", + "{{\"uri\":{f},\"isAuthority\":{},\"isPublic\":{},\"appAccessMode\":{f}", .{ std.json.fmt(space.uri, .{}), - space.is_owner, + space.is_authority, space.is_public, std.json.fmt(space.app_access_mode, .{}), }, diff --git a/src/http/router.zig b/src/http/router.zig index 983c4d3..f0c442d 100644 --- a/src/http/router.zig +++ b/src/http/router.zig @@ -213,13 +213,13 @@ pub const endpoints = [_]Endpoint{ .{ .route = .identity_submit_plc_operation, .method = "POST", .path = "/xrpc/com.atproto.identity.submitPlcOperation", .group = "identity", .auth = "bearer", .summary = "Submit a PLC operation." }, .{ .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"} }, - .{ .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 }, + .{ .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 }, .{ .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 }, - .{ .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 }, + .{ .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 }, .{ .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 }, .{ .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 }, - .{ .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 }, + .{ .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 }, .{ .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 }, .{ .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,11 +229,11 @@ pub const endpoints = [_]Endpoint{ .{ .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 }, .{ .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 }, .{ .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 }, - .{ .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 }, - .{ .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 }, + .{ .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 }, + .{ .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 }, - .{ .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 }, - .{ .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 }, + .{ .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 }, + .{ .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 }, }; pub fn route(method: httpz.Method, target: []const u8) Route { diff --git a/src/internal/permissioned_data.zig b/src/internal/permissioned_data.zig index ef983fe..f5e7234 100644 --- a/src/internal/permissioned_data.zig +++ b/src/internal/permissioned_data.zig @@ -1,7 +1,7 @@ //! Experimental permissioned-data primitives for `com.atproto.space.*`. //! //! Keep protocol mechanics here so the experimental LtHash, signed-commit, -//! member-grant, and space-credential code does not sprawl through stable PDS +//! delegation-token, and space-credential code does not sprawl through stable PDS //! auth, repo, or sync modules. const std = @import("std"); @@ -89,16 +89,16 @@ pub const SignedCommit = struct { rev: []const u8, }; -pub const MemberGrant = struct { - member_did: []const u8, - owner_did: []const u8, +pub const DelegationToken = struct { + requester_did: []const u8, + authority_did: []const u8, space: []const u8, client_id: []const u8, exp: i64, }; pub const SpaceCredential = struct { - owner_did: []const u8, + authority_did: []const u8, space: []const u8, client_id: []const u8, exp: i64, @@ -145,25 +145,27 @@ pub fn signedCommitJson(allocator: std.mem.Allocator, commit: SignedCommit) ![]c ); } -pub fn createMemberGrant( +pub fn createDelegationToken( allocator: std.mem.Allocator, io: std.Io, - member_did: []const u8, - owner_did: []const u8, + requester_did: []const u8, + authority_did: []const u8, space: []const u8, client_id: []const u8, keypair: *const zat.Keypair, ) ![]const u8 { const iat = unixNow(); - const exp = iat + 300; + const exp = iat + 60; const jti = try randomTokenId(allocator, io); defer allocator.free(jti); - const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"space_member_grant\",\"alg\":\"{s}\"}}", .{@tagName(keypair.algorithm())}); + const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"atproto-space-delegation+jwt\",\"alg\":\"{s}\",\"kid\":\"#atproto\"}}", .{@tagName(keypair.algorithm())}); defer allocator.free(header); + const audience = try std.fmt.allocPrint(allocator, "{s}#atproto_space_host", .{authority_did}); + defer allocator.free(audience); const payload = try std.fmt.allocPrint( allocator, - "{{\"iss\":{f},\"aud\":{f},\"space\":{f},\"clientId\":{f},\"lxm\":\"com.atproto.space.getSpaceCredential\",\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", - .{ 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, .{}) }, + "{{\"iss\":{f},\"aud\":{f},\"sub\":{f},\"client_id\":{f},\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", + .{ std.json.fmt(requester_did, .{}), std.json.fmt(audience, .{}), std.json.fmt(space, .{}), std.json.fmt(client_id, .{}), iat, exp, std.json.fmt(jti, .{}) }, ); defer allocator.free(payload); return zat.oauth.createJwt(allocator, header, payload, keypair); @@ -172,7 +174,7 @@ pub fn createMemberGrant( pub fn createSpaceCredential( allocator: std.mem.Allocator, io: std.Io, - owner_did: []const u8, + authority_did: []const u8, space: []const u8, client_id: []const u8, keypair: *const zat.Keypair, @@ -181,46 +183,54 @@ pub fn createSpaceCredential( const exp = iat + 7200; const jti = try randomTokenId(allocator, io); defer allocator.free(jti); - const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"space_credential\",\"alg\":\"{s}\"}}", .{@tagName(keypair.algorithm())}); + const header = try std.fmt.allocPrint(allocator, "{{\"typ\":\"atproto-space-credential+jwt\",\"alg\":\"{s}\",\"kid\":\"#atproto_space\"}}", .{@tagName(keypair.algorithm())}); defer allocator.free(header); const payload = try std.fmt.allocPrint( allocator, - "{{\"iss\":{f},\"space\":{f},\"clientId\":{f},\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", - .{ std.json.fmt(owner_did, .{}), std.json.fmt(space, .{}), std.json.fmt(client_id, .{}), iat, exp, std.json.fmt(jti, .{}) }, + "{{\"iss\":{f},\"sub\":{f},\"client_id\":{f},\"iat\":{d},\"exp\":{d},\"jti\":{f}}}", + .{ std.json.fmt(authority_did, .{}), std.json.fmt(space, .{}), std.json.fmt(client_id, .{}), iat, exp, std.json.fmt(jti, .{}) }, ); defer allocator.free(payload); return zat.oauth.createJwt(allocator, header, payload, keypair); } -pub fn verifyMemberGrant(allocator: std.mem.Allocator, token: []const u8, public_key_multibase: []const u8) !MemberGrant { - const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "space_member_grant"); +pub fn verifyDelegationToken(allocator: std.mem.Allocator, token: []const u8, public_key_multibase: []const u8) !DelegationToken { + const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "atproto-space-delegation+jwt"); defer parsed.deinit(); - const lxm = zat.json.getString(parsed.payload.value, "lxm") orelse return error.InvalidJwt; - if (!std.mem.eql(u8, lxm, "com.atproto.space.getSpaceCredential")) return error.InvalidJwt; const exp = zat.json.getInt(parsed.payload.value, "exp") orelse return error.InvalidJwt; if (exp < unixNow()) return error.ExpiredJwt; + const audience = zat.json.getString(parsed.payload.value, "aud") orelse return error.InvalidJwt; + const authority_did = authorityDidFromAudience(audience) orelse return error.InvalidJwt; return .{ - .member_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), - .owner_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "aud") orelse return error.InvalidJwt), - .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "space") orelse return error.InvalidJwt), - .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "clientId") orelse return error.InvalidJwt), + .requester_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), + .authority_did = try allocator.dupe(u8, authority_did), + .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "sub") orelse return error.InvalidJwt), + .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "client_id") orelse return error.InvalidJwt), .exp = exp, }; } pub fn verifySpaceCredential(allocator: std.mem.Allocator, token: []const u8, public_key_multibase: []const u8) !SpaceCredential { - const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "space_credential"); + const parsed = try parseAndVerifyJwt(allocator, token, public_key_multibase, "atproto-space-credential+jwt"); defer parsed.deinit(); const exp = zat.json.getInt(parsed.payload.value, "exp") orelse return error.InvalidJwt; if (exp < unixNow()) return error.ExpiredJwt; return .{ - .owner_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), - .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "space") orelse return error.InvalidJwt), - .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "clientId") orelse return error.InvalidJwt), + .authority_did = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "iss") orelse return error.InvalidJwt), + .space = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "sub") orelse return error.InvalidJwt), + .client_id = try allocator.dupe(u8, zat.json.getString(parsed.payload.value, "client_id") orelse return error.InvalidJwt), .exp = exp, }; } +fn authorityDidFromAudience(audience: []const u8) ?[]const u8 { + const suffix = "#atproto_space_host"; + if (!std.mem.endsWith(u8, audience, suffix)) return null; + const authority_did = audience[0 .. audience.len - suffix.len]; + if (zat.Did.parse(authority_did) == null) return null; + return authority_did; +} + pub fn unverifiedStringClaim(allocator: std.mem.Allocator, token: []const u8, claim: []const u8) ![]const u8 { var parts: [3][]const u8 = undefined; var part_count: usize = 0; @@ -375,7 +385,7 @@ test "LtHash snapshot vector" { try std.testing.expectEqualStrings(expected, &std.fmt.bytesToHex(hash.bytes, .lower)); } -test "member grant and space credential round trip" { +test "delegation token and space credential round trip" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -389,34 +399,34 @@ test "member grant and space credential round trip" { const did_key = try keypair.did(allocator); const public_key_multibase = did_key["did:key:".len..]; - const grant_token = try createMemberGrant( + const delegation_token = try createDelegationToken( allocator, std.Options.debug_io, - "did:plc:membergrant", - "did:plc:spaceowner", - "ats://did:plc:spaceowner/fm.plyr.privateMedia/self", + "did:plc:requester", + "did:plc:spaceauthority", + "ats://did:plc:spaceauthority/fm.plyr.privateMedia/self", "https://plyr.fm/oauth-client.json", &keypair, ); - const grant = try verifyMemberGrant(allocator, grant_token, public_key_multibase); - try std.testing.expectEqualStrings("did:plc:membergrant", grant.member_did); - try std.testing.expectEqualStrings("did:plc:spaceowner", grant.owner_did); - try std.testing.expectEqualStrings("ats://did:plc:spaceowner/fm.plyr.privateMedia/self", grant.space); - try std.testing.expectEqualStrings("https://plyr.fm/oauth-client.json", grant.client_id); + const delegation = try verifyDelegationToken(allocator, delegation_token, public_key_multibase); + try std.testing.expectEqualStrings("did:plc:requester", delegation.requester_did); + try std.testing.expectEqualStrings("did:plc:spaceauthority", delegation.authority_did); + try std.testing.expectEqualStrings("ats://did:plc:spaceauthority/fm.plyr.privateMedia/self", delegation.space); + try std.testing.expectEqualStrings("https://plyr.fm/oauth-client.json", delegation.client_id); const credential_token = try createSpaceCredential( allocator, std.Options.debug_io, - "did:plc:spaceowner", - grant.space, - grant.client_id, + "did:plc:spaceauthority", + delegation.space, + delegation.client_id, &keypair, ); const credential = try verifySpaceCredential(allocator, credential_token, public_key_multibase); - try std.testing.expectEqualStrings("did:plc:spaceowner", credential.owner_did); - try std.testing.expectEqualStrings(grant.space, credential.space); - try std.testing.expectEqualStrings(grant.client_id, credential.client_id); + try std.testing.expectEqualStrings("did:plc:spaceauthority", credential.authority_did); + try std.testing.expectEqualStrings(delegation.space, credential.space); + try std.testing.expectEqualStrings(delegation.client_id, credential.client_id); - try std.testing.expectError(error.InvalidJwt, verifySpaceCredential(allocator, grant_token, public_key_multibase)); - try std.testing.expectError(error.InvalidJwt, verifyMemberGrant(allocator, credential_token, public_key_multibase)); + try std.testing.expectError(error.InvalidJwt, verifySpaceCredential(allocator, delegation_token, public_key_multibase)); + try std.testing.expectError(error.InvalidJwt, verifyDelegationToken(allocator, credential_token, public_key_multibase)); } diff --git a/src/storage/store.zig b/src/storage/store.zig index c7a91db..cd6e377 100644 --- a/src/storage/store.zig +++ b/src/storage/store.zig @@ -298,14 +298,14 @@ pub const WriteResult = struct { pub const SpaceConfig = struct { uri: []const u8, - owner_did: []const u8, + authority_did: []const u8, space_type: []const u8, skey: []const u8, managing_app: ?[]const u8, is_public: bool, app_access_mode: []const u8, app_exceptions_json: []const u8, - is_owner: bool, + is_authority: bool, deleted_at: ?i64, }; @@ -3045,10 +3045,10 @@ pub fn prepareRecordValue( pub const CreateSpaceInput = struct { actor_did: []const u8, - owner_did: []const u8, + authority_did: []const u8, space_type: []const u8, skey: []const u8, - is_owner: bool, + is_authority: bool, managing_app: ?[]const u8, is_public: bool, app_access_mode: []const u8, @@ -3057,14 +3057,14 @@ pub const CreateSpaceInput = struct { pub fn createSpace(allocator: std.mem.Allocator, input: CreateSpaceInput) !SpaceConfig { if (zat.Did.parse(input.actor_did) == null) return Error.InvalidRepoPath; - if (zat.Did.parse(input.owner_did) == null) return Error.InvalidRepoPath; + if (zat.Did.parse(input.authority_did) == null) return Error.InvalidRepoPath; if (zat.Nsid.parse(input.space_type) == null) return Error.InvalidCollection; if (zat.Rkey.parse(input.skey) == null) return Error.InvalidRecordKey; if (!std.mem.eql(u8, input.app_access_mode, "allow") and !std.mem.eql(u8, input.app_access_mode, "deny")) { return Error.InvalidRecordType; } - const uri = try std.fmt.allocPrint(allocator, "ats://{s}/{s}/{s}", .{ input.owner_did, input.space_type, input.skey }); + const uri = try std.fmt.allocPrint(allocator, "ats://{s}/{s}/{s}", .{ input.authority_did, input.space_type, input.skey }); db_mutex.lockUncancelable(store_io); defer db_mutex.unlock(store_io); @@ -3076,12 +3076,12 @@ pub fn createSpace(allocator: std.mem.Allocator, input: CreateSpaceInput) !Space errdefer conn.rollback(); try conn.exec( \\INSERT INTO permissioned_spaces ( - \\ uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json + \\ uri, authority_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json \\) VALUES (?, ?, ?, ?, ?, ?, ?, ?) \\ON CONFLICT(uri) DO NOTHING , .{ uri, - input.owner_did, + input.authority_did, input.space_type, input.skey, input.managing_app, @@ -3089,7 +3089,7 @@ pub fn createSpace(allocator: std.mem.Allocator, input: CreateSpaceInput) !Space input.app_access_mode, input.app_exceptions_json, }); - if (input.is_owner) { + if (input.is_authority) { try conn.exec( \\UPDATE permissioned_spaces \\SET managing_app = ?, @@ -3109,11 +3109,11 @@ pub fn createSpace(allocator: std.mem.Allocator, input: CreateSpaceInput) !Space \\INSERT INTO permissioned_space_repos (space, repo_did, set_hash, rev) \\VALUES (?, ?, NULL, NULL) \\ON CONFLICT(space, repo_did) DO NOTHING - , .{ uri, input.owner_did }); + , .{ uri, input.authority_did }); try conn.exec( - \\INSERT INTO permissioned_space_actor_state (space, actor_did, is_owner) + \\INSERT INTO permissioned_space_actor_state (space, actor_did, is_authority) \\VALUES (?, ?, ?) - , .{ uri, input.actor_did, @as(i64, if (input.is_owner) 1 else 0) }); + , .{ uri, input.actor_did, @as(i64, if (input.is_authority) 1 else 0) }); try conn.commit(); return (try getSpaceLocked(allocator, input.actor_did, uri)) orelse Error.RepoNotFound; @@ -3146,7 +3146,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND s.owner_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, did, space_type, cursor, capped_limit }) @@ -3155,7 +3155,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND s.owner_did = ? AND s.space_type = ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND s.space_type = ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, did, space_type, capped_limit }) @@ -3164,7 +3164,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND s.owner_did = ? AND s.uri > ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND s.uri > ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, did, cursor, capped_limit }) @@ -3173,7 +3173,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND s.owner_did = ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND s.authority_did = ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, did, capped_limit }) @@ -3183,7 +3183,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND s.space_type = ? AND s.uri > ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, space_type, cursor, capped_limit }) @@ -3192,7 +3192,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND s.space_type = ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND s.space_type = ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, space_type, capped_limit }) @@ -3201,7 +3201,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND s.uri > ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND s.uri > ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, cursor, capped_limit }) @@ -3210,7 +3210,7 @@ pub fn listSpaces( \\SELECT s.uri \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri - \\WHERE a.actor_did = ? AND a.is_owner = 1 + \\WHERE a.actor_did = ? AND a.is_authority = 1 \\ORDER BY s.uri ASC \\LIMIT ? , .{ actor_did, capped_limit }); @@ -3264,7 +3264,7 @@ pub fn markSpaceDeleted(actor_did: []const u8, space: []const u8) !void { try conn.exec("UPDATE permissioned_space_actor_state SET deleted_at = unixepoch() WHERE space = ? AND actor_did = ?", .{ space, actor_did }); } -pub fn purgeOwnerSpaceData(space: []const u8) !void { +pub fn purgeAuthoritySpaceData(space: []const u8) !void { db_mutex.lockUncancelable(store_io); defer db_mutex.unlock(store_io); try requireInitialized(); @@ -3621,7 +3621,7 @@ fn insertRecordOplogLocked( fn getSpaceConfigLocked(allocator: std.mem.Allocator, uri: []const u8) !?SpaceConfig { const row = try conn.row( - \\SELECT uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json + \\SELECT uri, authority_did, space_type, skey, managing_app, is_public, app_access_mode, app_exceptions_json \\FROM permissioned_spaces \\WHERE uri = ? , .{uri}); @@ -3630,22 +3630,22 @@ fn getSpaceConfigLocked(allocator: std.mem.Allocator, uri: []const u8) !?SpaceCo return .{ .uri = try allocator.dupe(u8, row.?.text(0)), - .owner_did = try allocator.dupe(u8, row.?.text(1)), + .authority_did = try allocator.dupe(u8, row.?.text(1)), .space_type = try allocator.dupe(u8, row.?.text(2)), .skey = try allocator.dupe(u8, row.?.text(3)), .managing_app = if (row.?.nullableText(4)) |value| try allocator.dupe(u8, value) else null, .is_public = row.?.int(5) != 0, .app_access_mode = try allocator.dupe(u8, row.?.text(6)), .app_exceptions_json = try allocator.dupe(u8, row.?.text(7)), - .is_owner = false, + .is_authority = false, .deleted_at = null, }; } fn getSpaceLocked(allocator: std.mem.Allocator, actor_did: []const u8, uri: []const u8) !?SpaceConfig { const row = try conn.row( - \\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, - \\ a.is_owner, a.deleted_at + \\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, + \\ a.is_authority, a.deleted_at \\FROM permissioned_spaces s \\JOIN permissioned_space_actor_state a ON a.space = s.uri \\WHERE s.uri = ? AND a.actor_did = ? @@ -3655,14 +3655,14 @@ fn getSpaceLocked(allocator: std.mem.Allocator, actor_did: []const u8, uri: []co return .{ .uri = try allocator.dupe(u8, row.?.text(0)), - .owner_did = try allocator.dupe(u8, row.?.text(1)), + .authority_did = try allocator.dupe(u8, row.?.text(1)), .space_type = try allocator.dupe(u8, row.?.text(2)), .skey = try allocator.dupe(u8, row.?.text(3)), .managing_app = if (row.?.nullableText(4)) |value| try allocator.dupe(u8, value) else null, .is_public = row.?.int(5) != 0, .app_access_mode = try allocator.dupe(u8, row.?.text(6)), .app_exceptions_json = try allocator.dupe(u8, row.?.text(7)), - .is_owner = row.?.int(8) != 0, + .is_authority = row.?.int(8) != 0, .deleted_at = if (row.?.nullableInt(9)) |value| value else null, }; } @@ -3701,7 +3701,7 @@ fn parseSpaceRecordCursor(cursor: []const u8) struct { collection: []const u8, r } const SpaceParts = struct { - owner_did: []const u8, + authority_did: []const u8, space_type: []const u8, skey: []const u8, }; @@ -3711,15 +3711,15 @@ fn parseSpaceParts(uri: []const u8) ?SpaceParts { if (!std.mem.startsWith(u8, uri, prefix)) return null; const rest = uri[prefix.len..]; const first = std.mem.indexOfScalar(u8, rest, '/') orelse return null; - const owner_did = rest[0..first]; - if (zat.Did.parse(owner_did) == null) return null; + const authority_did = rest[0..first]; + if (zat.Did.parse(authority_did) == null) return null; const after_did = rest[first + 1 ..]; const second = std.mem.indexOfScalar(u8, after_did, '/') orelse return null; const space_type = after_did[0..second]; if (zat.Nsid.parse(space_type) == null) return null; const skey = after_did[second + 1 ..]; if (zat.Rkey.parse(skey) == null) return null; - return .{ .owner_did = owner_did, .space_type = space_type, .skey = skey }; + return .{ .authority_did = authority_did, .space_type = space_type, .skey = skey }; } fn migrate() !void { @@ -3769,10 +3769,9 @@ fn migrate() !void { fn migratePermissionedDataTables() !void { try ensureMigrationTable(); - try migratePermissionedSpacesOwnerFk(); - try migratePermissionedSpaceActorState(); + try migratePermissionedSpacesAuthorityColumns(); + try migratePermissionedSpaceAuthorityFlag(); try migratePermissionedRecordOplogRepo(); - try migratePermissionedDropMemberLists(); } fn ensureMigrationTable() !void { @@ -3794,6 +3793,26 @@ fn accountsColumnExistsLocked(column: []const u8) !bool { return false; } +fn permissionedSpacesColumnExistsLocked(column: []const u8) !bool { + var rows = try conn.rows("PRAGMA table_info(permissioned_spaces)", .{}); + defer rows.deinit(); + while (rows.next()) |row| { + if (std.mem.eql(u8, row.text(1), column)) return true; + } + if (rows.err) |err| return err; + return false; +} + +fn permissionedSpaceActorStateColumnExistsLocked(column: []const u8) !bool { + var rows = try conn.rows("PRAGMA table_info(permissioned_space_actor_state)", .{}); + defer rows.deinit(); + while (rows.next()) |row| { + if (std.mem.eql(u8, row.text(1), column)) return true; + } + if (rows.err) |err| return err; + return false; +} + fn migrationApplied(name: []const u8) !bool { const row = try conn.row("SELECT 1 FROM zds_migrations WHERE name = ?", .{name}); if (row == null) return false; @@ -3805,30 +3824,34 @@ fn markMigrationApplied(name: []const u8) !void { try conn.exec("INSERT OR IGNORE INTO zds_migrations (name) VALUES (?)", .{name}); } -fn migratePermissionedSpacesOwnerFk() !void { - const name = "permissioned-spaces-owner-fk"; +fn migratePermissionedSpacesAuthorityColumns() !void { + const name = "permissioned-spaces-authority-columns"; if (try migrationApplied(name)) return; + if (!try permissionedSpacesColumnExistsLocked("owner_did")) { + try markMigrationApplied(name); + return; + } try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_spaces_next"); try conn.execNoArgs( \\CREATE TABLE permissioned_spaces_next ( \\ uri TEXT PRIMARY KEY, - \\ owner_did TEXT NOT NULL, + \\ authority_did TEXT NOT NULL, \\ space_type TEXT NOT NULL, \\ skey TEXT NOT NULL, \\ managing_app TEXT, \\ is_public INTEGER NOT NULL DEFAULT 0, \\ app_access_mode TEXT NOT NULL DEFAULT 'allow', \\ app_exceptions_json TEXT NOT NULL DEFAULT '[]', - \\ is_owner INTEGER NOT NULL DEFAULT 1, + \\ is_authority INTEGER NOT NULL DEFAULT 1, \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), \\ deleted_at INTEGER, - \\ UNIQUE(owner_did, space_type, skey) + \\ UNIQUE(authority_did, space_type, skey) \\) ); try conn.execNoArgs( \\INSERT INTO permissioned_spaces_next ( - \\ uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, - \\ app_exceptions_json, is_owner, created_at, deleted_at + \\ uri, authority_did, space_type, skey, managing_app, is_public, app_access_mode, + \\ app_exceptions_json, is_authority, created_at, deleted_at \\) \\SELECT uri, owner_did, space_type, skey, managing_app, is_public, app_access_mode, \\ app_exceptions_json, is_owner, created_at, deleted_at @@ -3839,22 +3862,47 @@ fn migratePermissionedSpacesOwnerFk() !void { try conn.execNoArgs("DROP TABLE permissioned_spaces"); try conn.execNoArgs("ALTER TABLE permissioned_spaces_next RENAME TO permissioned_spaces"); try conn.execNoArgs("PRAGMA foreign_keys = ON"); - try conn.execNoArgs("CREATE INDEX IF NOT EXISTS permissioned_spaces_owner_idx ON permissioned_spaces (owner_did, space_type, uri)"); + try conn.execNoArgs("CREATE INDEX IF NOT EXISTS permissioned_spaces_authority_idx ON permissioned_spaces (authority_did, space_type, uri)"); try markMigrationApplied(name); } -fn migratePermissionedSpaceActorState() !void { - const name = "permissioned-space-actor-state"; +fn migratePermissionedSpaceAuthorityFlag() !void { + const name = "permissioned-space-authority-flag"; if (try migrationApplied(name)) return; + if (try permissionedSpaceActorStateColumnExistsLocked("is_owner")) { + try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_actor_state_next"); + try conn.execNoArgs( + \\CREATE TABLE permissioned_space_actor_state_next ( + \\ space TEXT NOT NULL REFERENCES permissioned_spaces(uri) ON DELETE CASCADE, + \\ actor_did TEXT NOT NULL, + \\ is_authority INTEGER NOT NULL DEFAULT 0, + \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), + \\ deleted_at INTEGER, + \\ PRIMARY KEY (space, actor_did) + \\) + ); + try conn.execNoArgs( + \\INSERT INTO permissioned_space_actor_state_next ( + \\ space, actor_did, is_authority, created_at, deleted_at + \\) + \\SELECT space, actor_did, is_owner, created_at, deleted_at + \\FROM permissioned_space_actor_state + ); + try conn.execNoArgs("PRAGMA foreign_keys = OFF"); + errdefer conn.execNoArgs("PRAGMA foreign_keys = ON") catch {}; + try conn.execNoArgs("DROP TABLE permissioned_space_actor_state"); + try conn.execNoArgs("ALTER TABLE permissioned_space_actor_state_next RENAME TO permissioned_space_actor_state"); + try conn.execNoArgs("PRAGMA foreign_keys = ON"); + } try conn.execNoArgs( - \\INSERT OR IGNORE INTO permissioned_space_actor_state (space, actor_did, is_owner, deleted_at) + \\INSERT OR IGNORE INTO permissioned_space_actor_state (space, actor_did, is_authority, deleted_at) \\SELECT s.uri, - \\ CASE WHEN s.is_owner = 1 THEN s.owner_did ELSE COALESCE(r.repo_did, s.owner_did) END, - \\ s.is_owner, + \\ CASE WHEN s.is_authority = 1 THEN s.authority_did ELSE COALESCE(r.repo_did, s.authority_did) END, + \\ s.is_authority, \\ s.deleted_at \\FROM permissioned_spaces s \\LEFT JOIN permissioned_space_repos r ON r.space = s.uri - \\WHERE s.is_owner = 1 + \\WHERE s.is_authority = 1 ); try markMigrationApplied(name); } @@ -3889,15 +3937,6 @@ fn migratePermissionedRecordOplogRepo() !void { try markMigrationApplied(name); } -fn migratePermissionedDropMemberLists() !void { - const name = "permissioned-drop-member-lists"; - if (try migrationApplied(name)) return; - try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_members"); - try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_member_state"); - try conn.execNoArgs("DROP TABLE IF EXISTS permissioned_space_member_oplog"); - try markMigrationApplied(name); -} - fn migrateSeqEventsSync11() !void { const name = "sync11-seq-events"; if (try migrationApplied(name)) return; @@ -5540,24 +5579,24 @@ const schema_statements = [_][*:0]const u8{ "CREATE INDEX IF NOT EXISTS reserved_signing_keys_expires_idx ON reserved_signing_keys (expires_at) WHERE used_at IS NULL", \\CREATE TABLE IF NOT EXISTS permissioned_spaces ( \\ uri TEXT PRIMARY KEY, - \\ owner_did TEXT NOT NULL, + \\ authority_did TEXT NOT NULL, \\ space_type TEXT NOT NULL, \\ skey TEXT NOT NULL, \\ managing_app TEXT, \\ is_public INTEGER NOT NULL DEFAULT 0, \\ app_access_mode TEXT NOT NULL DEFAULT 'allow', \\ app_exceptions_json TEXT NOT NULL DEFAULT '[]', - \\ is_owner INTEGER NOT NULL DEFAULT 1, + \\ is_authority INTEGER NOT NULL DEFAULT 1, \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), \\ deleted_at INTEGER, - \\ UNIQUE(owner_did, space_type, skey) + \\ UNIQUE(authority_did, space_type, skey) \\) , - "CREATE INDEX IF NOT EXISTS permissioned_spaces_owner_idx ON permissioned_spaces (owner_did, space_type, uri)", + "CREATE INDEX IF NOT EXISTS permissioned_spaces_authority_idx ON permissioned_spaces (authority_did, space_type, uri)", \\CREATE TABLE IF NOT EXISTS permissioned_space_actor_state ( \\ space TEXT NOT NULL REFERENCES permissioned_spaces(uri) ON DELETE CASCADE, \\ actor_did TEXT NOT NULL, - \\ is_owner INTEGER NOT NULL DEFAULT 0, + \\ is_authority INTEGER NOT NULL DEFAULT 0, \\ created_at INTEGER NOT NULL DEFAULT (unixepoch()), \\ deleted_at INTEGER, \\ PRIMARY KEY (space, actor_did) @@ -5982,30 +6021,30 @@ test "permissioned spaces store self-owned records outside public repo" { "space.test", "space@test.com", "password", - "did:plc:spaceowneralice", + "did:plc:spaceauthorityalice", true, ); const space = try createSpace(allocator, .{ .actor_did = account.did, - .owner_did = account.did, + .authority_did = account.did, .space_type = "fm.plyr.privateMedia", .skey = "self", - .is_owner = true, + .is_authority = true, .managing_app = "did:web:plyr.fm", .is_public = false, .app_access_mode = "allow", .app_exceptions_json = "[]", }); - try std.testing.expectEqualStrings("ats://did:plc:spaceowneralice/fm.plyr.privateMedia/self", space.uri); - try std.testing.expect(space.is_owner); + try std.testing.expectEqualStrings("ats://did:plc:spaceauthorityalice/fm.plyr.privateMedia/self", space.uri); + try std.testing.expect(space.is_authority); try std.testing.expectError(Error.InvalidRefreshSession, createSpace(allocator, .{ .actor_did = account.did, - .owner_did = account.did, + .authority_did = account.did, .space_type = "fm.plyr.privateMedia", .skey = "self", - .is_owner = true, + .is_authority = true, .managing_app = null, .is_public = false, .app_access_mode = "allow", @@ -6036,7 +6075,7 @@ test "permissioned spaces store self-owned records outside public repo" { try std.testing.expectEqualStrings("track-one", listed[0].rkey); } -test "permissioned spaces keep owner-local state per space URI" { +test "permissioned spaces keep authority-local state per space URI" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -6046,18 +6085,18 @@ test "permissioned spaces keep owner-local state per space URI" { const owner = try createAccount( allocator, - "owner-space.test", - "owner-space@test.com", + "authority-space.test", + "authority-space@test.com", "password", - "did:plc:spaceownerlocal", + "did:plc:spaceauthoritylocal", true, ); const created = try createSpace(allocator, .{ .actor_did = owner.did, - .owner_did = owner.did, + .authority_did = owner.did, .space_type = "fm.plyr.privateMedia", .skey = "self", - .is_owner = true, + .is_authority = true, .managing_app = null, .is_public = false, .app_access_mode = "allow", @@ -6065,12 +6104,12 @@ test "permissioned spaces keep owner-local state per space URI" { }); const owner_view = (try getSpace(allocator, owner.did, created.uri)).?; - try std.testing.expect(owner_view.is_owner); + try std.testing.expect(owner_view.is_authority); try std.testing.expect(owner_view.deleted_at == null); const owner_spaces = try listSpaces(allocator, owner.did, owner.did, "fm.plyr.privateMedia", null, 50); try std.testing.expectEqual(@as(usize, 1), owner_spaces.len); - try std.testing.expect(owner_spaces[0].is_owner); + try std.testing.expect(owner_spaces[0].is_authority); try markSpaceDeleted(owner.did, created.uri); const deleted_owner_view = (try getSpace(allocator, owner.did, created.uri)).?; @@ -6085,43 +6124,43 @@ test "permissioned space create is duplicate-checked per actor" { try init(std.Options.debug_io, ":memory:"); defer close(); - const owner = try createAccount(allocator, "owner-first.test", "owner-first@test.com", "password", "did:plc:ownerfirst", true); + const owner = try createAccount(allocator, "authority-first.test", "authority-first@test.com", "password", "did:plc:ownerfirst", true); const viewer = try createAccount(allocator, "viewer-first.test", "viewer-first@test.com", "password", "did:plc:viewerfirst", true); const viewer_row = try createSpace(allocator, .{ .actor_did = viewer.did, - .owner_did = owner.did, + .authority_did = owner.did, .space_type = "fm.plyr.privateMedia", .skey = "self", - .is_owner = false, + .is_authority = false, .managing_app = null, .is_public = false, .app_access_mode = "allow", .app_exceptions_json = "[]", }); - try std.testing.expect(!viewer_row.is_owner); + try std.testing.expect(!viewer_row.is_authority); const owner_row = try createSpace(allocator, .{ .actor_did = owner.did, - .owner_did = owner.did, + .authority_did = owner.did, .space_type = "fm.plyr.privateMedia", .skey = "self", - .is_owner = true, + .is_authority = true, .managing_app = "did:web:plyr.fm", .is_public = true, .app_access_mode = "deny", .app_exceptions_json = "[\"did:web:allowed.example\"]", }); - try std.testing.expect(owner_row.is_owner); + try std.testing.expect(owner_row.is_authority); try std.testing.expect(owner_row.is_public); try std.testing.expectEqualStrings("did:web:plyr.fm", owner_row.managing_app.?); try std.testing.expectError(Error.InvalidRefreshSession, createSpace(allocator, .{ .actor_did = viewer.did, - .owner_did = owner.did, + .authority_did = owner.did, .space_type = "fm.plyr.privateMedia", .skey = "self", - .is_owner = false, + .is_authority = false, .managing_app = null, .is_public = false, .app_access_mode = "allow", diff --git a/tools/smoke-permissioned.sh b/tools/smoke-permissioned.sh index e3d4a58..d5a43ae 100755 --- a/tools/smoke-permissioned.sh +++ b/tools/smoke-permissioned.sh @@ -69,33 +69,28 @@ test "$public_blob_status" = "404" space_create=$(curl -fsS -X POST "$base/xrpc/com.atproto.space.createSpace" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ - --data '{"did":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","managingApp":"did:web:plyr.fm","isPublic":false,"appAccessMode":"allow","appExceptions":[]}') + --data '{"authority":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","managingApp":"did:web:plyr.fm","isPublic":false,"appAccessMode":"allow","appExceptions":[]}') printf '%s' "$space_create" | grep -q '"uri":"ats://did:plc:permissionsmoke/fm.plyr.privateMedia/self"' ! printf '%s' "$space_create" | grep -q '"config":' space_duplicate_status=$(curl -sS -o /tmp/zds-space-duplicate.json -w '%{http_code}' -X POST "$base/xrpc/com.atproto.space.createSpace" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ - --data '{"did":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","isPublic":false}') + --data '{"authority":"did:plc:permissionsmoke","type":"fm.plyr.privateMedia","skey":"self","isPublic":false}') test "$space_duplicate_status" = "400" grep -q '"error":"SpaceAlreadyExists"' /tmp/zds-space-duplicate.json space_get=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.getSpace?space=$encoded_space") -printf '%s' "$space_get" | grep -q '"isOwner":true' +printf '%s' "$space_get" | grep -q '"isAuthority":true' printf '%s' "$space_get" | grep -q '"managingApp":"did:web:plyr.fm"' ! printf '%s' "$space_get" | grep -q '"members":' -space_list=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.listSpaces?did=did:plc:permissionsmoke&type=fm.plyr.privateMedia") +space_list=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.listSpaces?authority=did:plc:permissionsmoke&type=fm.plyr.privateMedia") printf '%s' "$space_list" | grep -q '"uri":"ats://did:plc:permissionsmoke/fm.plyr.privateMedia/self"' -printf '%s' "$space_list" | grep -q '"isOwner":true' +printf '%s' "$space_list" | grep -q '"isAuthority":true' printf '%s' "$space_list" | grep -q '"cursor":"ats://did:plc:permissionsmoke/fm.plyr.privateMedia/self"' ! printf '%s' "$space_list" | grep -q '"isMember":' -space_members_status=$(curl -sS -o /tmp/zds-space-members.json -w '%{http_code}' \ - -H "authorization: Bearer $token" "$base/xrpc/com.atproto.space.getMembers?space=$encoded_space&limit=1") -test "$space_members_status" = "404" -grep -q '"error":"UnknownMethod"' /tmp/zds-space-members.json - space_record=$(curl -fsS -X POST "$base/xrpc/com.atproto.space.createRecord" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \