···33return [
44 /*
55 |--------------------------------------------------------------------------
66- | Client Metadata
66+ | Client Configuration
77 |--------------------------------------------------------------------------
88 |
99- | OAuth client configuration. The metadata URL must be publicly accessible
1010- | and serve the client-metadata.json file.
99+ | OAuth client configuration. The client_id is a URL that serves as the
1010+ | unique identifier for your OAuth client. In production, this must be
1111+ | an HTTPS URL pointing to your publicly accessible client metadata.
1212+ |
1313+ | For local development, use 'http://localhost' (no port) as the client_id.
1414+ | The redirect_uri for localhost must use 127.0.0.1 with a port.
1515+ |
1616+ | @see https://atproto.com/specs/oauth#clients
1117 |
1218 */
1319 'client' => [
1420 'name' => env('ATP_CLIENT_NAME', config('app.name')),
1521 'url' => env('ATP_CLIENT_URL', config('app.url')),
1616- 'metadata_url' => env('ATP_CLIENT_METADATA_URL'),
1717- 'redirect_uris' => [
1818- env('ATP_CLIENT_REDIRECT_URI', config('app.url').'/auth/atp/callback'),
1919- ],
2222+2323+ // The client_id is the URL to your client metadata document.
2424+ // For production: 'https://example.com/oauth/client-metadata.json'
2525+ // For localhost: 'http://localhost' (exactly, no port)
2626+ 'client_id' => env('ATP_CLIENT_ID'),
2727+2828+ // Redirect URIs for OAuth callback.
2929+ // For localhost development, use 'http://127.0.0.1:<port>/callback'
3030+ 'redirect_uris' => array_filter([
3131+ env('ATP_CLIENT_REDIRECT_URI'),
3232+ ]),
3333+2034 'scopes' => ['atproto', 'transition:generic'],
2135 ],
2236
+89-9
src/Auth/ClientMetadataManager.php
···2233namespace SocialDept\AtpClient\Auth;
4455+/**
66+ * Manages OAuth client metadata for AT Protocol authentication.
77+ *
88+ * The client_id in atproto OAuth is a URL that serves as both the unique
99+ * identifier and the location of the client metadata document.
1010+ *
1111+ * For production: Use an HTTPS URL pointing to your client metadata.
1212+ * For localhost: Use exactly 'http://localhost' (no port).
1313+ *
1414+ * @see https://atproto.com/specs/oauth#clients
1515+ */
516class ClientMetadataManager
617{
718 /**
88- * Get the client ID (typically the client URL)
1919+ * Get the client ID (URL to client metadata document).
2020+ *
2121+ * For production clients, this is an HTTPS URL like:
2222+ * 'https://example.com/oauth/client-metadata.json'
2323+ *
2424+ * For localhost development, this must be exactly 'http://localhost'
2525+ * (no port number allowed per atproto spec).
926 */
1027 public function getClientId(): string
1128 {
1212- return config('client.client.url');
2929+ $clientId = config('client.client.client_id');
3030+3131+ if ($clientId) {
3232+ return $clientId;
3333+ }
3434+3535+ // Fall back to auto-generated client_id based on app URL
3636+ return $this->generateClientId();
1337 }
14381539 /**
1616- * Get the client metadata URL
4040+ * Check if this is a localhost development client.
1741 */
1818- public function getMetadataUrl(): ?string
4242+ public function isLocalhost(): bool
1943 {
2020- return config('client.client.metadata_url');
4444+ return $this->getClientId() === 'http://localhost';
2145 }
22462347 /**
2424- * Get the redirect URIs
4848+ * Get the redirect URIs.
4949+ *
5050+ * For localhost development, redirect URIs must use 127.0.0.1
5151+ * (not localhost) and can include a port number.
2552 *
2653 * @return array<string>
2754 */
2855 public function getRedirectUris(): array
2956 {
3030- return config('client.client.redirect_uris', []);
5757+ $uris = config('client.client.redirect_uris', []);
5858+5959+ if (! empty($uris)) {
6060+ return $uris;
6161+ }
6262+6363+ // Default redirect URI based on environment
6464+ if ($this->isLocalhost()) {
6565+ // For localhost, use 127.0.0.1
6666+ return ['http://127.0.0.1'];
6767+ }
6868+6969+ // For production, use app URL
7070+ return [config('client.client.url').'/auth/atp/callback'];
3171 }
32723373 /**
3434- * Get the OAuth scopes
7474+ * Get the OAuth scopes.
3575 *
3676 * @return array<string>
3777 */
···4181 }
42824383 /**
4444- * Get the client metadata as an array
8484+ * Get the client metadata as an array.
8585+ *
8686+ * This is the structure served at the client_id URL.
4587 *
4688 * @return array<string, mixed>
4789 */
···62104 'application_type' => 'web',
63105 'dpop_bound_access_tokens' => true,
64106 ];
107107+ }
108108+109109+ /**
110110+ * Generate client_id from app configuration.
111111+ *
112112+ * In production, points to the package's client-metadata.json endpoint.
113113+ * For localhost detection, checks if app URL contains localhost or .test.
114114+ */
115115+ protected function generateClientId(): string
116116+ {
117117+ $appUrl = config('client.client.url') ?? config('app.url');
118118+ $host = parse_url($appUrl, PHP_URL_HOST);
119119+120120+ // Detect local development environments
121121+ if ($this->isLocalDevelopment($host)) {
122122+ return 'http://localhost';
123123+ }
124124+125125+ // Production: point to client metadata endpoint
126126+ $prefix = config('client.oauth.prefix', '/atp/oauth/');
127127+128128+ return rtrim($appUrl, '/').rtrim($prefix, '/').'/client-metadata.json';
129129+ }
130130+131131+ /**
132132+ * Check if the host indicates a local development environment.
133133+ */
134134+ protected function isLocalDevelopment(?string $host): bool
135135+ {
136136+ if (! $host) {
137137+ return false;
138138+ }
139139+140140+ return $host === 'localhost'
141141+ || $host === '127.0.0.1'
142142+ || str_ends_with($host, '.localhost')
143143+ || str_ends_with($host, '.test')
144144+ || str_ends_with($host, '.local');
65145 }
66146}