1+ import * as Schema from "effect/Schema" ;
2+
13const OAUTH_SCOPE_TOKEN = / ^ [ \u0021 \u0023 - \u005b \u005d - \u007e ] + $ / u;
24
5+ export class OAuthScopeEncodingError extends Schema . TaggedErrorClass < OAuthScopeEncodingError > ( ) (
6+ "OAuthScopeEncodingError" ,
7+ {
8+ scopes : Schema . Array ( Schema . String ) ,
9+ invalidScopes : Schema . Array ( Schema . String ) ,
10+ duplicateScopes : Schema . Array ( Schema . String ) ,
11+ } ,
12+ ) {
13+ override get message ( ) : string {
14+ return "OAuth scopes must be non-empty, syntactically valid, and unique." ;
15+ }
16+ }
17+
318/**
419 * Decodes an RFC 6749 `scope` value as a set while preserving its first-seen
520 * order for canonical responses and logs.
@@ -18,12 +33,22 @@ export function parseOAuthScope(value: string): ReadonlyArray<string> | null {
1833}
1934
2035export function encodeOAuthScope ( scopes : ReadonlyArray < string > ) : string {
21- const encoded = scopes . join ( " " ) ;
22- const parsed = parseOAuthScope ( encoded ) ;
23- if ( parsed === null || parsed . length !== scopes . length ) {
24- throw new Error ( "OAuth scopes must be non-empty, valid, and unique." ) ;
36+ const invalidScopes = scopes . filter ( ( scope ) => ! OAUTH_SCOPE_TOKEN . test ( scope ) ) ;
37+ const seen = new Set < string > ( ) ;
38+ const duplicateScopes = new Set < string > ( ) ;
39+ for ( const scope of scopes ) {
40+ if ( seen . has ( scope ) ) duplicateScopes . add ( scope ) ;
41+ seen . add ( scope ) ;
42+ }
43+
44+ if ( scopes . length === 0 || invalidScopes . length > 0 || duplicateScopes . size > 0 ) {
45+ throw new OAuthScopeEncodingError ( {
46+ scopes,
47+ invalidScopes,
48+ duplicateScopes : [ ...duplicateScopes ] ,
49+ } ) ;
2550 }
26- return encoded ;
51+ return scopes . join ( " " ) ;
2752}
2853
2954export function oauthScopeSetEquals ( value : string , expectedScopes : ReadonlyArray < string > ) : boolean {
0 commit comments