atproto pds in zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387const std = @import("std");
pub const RepoAction = enum { create, update, delete };pub const SpaceAction = enum { read_self, read, create, update, delete, manage_create, manage_update, manage_delete };pub const AccountAction = enum { read, manage };pub const AccountAttr = enum { email, repo, status, wildcard };pub const IdentityAttr = enum { handle, wildcard };
pub fn oauthNeedsGranularCheck(maybe_scope: ?[]const u8) bool { const scope_text = maybe_scope orelse return false; return !std.mem.eql(u8, scope_text, "com.atproto.access") and !std.mem.eql(u8, scope_text, "atproto");}
pub fn repoAllows(maybe_scope: ?[]const u8, action: RepoAction, collection: []const u8) bool { if (!oauthNeedsGranularCheck(maybe_scope)) return true; var scopes = std.mem.splitScalar(u8, maybe_scope.?, ' '); while (scopes.next()) |scope| { if (scope.len == 0) continue; if (std.mem.eql(u8, scope, "transition:generic")) return true; if (!std.mem.startsWith(u8, scope, "repo")) continue; if (repoScopeMatches(scope, action, collection)) return true; } return false;}
pub fn blobAllows(maybe_scope: ?[]const u8, mime_type: []const u8) bool { if (!oauthNeedsGranularCheck(maybe_scope)) return true; var scopes = std.mem.splitScalar(u8, maybe_scope.?, ' '); while (scopes.next()) |scope| { if (scope.len == 0) continue; if (std.mem.eql(u8, scope, "transition:generic")) return true; if (!std.mem.startsWith(u8, scope, "blob")) continue; if (blobScopeMatches(scope, mime_type)) return true; } return false;}
pub fn rpcAllows(maybe_scope: ?[]const u8, aud: []const u8, lxm: []const u8) bool { if (!oauthNeedsGranularCheck(maybe_scope)) return true; var scopes = std.mem.splitScalar(u8, maybe_scope.?, ' '); while (scopes.next()) |scope| { if (scope.len == 0) continue; if (std.mem.eql(u8, scope, "transition:generic")) return true; if (!std.mem.startsWith(u8, scope, "rpc:")) continue; if (rpcScopeMatches(scope, aud, lxm)) return true; } return false;}
pub fn spaceAllows( maybe_scope: ?[]const u8, action: SpaceAction, space_type: []const u8, did: []const u8, skey: []const u8, collection: ?[]const u8,) bool { if (!oauthNeedsGranularCheck(maybe_scope)) return true; var scope_iter = std.mem.splitScalar(u8, maybe_scope.?, ' '); while (scope_iter.next()) |scope| { if (scope.len == 0) continue; if (std.mem.eql(u8, scope, "transition:generic")) return true; if (!std.mem.startsWith(u8, scope, "space:")) continue; if (spaceScopeMatches(scope, action, space_type, did, skey, collection)) return true; } return false;}
pub fn accountAllows(maybe_scope: ?[]const u8, attr: AccountAttr, action: AccountAction) bool { if (!oauthNeedsGranularCheck(maybe_scope)) return true; var scopes = std.mem.splitScalar(u8, maybe_scope.?, ' '); while (scopes.next()) |scope| { if (scope.len == 0) continue; if (std.mem.eql(u8, scope, "transition:generic")) return true; if (!std.mem.startsWith(u8, scope, "account")) continue; if (accountScopeMatches(scope, attr, action)) return true; } return false;}
pub fn identityAllows(maybe_scope: ?[]const u8, attr: IdentityAttr) bool { if (!oauthNeedsGranularCheck(maybe_scope)) return true; var scopes = std.mem.splitScalar(u8, maybe_scope.?, ' '); while (scopes.next()) |scope| { if (scope.len == 0) continue; if (std.mem.eql(u8, scope, "transition:generic")) return true; if (!std.mem.startsWith(u8, scope, "identity")) continue; if (identityScopeMatches(scope, attr)) return true; } return false;}
pub fn hasFullAccess(maybe_scope: ?[]const u8) bool { if (!oauthNeedsGranularCheck(maybe_scope)) return true; var scopes = std.mem.splitScalar(u8, maybe_scope.?, ' '); while (scopes.next()) |scope| { if (std.mem.eql(u8, scope, "atproto") or std.mem.eql(u8, scope, "transition:generic")) return true; } return false;}
fn accountScopeMatches(scope: []const u8, attr: AccountAttr, action: AccountAction) bool { const query_start = std.mem.indexOfScalar(u8, scope, '?'); const base = if (query_start) |idx| scope[0..idx] else scope; if (!accountAttrMatches(base, attr)) return false; if (query_start == null) return true;
var saw_action = false; var params = std.mem.splitScalar(u8, scope[query_start.? + 1 ..], '&'); while (params.next()) |param| { if (!std.mem.startsWith(u8, param, "action=")) continue; saw_action = true; if (std.mem.eql(u8, param["action=".len..], accountActionName(action))) return true; } return !saw_action;}
fn accountAttrMatches(base: []const u8, attr: AccountAttr) bool { if (std.mem.eql(u8, base, "account") or std.mem.eql(u8, base, "account:") or std.mem.eql(u8, base, "account:*")) return true; if (!std.mem.startsWith(u8, base, "account:")) return false; const allowed = base["account:".len..]; if (attr == .wildcard) return std.mem.eql(u8, allowed, "*"); return std.mem.eql(u8, allowed, accountAttrName(attr));}
fn accountActionName(action: AccountAction) []const u8 { return switch (action) { .read => "read", .manage => "manage", };}
fn accountAttrName(attr: AccountAttr) []const u8 { return switch (attr) { .email => "email", .repo => "repo", .status => "status", .wildcard => "*", };}
fn identityScopeMatches(scope: []const u8, attr: IdentityAttr) bool { const query_start = std.mem.indexOfScalar(u8, scope, '?'); const base = if (query_start) |idx| scope[0..idx] else scope; if (std.mem.eql(u8, base, "identity") or std.mem.eql(u8, base, "identity:") or std.mem.eql(u8, base, "identity:*")) return true; if (!std.mem.startsWith(u8, base, "identity:")) return false; const allowed = base["identity:".len..]; return switch (attr) { .handle => std.mem.eql(u8, allowed, "handle"), .wildcard => false, };}
fn repoScopeMatches(scope: []const u8, action: RepoAction, collection: []const u8) bool { const query_start = std.mem.indexOfScalar(u8, scope, '?'); const base = if (query_start) |idx| scope[0..idx] else scope; if (!repoScopeCollectionMatches(base, collection)) return false; if (query_start == null) return true;
var saw_action = false; var params = std.mem.splitScalar(u8, scope[query_start.? + 1 ..], '&'); while (params.next()) |param| { if (!std.mem.startsWith(u8, param, "action=")) continue; saw_action = true; if (std.mem.eql(u8, param["action=".len..], repoActionName(action))) return true; } return !saw_action;}
fn repoScopeCollectionMatches(base: []const u8, collection: []const u8) bool { if (std.mem.eql(u8, base, "repo") or std.mem.eql(u8, base, "repo:") or std.mem.eql(u8, base, "repo:*")) return true; if (!std.mem.startsWith(u8, base, "repo:")) return false; const allowed = base["repo:".len..]; if (std.mem.eql(u8, allowed, collection)) return true; if (std.mem.endsWith(u8, allowed, ".*")) { const prefix = allowed[0 .. allowed.len - 2]; return std.mem.startsWith(u8, collection, prefix) and collection.len > prefix.len and collection[prefix.len] == '.'; } return false;}
fn repoActionName(action: RepoAction) []const u8 { return switch (action) { .create => "create", .update => "update", .delete => "delete", };}
fn blobScopeMatches(scope: []const u8, mime_type: []const u8) bool { const query_start = std.mem.indexOfScalar(u8, scope, '?'); const base = if (query_start) |idx| scope[0..idx] else scope; if (blobMimeMatches(blobScopePattern(base), mime_type)) return true; if (query_start == null) return false;
var params = std.mem.splitScalar(u8, scope[query_start.? + 1 ..], '&'); while (params.next()) |param| { if (!std.mem.startsWith(u8, param, "accept=")) continue; if (blobMimeMatches(param["accept=".len..], mime_type)) return true; } return false;}
fn blobScopePattern(base: []const u8) []const u8 { if (std.mem.eql(u8, base, "blob") or std.mem.eql(u8, base, "blob:")) return "*/*"; if (!std.mem.startsWith(u8, base, "blob:")) return ""; return base["blob:".len..];}
fn blobMimeMatches(pattern: []const u8, mime_type: []const u8) bool { if (pattern.len == 0) return false; if (std.mem.eql(u8, pattern, "*/*")) return true; if (std.mem.eql(u8, pattern, mime_type)) return true; if (std.mem.endsWith(u8, pattern, "/*")) { const prefix = pattern[0 .. pattern.len - 2]; return std.mem.startsWith(u8, mime_type, prefix) and mime_type.len > prefix.len and mime_type[prefix.len] == '/'; } return false;}
fn rpcScopeMatches(scope: []const u8, aud: []const u8, lxm: []const u8) bool { const query_start = std.mem.indexOfScalar(u8, scope, '?'); const base = if (query_start) |idx| scope[0..idx] else scope; if (!rpcMethodMatches(base, lxm)) return false; if (query_start == null) return false;
var saw_aud = false; var params = std.mem.splitScalar(u8, scope[query_start.? + 1 ..], '&'); while (params.next()) |param| { if (!std.mem.startsWith(u8, param, "aud=")) continue; saw_aud = true; const allowed = param["aud=".len..]; if (std.mem.eql(u8, allowed, "*") or std.mem.eql(u8, allowed, aud)) return true; } return !saw_aud;}
fn rpcMethodMatches(base: []const u8, lxm: []const u8) bool { if (!std.mem.startsWith(u8, base, "rpc:")) return false; const allowed = base["rpc:".len..]; if (std.mem.eql(u8, allowed, "*") or std.mem.eql(u8, allowed, lxm)) return true; if (std.mem.endsWith(u8, allowed, ".*")) { const prefix = allowed[0 .. allowed.len - 2]; return std.mem.startsWith(u8, lxm, prefix) and lxm.len > prefix.len and lxm[prefix.len] == '.'; } return false;}
fn spaceScopeMatches( scope: []const u8, action: SpaceAction, space_type: []const u8, authority: []const u8, skey: []const u8, collection: ?[]const u8,) bool { const query_start = std.mem.indexOfScalar(u8, scope, '?'); const base = if (query_start) |idx| scope[0..idx] else scope; if (!spaceTypeMatches(base, space_type)) return false; if (query_start == null) return action == .read; var saw_action = false; var action_matches = false; var saw_manage = false; var manage_matches = false; var saw_collection = false; var collection_matches = false; var params = std.mem.splitScalar(u8, scope[query_start.? + 1 ..], '&'); while (params.next()) |param| { if (std.mem.startsWith(u8, param, "action=")) { saw_action = true; const allowed = param["action=".len..]; action_matches = action_matches or switch (action) { .read_self => std.mem.eql(u8, allowed, "read_self") or std.mem.eql(u8, allowed, "read"), .read, .create, .update, .delete => std.mem.eql(u8, allowed, spaceActionName(action)), else => false, }; } else if (std.mem.startsWith(u8, param, "manage=")) { saw_manage = true; manage_matches = manage_matches or std.mem.eql(u8, param["manage=".len..], spaceManageName(action)); } else if (std.mem.startsWith(u8, param, "authority=")) { const allowed = param["authority=".len..]; if (std.mem.eql(u8, allowed, "self")) return false; if (!std.mem.eql(u8, allowed, "*") and !std.mem.eql(u8, allowed, authority)) return false; } else if (std.mem.startsWith(u8, param, "skey=")) { const allowed = param["skey=".len..]; if (!std.mem.eql(u8, allowed, "*") and !std.mem.eql(u8, allowed, skey)) return false; } else if (std.mem.startsWith(u8, param, "collection=")) { if (action == .read or switch (action) { .manage_create, .manage_update, .manage_delete => true, else => false, }) continue; saw_collection = true; const requested = collection orelse return false; const allowed = param["collection=".len..]; collection_matches = collection_matches or collectionPatternMatches(allowed, requested); } } return switch (action) { .manage_create, .manage_update, .manage_delete => saw_manage and manage_matches, .read => if (saw_action) action_matches else true, .read_self => (if (saw_action) action_matches else true) and saw_collection and collection_matches, .create, .update, .delete => (if (saw_action) action_matches else true) and saw_collection and collection_matches, };}
fn spaceTypeMatches(base: []const u8, space_type: []const u8) bool { const prefix = "space:"; if (!std.mem.startsWith(u8, base, prefix)) return false; const allowed = base[prefix.len..]; return collectionPatternMatches(allowed, space_type);}
fn collectionPatternMatches(pattern: []const u8, value: []const u8) bool { if (std.mem.eql(u8, pattern, "*") or std.mem.eql(u8, pattern, value)) return true; if (std.mem.endsWith(u8, pattern, ".*")) { const prefix = pattern[0 .. pattern.len - 2]; return std.mem.startsWith(u8, value, prefix) and value.len > prefix.len and value[prefix.len] == '.'; } return false;}
fn spaceActionName(action: SpaceAction) []const u8 { return switch (action) { .read_self => "read_self", .read => "read", .create => "create", .update => "update", .delete => "delete", .manage_create, .manage_update, .manage_delete => "", };}
fn spaceManageName(action: SpaceAction) []const u8 { return switch (action) { .manage_create => "create", .manage_update => "update", .manage_delete => "delete", else => "", };}
test "repo scopes constrain action and collection" { try std.testing.expect(repoAllows("repo:*?action=create", .create, "app.bsky.feed.post")); try std.testing.expect(!repoAllows("repo:*?action=create", .delete, "app.bsky.feed.post")); try std.testing.expect(repoAllows("repo:app.bsky.feed.*", .update, "app.bsky.feed.like")); try std.testing.expect(!repoAllows("repo:app.bsky.feed.*", .update, "app.bsky.graph.follow"));}
test "blob scopes constrain mime types" { try std.testing.expect(blobAllows("blob:image/*", "image/jpeg")); try std.testing.expect(!blobAllows("blob:image/*", "video/mp4")); try std.testing.expect(blobAllows("blob?accept=image/*&accept=video/*", "video/mp4"));}
test "rpc scopes constrain method and audience" { try std.testing.expect(rpcAllows("rpc:app.bsky.feed.getTimeline?aud=did:web:api.bsky.app#bsky_appview", "did:web:api.bsky.app#bsky_appview", "app.bsky.feed.getTimeline")); try std.testing.expect(rpcAllows("rpc:*?aud=did:web:api.bsky.app#bsky_appview", "did:web:api.bsky.app#bsky_appview", "app.bsky.feed.getFeed")); try std.testing.expect(rpcAllows("rpc:app.bsky.feed.*?aud=*", "did:web:api.bsky.app#bsky_appview", "app.bsky.feed.getFeed")); try std.testing.expect(!rpcAllows("rpc:app.bsky.feed.*?aud=did:web:api.bsky.app#bsky_appview", "did:web:video.bsky.app#video", "app.bsky.feed.getFeed")); try std.testing.expect(!rpcAllows("repo:*", "did:web:api.bsky.app#bsky_appview", "app.bsky.feed.getFeed"));}
test "space scopes constrain type identity key action and collection" { try std.testing.expect(spaceAllows("space:fm.plyr.privateMedia?authority=did:plc:alice&skey=self&collection=fm.plyr.track&action=create", .create, "fm.plyr.privateMedia", "did:plc:alice", "self", "fm.plyr.track")); try std.testing.expect(!spaceAllows("space:fm.plyr.privateMedia?authority=did:plc:alice&skey=self&action=read", .create, "fm.plyr.privateMedia", "did:plc:alice", "self", "fm.plyr.track")); try std.testing.expect(spaceAllows("space:fm.plyr.*?authority=*&skey=*&action=read", .read, "fm.plyr.privateMedia", "did:plc:bob", "records", null)); try std.testing.expect(spaceAllows("space:fm.plyr.privateMedia?authority=did:plc:alice&collection=fm.plyr.track&action=read_self", .read_self, "fm.plyr.privateMedia", "did:plc:alice", "self", "fm.plyr.track")); try std.testing.expect(spaceAllows("space:fm.plyr.privateMedia?authority=did:plc:alice&collection=fm.plyr.track&action=read", .read, "fm.plyr.privateMedia", "did:plc:alice", "self", null)); try std.testing.expect(spaceAllows("space:fm.plyr.privateMedia?authority=did:plc:alice&manage=update", .manage_update, "fm.plyr.privateMedia", "did:plc:alice", "self", null)); try std.testing.expect(!spaceAllows("space:fm.plyr.privateMedia?authority=self&manage=update", .manage_update, "fm.plyr.privateMedia", "did:plc:alice", "self", null)); try std.testing.expect(!spaceAllows("repo:*", .read, "fm.plyr.privateMedia", "did:plc:bob", "records", null));}
test "account scopes constrain attribute and action" { try std.testing.expect(accountAllows("account:email?action=manage", .email, .manage)); try std.testing.expect(!accountAllows("account:email?action=read", .email, .manage)); try std.testing.expect(!accountAllows("account:repo?action=manage", .email, .manage)); try std.testing.expect(accountAllows("account:*", .repo, .manage));}
test "identity scopes constrain attribute" { try std.testing.expect(identityAllows("identity:handle", .handle)); try std.testing.expect(!identityAllows("identity:handle", .wildcard)); try std.testing.expect(identityAllows("identity:*", .wildcard));}