Laravel AT Protocol Client (alpha & unstable)
1<?php
2
3namespace SocialDept\AtpClient;
4
5use Illuminate\Routing\Router;
6use Illuminate\Support\Facades\Route;
7use Illuminate\Support\ServiceProvider;
8use SocialDept\AtpClient\Auth\ClientAssertionManager;
9use SocialDept\AtpClient\Auth\ClientMetadataManager;
10use SocialDept\AtpClient\Auth\DPoPKeyManager;
11use SocialDept\AtpClient\Auth\DPoPNonceManager;
12use SocialDept\AtpClient\Auth\OAuthEngine;
13use SocialDept\AtpClient\Auth\ScopeChecker;
14use SocialDept\AtpClient\Auth\ScopeGate;
15use SocialDept\AtpClient\Auth\TokenRefresher;
16use SocialDept\AtpClient\Enums\ScopeEnforcementLevel;
17use SocialDept\AtpClient\Http\Middleware\RequiresScopeMiddleware;
18use SocialDept\AtpClient\Console\GenerateOAuthKeyCommand;
19use SocialDept\AtpClient\Console\MakeAtpClientCommand;
20use SocialDept\AtpClient\Console\MakeAtpRequestCommand;
21use SocialDept\AtpClient\Contracts\CredentialProvider;
22use SocialDept\AtpClient\Contracts\KeyStore;
23use SocialDept\AtpClient\Http\Controllers\ClientMetadataController;
24use SocialDept\AtpClient\Http\Controllers\JwksController;
25use SocialDept\AtpClient\Http\DPoPClient;
26use SocialDept\AtpClient\Session\SessionManager;
27use SocialDept\AtpClient\Storage\EncryptedFileKeyStore;
28
29class AtpClientServiceProvider extends ServiceProvider
30{
31 /**
32 * Register any package services.
33 */
34 public function register(): void
35 {
36 $this->mergeConfigFrom(__DIR__.'/../config/client.php', 'atp-client');
37
38 // Register contracts
39 $this->app->singleton(CredentialProvider::class, function ($app) {
40 $provider = config('client.credential_provider');
41
42 return new $provider();
43 });
44
45 $this->app->singleton(KeyStore::class, function ($app) {
46 return new EncryptedFileKeyStore(
47 storage_path('app/atp-keys')
48 );
49 });
50
51 // Register core services
52 $this->app->singleton(ClientMetadataManager::class);
53 $this->app->singleton(ClientAssertionManager::class);
54 $this->app->singleton(DPoPKeyManager::class);
55 $this->app->singleton(DPoPNonceManager::class);
56 $this->app->singleton(DPoPClient::class);
57 $this->app->singleton(TokenRefresher::class);
58 $this->app->singleton(SessionManager::class, function ($app) {
59 return new SessionManager(
60 credentials: $app->make(CredentialProvider::class),
61 refresher: $app->make(TokenRefresher::class),
62 dpopManager: $app->make(DPoPKeyManager::class),
63 keyStore: $app->make(KeyStore::class),
64 refreshThreshold: config('client.session.refresh_threshold', 300),
65 );
66 });
67 $this->app->singleton(OAuthEngine::class);
68 $this->app->singleton(ScopeChecker::class, function ($app) {
69 return new ScopeChecker(
70 config('atp-client.scope_enforcement', ScopeEnforcementLevel::Permissive)
71 );
72 });
73
74 // Register ScopeGate for AtpScope facade
75 $this->app->singleton('atp-scope', function ($app) {
76 return new ScopeGate(
77 $app->make(SessionManager::class),
78 $app->make(ScopeChecker::class),
79 );
80 });
81
82 // Register main client facade accessor
83 $this->app->bind('atp-client', function ($app) {
84 return new class($app)
85 {
86 protected $app;
87
88 protected ?CredentialProvider $defaultProvider = null;
89
90 public function __construct($app)
91 {
92 $this->app = $app;
93 }
94
95 public function as(string $actor): AtpClient
96 {
97 return new AtpClient(
98 $this->app->make(SessionManager::class),
99 $actor
100 );
101 }
102
103 public function login(string $actor, string $password): AtpClient
104 {
105 $this->app->make(SessionManager::class)
106 ->fromAppPassword($actor, $password);
107
108 return $this->as($actor);
109 }
110
111 public function oauth(): OAuthEngine
112 {
113 return $this->app->make(OAuthEngine::class);
114 }
115
116 public function setDefaultProvider(CredentialProvider $provider): void
117 {
118 $this->defaultProvider = $provider;
119 $this->app->instance(CredentialProvider::class, $provider);
120 }
121
122 public function public(?string $service = null): AtpClient
123 {
124 return new AtpClient(
125 sessions: null,
126 did: null,
127 serviceUrl: $service ?? config('atp-client.public.service_url', 'https://public.api.bsky.app')
128 );
129 }
130 };
131 });
132 }
133
134 /**
135 * Perform post-registration booting of services.
136 */
137 public function boot(): void
138 {
139 if ($this->app->runningInConsole()) {
140 $this->publishes([
141 __DIR__.'/../config/client.php' => config_path('client.php'),
142 ], 'atp-client-config');
143
144 $this->commands([
145 GenerateOAuthKeyCommand::class,
146 MakeAtpClientCommand::class,
147 MakeAtpRequestCommand::class,
148 ]);
149 }
150
151 $this->registerRoutes();
152 $this->registerMiddleware();
153 }
154
155 /**
156 * Register middleware aliases
157 */
158 protected function registerMiddleware(): void
159 {
160 /** @var Router $router */
161 $router = $this->app->make(Router::class);
162 $router->aliasMiddleware('atp.scope', RequiresScopeMiddleware::class);
163 }
164
165 /**
166 * Register OAuth metadata routes
167 */
168 protected function registerRoutes(): void
169 {
170 if (config('client.oauth.disabled')) {
171 return;
172 }
173
174 $prefix = config('client.oauth.prefix', '/atp/oauth/');
175
176 Route::prefix($prefix)->group(function () {
177 Route::get('client-metadata.json', ClientMetadataController::class)
178 ->name('atp.oauth.client-metadata');
179
180 Route::get('jwks.json', JwksController::class)
181 ->name('atp.oauth.jwks');
182 });
183
184 // Register recommended client id convention (see: https://atproto.com/guides/oauth#clients)
185 Route::get('oauth-client-metadata.json', ClientMetadataController::class)
186 ->name('atp.oauth.json');
187 }
188
189 /**
190 * Get the services provided by the provider.
191 *
192 * @return array<string>
193 */
194 public function provides(): array
195 {
196 return ['atp-client', 'atp-scope'];
197 }
198}