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