···1+<?php
2+3+namespace SocialDept\AtpClient\Attributes;
4+5+use Attribute;
6+7+/**
8+ * Documents that a method is a public endpoint that does not require authentication.
9+ *
10+ * This attribute currently serves as documentation to indicate which AT Protocol
11+ * endpoints can be called without an authenticated session. It helps developers
12+ * understand which endpoints work with `Atp::public()` against public API endpoints
13+ * like `https://public.api.bsky.app`.
14+ *
15+ * While this attribute does not currently perform runtime enforcement, scope
16+ * validation will be implemented in a future release. Correctly attributing
17+ * endpoints now ensures forward compatibility when enforcement is enabled.
18+ *
19+ * Public endpoints typically include operations like:
20+ * - Reading public profiles and posts
21+ * - Searching actors and content
22+ * - Resolving handles to DIDs
23+ * - Accessing repository data (sync endpoints)
24+ * - Describing servers and feed generators
25+ *
26+ * @example Basic usage
27+ * ```php
28+ * #[PublicEndpoint]
29+ * public function getProfile(string $actor): ProfileViewDetailed
30+ * ```
31+ *
32+ * @see \SocialDept\AtpClient\Attributes\ScopedEndpoint For endpoints that require authentication
33+ */
34+#[Attribute(Attribute::TARGET_METHOD)]
35+class PublicEndpoint
36+{
37+ /**
38+ * @param string $description Human-readable description of the endpoint
39+ */
40+ public function __construct(
41+ public readonly string $description = '',
42+ ) {}
43+}
···9 /**
10 * The parent AtpClient instance we belong to
11 */
12- public AtpClient $atp;
1314 public function __construct($parent)
15 {
16- $this->atp = $parent->atp;
17 }
18}
···9 /**
10 * The parent AtpClient instance we belong to
11 */
12+ protected AtpClient $atp;
1314 public function __construct($parent)
15 {
16+ $this->atp = $parent->root();
17 }
18}
···23namespace SocialDept\AtpClient\Data;
45-class StrongRef
000006{
7 public function __construct(
8 public readonly string $uri,
···23namespace SocialDept\AtpClient\Data;
45+use Illuminate\Contracts\Support\Arrayable;
6+7+/**
8+ * @implements Arrayable<string, string>
9+ */
10+class StrongRef implements Arrayable
11{
12 public function __construct(
13 public readonly string $uri,
+12
src/Enums/Nsid/AtprotoIdentity.php
···000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum AtprotoIdentity: string
8+{
9+ use HasScopeHelpers;
10+ case ResolveHandle = 'com.atproto.identity.resolveHandle';
11+ case UpdateHandle = 'com.atproto.identity.updateHandle';
12+}
+17
src/Enums/Nsid/AtprotoRepo.php
···00000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum AtprotoRepo: string
8+{
9+ use HasScopeHelpers;
10+ case CreateRecord = 'com.atproto.repo.createRecord';
11+ case DeleteRecord = 'com.atproto.repo.deleteRecord';
12+ case PutRecord = 'com.atproto.repo.putRecord';
13+ case GetRecord = 'com.atproto.repo.getRecord';
14+ case ListRecords = 'com.atproto.repo.listRecords';
15+ case UploadBlob = 'com.atproto.repo.uploadBlob';
16+ case DescribeRepo = 'com.atproto.repo.describeRepo';
17+}
+14
src/Enums/Nsid/AtprotoServer.php
···00000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum AtprotoServer: string
8+{
9+ use HasScopeHelpers;
10+ case CreateSession = 'com.atproto.server.createSession';
11+ case RefreshSession = 'com.atproto.server.refreshSession';
12+ case GetSession = 'com.atproto.server.getSession';
13+ case DescribeServer = 'com.atproto.server.describeServer';
14+}
+19
src/Enums/Nsid/AtprotoSync.php
···0000000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum AtprotoSync: string
8+{
9+ use HasScopeHelpers;
10+ case GetBlob = 'com.atproto.sync.getBlob';
11+ case GetRepo = 'com.atproto.sync.getRepo';
12+ case ListRepos = 'com.atproto.sync.listRepos';
13+ case ListReposByCollection = 'com.atproto.sync.listReposByCollection';
14+ case GetLatestCommit = 'com.atproto.sync.getLatestCommit';
15+ case GetRecord = 'com.atproto.sync.getRecord';
16+ case ListBlobs = 'com.atproto.sync.listBlobs';
17+ case GetBlocks = 'com.atproto.sync.getBlocks';
18+ case GetRepoStatus = 'com.atproto.sync.getRepoStatus';
19+}
+18
src/Enums/Nsid/BskyActor.php
···000000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum BskyActor: string
8+{
9+ use HasScopeHelpers;
10+ case GetProfile = 'app.bsky.actor.getProfile';
11+ case GetProfiles = 'app.bsky.actor.getProfiles';
12+ case GetSuggestions = 'app.bsky.actor.getSuggestions';
13+ case SearchActors = 'app.bsky.actor.searchActors';
14+ case SearchActorsTypeahead = 'app.bsky.actor.searchActorsTypeahead';
15+16+ // Record type
17+ case Profile = 'app.bsky.actor.profile';
18+}
+29
src/Enums/Nsid/BskyFeed.php
···00000000000000000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum BskyFeed: string
8+{
9+ use HasScopeHelpers;
10+ case DescribeFeedGenerator = 'app.bsky.feed.describeFeedGenerator';
11+ case GetAuthorFeed = 'app.bsky.feed.getAuthorFeed';
12+ case GetActorFeeds = 'app.bsky.feed.getActorFeeds';
13+ case GetActorLikes = 'app.bsky.feed.getActorLikes';
14+ case GetFeed = 'app.bsky.feed.getFeed';
15+ case GetFeedGenerator = 'app.bsky.feed.getFeedGenerator';
16+ case GetFeedGenerators = 'app.bsky.feed.getFeedGenerators';
17+ case GetLikes = 'app.bsky.feed.getLikes';
18+ case GetPostThread = 'app.bsky.feed.getPostThread';
19+ case GetPosts = 'app.bsky.feed.getPosts';
20+ case GetQuotes = 'app.bsky.feed.getQuotes';
21+ case GetRepostedBy = 'app.bsky.feed.getRepostedBy';
22+ case GetSuggestedFeeds = 'app.bsky.feed.getSuggestedFeeds';
23+ case GetTimeline = 'app.bsky.feed.getTimeline';
24+ case SearchPosts = 'app.bsky.feed.searchPosts';
25+26+ // Record types
27+ case Post = 'app.bsky.feed.post';
28+ case Like = 'app.bsky.feed.like';
29+}
+22
src/Enums/Nsid/BskyGraph.php
···0000000000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum BskyGraph: string
8+{
9+ use HasScopeHelpers;
10+ case GetFollowers = 'app.bsky.graph.getFollowers';
11+ case GetFollows = 'app.bsky.graph.getFollows';
12+ case GetKnownFollowers = 'app.bsky.graph.getKnownFollowers';
13+ case GetList = 'app.bsky.graph.getList';
14+ case GetLists = 'app.bsky.graph.getLists';
15+ case GetRelationships = 'app.bsky.graph.getRelationships';
16+ case GetStarterPack = 'app.bsky.graph.getStarterPack';
17+ case GetStarterPacks = 'app.bsky.graph.getStarterPacks';
18+ case GetSuggestedFollowsByActor = 'app.bsky.graph.getSuggestedFollowsByActor';
19+20+ // Record type
21+ case Follow = 'app.bsky.graph.follow';
22+}
+11
src/Enums/Nsid/BskyLabeler.php
···00000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum BskyLabeler: string
8+{
9+ use HasScopeHelpers;
10+ case GetServices = 'app.bsky.labeler.getServices';
11+}
+13
src/Enums/Nsid/ChatActor.php
···0000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum ChatActor: string
8+{
9+ use HasScopeHelpers;
10+ case GetActorMetadata = 'chat.bsky.actor.getActorMetadata';
11+ case ExportAccountData = 'chat.bsky.actor.exportAccountData';
12+ case DeleteAccount = 'chat.bsky.actor.deleteAccount';
13+}
+22
src/Enums/Nsid/ChatConvo.php
···0000000000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum ChatConvo: string
8+{
9+ use HasScopeHelpers;
10+ case GetConvo = 'chat.bsky.convo.getConvo';
11+ case GetConvoForMembers = 'chat.bsky.convo.getConvoForMembers';
12+ case ListConvos = 'chat.bsky.convo.listConvos';
13+ case GetMessages = 'chat.bsky.convo.getMessages';
14+ case SendMessage = 'chat.bsky.convo.sendMessage';
15+ case SendMessageBatch = 'chat.bsky.convo.sendMessageBatch';
16+ case DeleteMessageForSelf = 'chat.bsky.convo.deleteMessageForSelf';
17+ case UpdateRead = 'chat.bsky.convo.updateRead';
18+ case MuteConvo = 'chat.bsky.convo.muteConvo';
19+ case UnmuteConvo = 'chat.bsky.convo.unmuteConvo';
20+ case LeaveConvo = 'chat.bsky.convo.leaveConvo';
21+ case GetLog = 'chat.bsky.convo.getLog';
22+}
+34
src/Enums/Nsid/Concerns/HasScopeHelpers.php
···0000000000000000000000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid\Concerns;
4+5+trait HasScopeHelpers
6+{
7+ /**
8+ * Get the RPC scope format for this NSID.
9+ *
10+ * @example BskyActor::GetProfile->rpc() // "rpc:app.bsky.actor.getProfile"
11+ */
12+ public function rpc(): string
13+ {
14+ return 'rpc:' . $this->value;
15+ }
16+17+ /**
18+ * Get the repo scope format for this NSID.
19+ *
20+ * @example BskyGraph::Follow->repo(['create']) // "repo:app.bsky.graph.follow?action=create"
21+ * @example BskyFeed::Post->repo(['create', 'delete']) // "repo:app.bsky.feed.post?action=create&action=delete"
22+ * @example BskyFeed::Post->repo() // "repo:app.bsky.feed.post"
23+ */
24+ public function repo(array $actions = []): string
25+ {
26+ $scope = 'repo:' . $this->value;
27+28+ if (! empty($actions)) {
29+ $scope .= '?' . implode('&', array_map(fn ($action) => "action={$action}", $actions));
30+ }
31+32+ return $scope;
33+ }
34+}
+18
src/Enums/Nsid/OzoneModeration.php
···000000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum OzoneModeration: string
8+{
9+ use HasScopeHelpers;
10+ case GetEvent = 'tools.ozone.moderation.getEvent';
11+ case GetEvents = 'tools.ozone.moderation.getEvents';
12+ case GetRecord = 'tools.ozone.moderation.getRecord';
13+ case GetRepo = 'tools.ozone.moderation.getRepo';
14+ case QueryEvents = 'tools.ozone.moderation.queryEvents';
15+ case QueryStatuses = 'tools.ozone.moderation.queryStatuses';
16+ case SearchRepos = 'tools.ozone.moderation.searchRepos';
17+ case EmitEvent = 'tools.ozone.moderation.emitEvent';
18+}
+12
src/Enums/Nsid/OzoneServer.php
···000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum OzoneServer: string
8+{
9+ use HasScopeHelpers;
10+ case GetBlob = 'tools.ozone.server.getBlob';
11+ case GetConfig = 'tools.ozone.server.getConfig';
12+}
+15
src/Enums/Nsid/OzoneTeam.php
···000000000000000
···1+<?php
2+3+namespace SocialDept\AtpClient\Enums\Nsid;
4+5+use SocialDept\AtpClient\Enums\Nsid\Concerns\HasScopeHelpers;
6+7+enum OzoneTeam: string
8+{
9+ use HasScopeHelpers;
10+ case GetMember = 'tools.ozone.team.getMember';
11+ case ListMembers = 'tools.ozone.team.listMembers';
12+ case AddMember = 'tools.ozone.team.addMember';
13+ case UpdateMember = 'tools.ozone.team.updateMember';
14+ case DeleteMember = 'tools.ozone.team.deleteMember';
15+}
+10-5
src/Enums/Scope.php
···23namespace SocialDept\AtpClient\Enums;
4005enum Scope: string
6{
7 // Transition scopes (current)
···13 /**
14 * Build a repo scope string for record operations.
15 *
16- * @param string $collection The collection NSID (e.g., 'app.bsky.feed.post')
17- * @param string|null $action The action (create, update, delete)
0018 */
19- public static function repo(string $collection, ?string $action = null): string
20 {
021 $scope = "repo:{$collection}";
2223- if ($action !== null) {
24- $scope .= "?action={$action}";
25 }
2627 return $scope;