@@ -4,6 +4,7 @@ import type { ATProtoSDKConfig } from "../core/config.js";
44import { AuthenticationError , NetworkError } from "../core/errors.js" ;
55import { InMemorySessionStore } from "../storage/InMemorySessionStore.js" ;
66import { InMemoryStateStore } from "../storage/InMemoryStateStore.js" ;
7+ import { parseScope , validateScope , ATPROTO_SCOPE } from "./permissions.js" ;
78
89/**
910 * Options for the OAuth authorization flow.
@@ -179,7 +180,7 @@ export class OAuthClient {
179180 */
180181 private buildClientMetadata ( ) {
181182 const clientIdUrl = new URL ( this . config . oauth . clientId ) ;
182- return {
183+ const metadata = {
183184 client_id : this . config . oauth . clientId ,
184185 client_name : "ATProto SDK Client" ,
185186 client_uri : clientIdUrl . origin ,
@@ -193,6 +194,89 @@ export class OAuthClient {
193194 dpop_bound_access_tokens : true ,
194195 jwks_uri : this . config . oauth . jwksUri ,
195196 } as const ;
197+
198+ // Validate scope before returning metadata
199+ this . validateClientMetadataScope ( metadata . scope ) ;
200+
201+ return metadata ;
202+ }
203+
204+ /**
205+ * Validates the OAuth scope in client metadata and logs warnings/suggestions.
206+ *
207+ * This method:
208+ * 1. Checks if the scope is well-formed using permission utilities
209+ * 2. Detects mixing of transitional and granular permissions
210+ * 3. Logs warnings for missing `atproto` scope
211+ * 4. Suggests migration to granular permissions for transitional scopes
212+ *
213+ * @param scope - The OAuth scope string to validate
214+ * @internal
215+ */
216+ private validateClientMetadataScope ( scope : string ) : void {
217+ // Parse the scope into individual permissions
218+ const permissions = parseScope ( scope ) ;
219+
220+ // Validate well-formedness
221+ const validation = validateScope ( scope ) ;
222+ if ( ! validation . isValid ) {
223+ this . logger ?. error ( "Invalid OAuth scope detected" , {
224+ invalidPermissions : validation . invalidPermissions ,
225+ scope,
226+ } ) ;
227+ }
228+
229+ // Check for atproto scope
230+ const hasAtproto = permissions . includes ( ATPROTO_SCOPE ) ;
231+ if ( ! hasAtproto ) {
232+ this . logger ?. warn ( "OAuth scope missing 'atproto' - basic API access may be limited" , {
233+ scope,
234+ suggestion : "Add 'atproto' to your scope for basic API access" ,
235+ } ) ;
236+ }
237+
238+ // Detect transitional scopes
239+ const transitionalScopes = permissions . filter ( ( p ) => p . startsWith ( "transition:" ) ) ;
240+ const granularScopes = permissions . filter (
241+ ( p ) =>
242+ p . startsWith ( "account:" ) ||
243+ p . startsWith ( "repo:" ) ||
244+ p . startsWith ( "blob" ) ||
245+ p . startsWith ( "rpc:" ) ||
246+ p . startsWith ( "identity:" ) ||
247+ p . startsWith ( "include:" ) ,
248+ ) ;
249+
250+ // Log info about transitional scopes
251+ if ( transitionalScopes . length > 0 ) {
252+ this . logger ?. info ( "Using transitional OAuth scopes (legacy)" , {
253+ transitionalScopes,
254+ note : "Transitional scopes are supported but granular permissions are recommended" ,
255+ } ) ;
256+
257+ // Suggest migration to granular permissions
258+ if ( transitionalScopes . includes ( "transition:email" ) ) {
259+ this . logger ?. info ( "Consider migrating 'transition:email' to granular permissions" , {
260+ suggestion : "Use: account:email?action=read" ,
261+ example : "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.EMAIL_READ" ,
262+ } ) ;
263+ }
264+ if ( transitionalScopes . includes ( "transition:generic" ) ) {
265+ this . logger ?. info ( "Consider migrating 'transition:generic' to granular permissions" , {
266+ suggestion : "Use specific permissions like: repo:* account:repo?action=read" ,
267+ example : "import { ScopePresets } from '@hypercerts-org/sdk-core'; scope: ScopePresets.FULL_ACCESS" ,
268+ } ) ;
269+ }
270+ }
271+
272+ // Warn if mixing transitional and granular
273+ if ( transitionalScopes . length > 0 && granularScopes . length > 0 ) {
274+ this . logger ?. warn ( "Mixing transitional and granular OAuth scopes" , {
275+ transitionalScopes,
276+ granularScopes,
277+ note : "While supported, it's recommended to use either transitional or granular permissions consistently" ,
278+ } ) ;
279+ }
196280 }
197281
198282 /**
0 commit comments