diff --git a/src/lib/feedback.test.ts b/src/lib/feedback.test.ts index 17cad2b..b60d5fc 100644 --- a/src/lib/feedback.test.ts +++ b/src/lib/feedback.test.ts @@ -11,7 +11,6 @@ import { cleanTags, cleanTitle, discussionUrl, - isScopeError, } from './feedback' const space = { uri: SPACE_URI, cid: 'bafyreigxyqzvvsynbpuhn35uls646smjbrp4vybh6nyuesn4elhkzozgme' } @@ -89,19 +88,3 @@ describe('board links', () => { expect(discussionUrl('did:plc:abc', '3kxyz')).toBe('https://userinput.app/d/did:plc:abc/3kxyz') }) }) - -describe('isScopeError', () => { - it('recognizes the PDS refusing a write the grant does not cover', () => { - expect(isScopeError({ error: 'ScopeMissingError', message: 'Missing required scope' })).toBe( - true, - ) - expect(isScopeError(new Error('Missing required scope "repo:app.userinput.discussion"'))).toBe( - true, - ) - }) - - it('leaves every other failure alone', () => { - expect(isScopeError(new Error('Failed to fetch'))).toBe(false) - expect(isScopeError(undefined)).toBe(false) - }) -}) diff --git a/src/lib/feedback.ts b/src/lib/feedback.ts index 17e29cb..583fa96 100644 --- a/src/lib/feedback.ts +++ b/src/lib/feedback.ts @@ -199,21 +199,3 @@ export async function fetchSpace(): Promise { tags: readTags(value.tags), } } - -/** - * Whether a failed write means the session was granted before this extension - * asked for the feedback scope. - * - * Asking the session instead is not possible even in principle: the - * authorization server expands `include:` into the permissions the set - * holds before minting the token, so the granted scope never contains the - * string that was requested, and the set's members are only known over there. - * So the write is attempted and the PDS is what says no — a missing permission - * comes back as `ScopeMissingError` naming the exact scope it wanted. - */ -export function isScopeError(err: unknown): boolean { - const e = err as { error?: unknown; message?: unknown } | null - const name = typeof e?.error === 'string' ? e.error : '' - const message = typeof e?.message === 'string' ? e.message : '' - return /scope/i.test(name) || /\bscope\b/i.test(message) -} diff --git a/src/lib/scope.test.ts b/src/lib/scope.test.ts new file mode 100644 index 0000000..352dd60 --- /dev/null +++ b/src/lib/scope.test.ts @@ -0,0 +1,22 @@ +// Moved from feedback.test.ts with isScopeError itself: the labeler list now +// asks the same question about a read that the feedback form asks about a +// write. + +import { describe, expect, it } from 'vitest' +import { isScopeError } from './scope' + +describe('isScopeError', () => { + it('recognizes the PDS refusing a write the grant does not cover', () => { + expect(isScopeError({ error: 'ScopeMissingError', message: 'Missing required scope' })).toBe( + true, + ) + expect(isScopeError(new Error('Missing required scope "repo:app.userinput.discussion"'))).toBe( + true, + ) + }) + + it('leaves every other failure alone', () => { + expect(isScopeError(new Error('Failed to fetch'))).toBe(false) + expect(isScopeError(undefined)).toBe(false) + }) +}) diff --git a/src/lib/scope.ts b/src/lib/scope.ts new file mode 100644 index 0000000..5a93822 --- /dev/null +++ b/src/lib/scope.ts @@ -0,0 +1,24 @@ +// Telling "this session was granted before we asked for that permission" +// apart from any other refusal. +// +// Shared, because more than one feature asks the PDS for something a session +// minted under an older `oauth/client-metadata.json` was never granted: the +// feedback form writes a discussion record, and the labeler list reads the +// account's preferences. + +/** + * Whether a failed call means the session predates the scope it needed. + * + * Asking the session instead is not possible even in principle: the + * authorization server expands `include:` into the permissions the set + * holds before minting the token, so the granted scope never contains the + * string that was requested, and the set's members are only known over there. + * So the call is attempted and the PDS is what says no — a missing permission + * comes back as `ScopeMissingError` naming the exact scope it wanted. + */ +export function isScopeError(err: unknown): boolean { + const e = err as { error?: unknown; message?: unknown } | null + const name = typeof e?.error === 'string' ? e.error : '' + const message = typeof e?.message === 'string' ? e.message : '' + return /scope/i.test(name) || /\bscope\b/i.test(message) +} diff --git a/src/popup/popup.ts b/src/popup/popup.ts index ff008ff..32638f8 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -9,10 +9,10 @@ import { buildDiscussion, discussionUrl, fetchSpace, - isScopeError, } from '../lib/feedback' import { type LabelView, labelerDids, labelsFor } from '../lib/labels' import { refreshStoredSession, restoreAgent, signOut } from '../lib/oauth' +import { isScopeError } from '../lib/scope' import { type Owner, fetchOwner, ownerName, ownerSubtitle } from '../lib/profile' import { SESSION_EXPIRED_KEY, getStoredSession } from '../lib/session-store' import { DEFAULT_READER_ID, READERS, type Reader, docUrl, pubSiteUrl, pubUrl } from '../lib/readers'