Open History to see the last commit
1import * as Schema from "effect/Schema";
2
3const OAUTH_SCOPE_TOKEN = /^[\u0021\u0023-\u005b\u005d-\u007e]+$/u;
4
5export class OAuthScopeEncodingError extends Schema.TaggedError<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
18export function parseOAuthScope(value: string): ReadonlyArray<string> | null {
19 if (value.length === 0) {
20 return null;
21 }
22
23 const scopes = value.split(" ");
24 if (scopes.some((scope) => !OAUTH_SCOPE_TOKEN.test(scope))) {
25 return null;
26 }
27
28 return [...new Set(scopes)];
29}
30
31export function encodeOAuthScope(scopes: ReadonlyArray<string>): string {
32 const invalidScopes = scopes.filter((scope) => !OAUTH_SCOPE_TOKEN.test(scope));
33 const seen = new Set<string>();
34 const duplicateScopes = new Set<string>();
35 for (const scope of scopes) {
36 if (seen.has(scope)) duplicateScopes.add(scope);
37 seen.add(scope);
38 }
39
40 if (scopes.length === 0 || invalidScopes.length > 0 || duplicateScopes.size > 0) {
41 throw new OAuthScopeEncodingError({
42 scopes,
43 invalidScopes,
44 duplicateScopes: [...duplicateScopes],
45 });
46 }
47 return scopes.join(" ");
48}
49
50export function oauthScopeSetEquals(value: string, expectedScopes: ReadonlyArray<string>): boolean {
51 const scopes = parseOAuthScope(value);
52 return (
53 scopes !== null &&
54 scopes.length === new Set(expectedScopes).size &&
55 scopes.every((scope) => expectedScopes.includes(scope))
56 );
57}
58
59export function parseAllowedOAuthScope<Scope extends string>(input: {
60 readonly value: string;
61 readonly allowedScopes: ReadonlySet<Scope>;
62}): ReadonlyArray<Scope> | null {
63 const scopes = parseOAuthScope(input.value);
64 if (
65 scopes === null ||
66 !scopes.every((scope): scope is Scope => input.allowedScopes.has(scope as Scope))
67 ) {
68 return null;
69 }
70 return scopes;
71}