diff --git a/src/core/oauth.zig b/src/core/oauth.zig index 79f427d..b24acd5 100644 --- a/src/core/oauth.zig +++ b/src/core/oauth.zig @@ -21,14 +21,16 @@ const session = @import("session.zig"); // collections (threads, posts, invites, join events). pub const scope = "atproto repo:* space:at.linji.space?authority=*&action=read&action=create&action=update&action=delete&collection=at.linji.post&collection=at.linji.thread&collection=at.linji.invite&collection=at.linji.join&manage=create&manage=update&manage=delete"; -/// The granular rpc permission bsky.social requires before it mints a -/// service-auth JWT (getServiceAuth) that lets the bootstrap adopt the DID -/// on the space host. The aud is the space host's DID. The legacy -/// `atproto` scope is composed at the call sites (PAR requests need it; -/// the client metadata must not declare it twice). -pub fn externalScope(allocator: std.mem.Allocator, space_did: []const u8) ![]u8 { - return std.fmt.allocPrint(allocator, "rpc:com.atproto.server.createAccount?aud={s}", .{space_did}); -} +/// Scope for the external home-PDS leg. bsky.social silently drops +/// aud-specific rpc scopes from the granted token (accepted at PAR, absent +/// from the access token - see docs/post/000.dj), then 403s getServiceAuth +/// for lacking it. `transition:generic` is the registry migration set that +/// its PDS grants and whose rpc permission covers service-auth minting for +/// any non-chat method (ScopePermissionsTransition.allowsRpc) - all our +/// bootstrap needs (lxm=com.atproto.server.createAccount). The legacy +/// `atproto` token is composed at the call sites (PAR requests need it; the +/// client metadata must not declare it twice). +pub const external_scope = "transition:generic"; const AsEndpoints = struct { issuer: []const u8, @@ -193,20 +195,17 @@ pub const PendingLogin = struct { /// Serve this at {base_url}/client-metadata.json (stable client identity). /// The metadata declares every scope this client may request, including the -/// rpc scope external PDSes check before minting service-auth JWTs. +/// external-leg scope (see `external_scope`). pub fn clientMetadata( allocator: std.mem.Allocator, store: *accounts.Store, io: std.Io, base_url: []const u8, - space_did: []const u8, ) ![]u8 { const client_kp = try clientKeypair(store, io); const client_id = try std.fmt.allocPrint(allocator, "{s}/client-metadata.json", .{base_url}); const redirect_uri = try std.fmt.allocPrint(allocator, "{s}/oauth/callback", .{base_url}); - const ext = try externalScope(allocator, space_did); - defer allocator.free(ext); - const metadata_scope = try std.fmt.allocPrint(allocator, "{s} {s}", .{ scope, ext }); + const metadata_scope = try std.fmt.allocPrint(allocator, "{s} {s}", .{ scope, external_scope }); defer allocator.free(metadata_scope); return zat.oauth.clientMetadataJson(allocator, .{ .client_id = client_id, @@ -265,19 +264,15 @@ pub fn beginLoginAt( const client_id = try std.fmt.allocPrint(allocator, "{s}/client-metadata.json", .{base_url}); const redirect_uri = try std.fmt.allocPrint(allocator, "{s}/oauth/callback", .{base_url}); - // External home PDS: request the rpc scope it demands for minting the - // service-auth JWT (see externalScope); the space host itself gets the + // External home PDS: request the scope it demands for minting the + // service-auth JWT (see external_scope); the space host itself gets the // full linji scope. var ext_scope: ?[]u8 = null; defer if (ext_scope) |s| allocator.free(s); const requested_scope: []const u8 = if (std.mem.eql(u8, pds_url, space_pds)) scope else blk: { - const space_did = try describeServerDid(allocator, io, space_pds); - defer allocator.free(space_did); - const rpc_scope = try externalScope(allocator, space_did); - defer allocator.free(rpc_scope); - ext_scope = try std.fmt.allocPrint(allocator, "atproto {s}", .{rpc_scope}); + ext_scope = try std.fmt.allocPrint(allocator, "atproto {s}", .{external_scope}); break :blk ext_scope.?; }; @@ -681,11 +676,11 @@ fn createExternalAccount( return error.BootstrapFailed; } -test "externalScope carries the space host audience" { +test "external scope is the transition set composed with atproto" { const allocator = std.testing.allocator; - const s = try externalScope(allocator, "did:web:zds.linji.at"); + const s = try std.fmt.allocPrint(allocator, "atproto {s}", .{external_scope}); defer allocator.free(s); - try std.testing.expectEqualStrings("rpc:com.atproto.server.createAccount?aud=did:web:zds.linji.at", s); + try std.testing.expectEqualStrings("atproto transition:generic", s); } test "isHostedHandle only matches our own domain" { diff --git a/src/server.zig b/src/server.zig index ea72488..4c0f0b8 100644 --- a/src/server.zig +++ b/src/server.zig @@ -81,11 +81,7 @@ pub fn serve( port: u16, ) !void { const zds_invite: ?[]const u8 = if (std.c.getenv("LINJI_ZDS_INVITE")) |v| std.mem.span(v) else null; - // The client metadata must declare the rpc scope external PDSes check - // before minting service-auth JWTs, so resolve the space host's DID up - // front. The app is useless without zds anyway - fail fast and clearly. - const space_did = try oauth.describeServerDid(allocator, io, pds); - const metadata = try oauth.clientMetadata(allocator, store, io, base_url, space_did); + const metadata = try oauth.clientMetadata(allocator, store, io, base_url); var app: App = .{ .allocator = allocator, .io = io,