Skip to content

Commit 54ab459

Browse files
authored
fix(oauth-provider): accept query-only granular scopes (#175)
`parseScope` only looked for `:` to find the resource prefix, so valid canonical scopes using the `prefix?query` form (e.g. a multi-collection `repo?collection=a&collection=b` produced by expanding a permission set) were rejected as `Unknown scope resource`. Use the earlier of `:` and `?` as the delimiter, matching `@atproto/oauth-scopes` syntax.
1 parent 71b988e commit 54ab459

3 files changed

Lines changed: 24 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@getcirrus/oauth-provider": patch
3+
---
4+
5+
Fix `parseScope` rejecting valid granular scopes that use the query-only form (e.g. `repo?collection=a&collection=b`) with `Unknown scope resource`. The parser previously only looked for `:` as the prefix delimiter, but per `@atproto/oauth-scopes` syntax a scope can use `prefix:positional`, `prefix?query`, or both. This affected permission sets whose `repo` permission listed multiple collections, since those expand to a single query-form token.

packages/oauth-provider/src/scopes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ export function parseScope(
124124
}
125125

126126
const colon = scope.indexOf(":");
127-
const resource = colon === -1 ? scope : scope.slice(0, colon);
127+
const question = scope.indexOf("?");
128+
const end =
129+
colon === -1 ? question : question === -1 ? colon : Math.min(colon, question);
130+
const resource = end === -1 ? scope : scope.slice(0, end);
128131
const parser =
129132
STRUCTURAL_PARSERS[
130133
resource as (typeof GRANULAR_RESOURCES)[number]

packages/oauth-provider/test/scopes.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ describe("parseScope", () => {
6060
expect(() => parseScope("atproto madeup:thing")).toThrow(ScopeParseError);
6161
});
6262

63+
it("accepts repo scopes in query-only form (no positional)", () => {
64+
const set = parseScope("atproto repo?collection=app.bsky.feed.post");
65+
expect(set.has("repo?collection=app.bsky.feed.post")).toBe(true);
66+
});
67+
68+
it("accepts repo scopes with multiple collections", () => {
69+
const scope =
70+
"repo?collection=site.standard.document" +
71+
"&collection=site.standard.graph.recommend" +
72+
"&collection=site.standard.graph.subscription" +
73+
"&collection=site.standard.publication";
74+
const set = parseScope(`atproto ${scope}`);
75+
expect(set.has(scope)).toBe(true);
76+
});
77+
6378
it("rejects include: scopes by default (strict mode)", () => {
6479
expect(() =>
6580
parseScope("atproto include:com.example.basic?aud=did:web:foo%23svc"),

0 commit comments

Comments
 (0)