Connecting a token that lacks a scope names the scope, and the token form lists what a token needs #70

Open
tdobbertin-espo wants to merge 1 commit from tdobbertin-espo/forge:fix/token-scope-words into main
First-time contributor

Forgejo and Gitea answer a token that lacks a required scope with 403 and a body that names the scope: token does not have at least one of required scope(s): [read:user]. The API client mapped every 403 to "the token may not do that on the origin" and did not read the body. Connect then said "That token was refused by the origin". That is the same sentence for a wrong token, an expired token and a narrow token, so a person could not tell which one they had. A narrow token is the usual case: Forgejo's token page asks for each scope, and a forge's whoami needs User: Read.

ApiClient now reads the body of a 403 (4 KB at most) and keeps only the words that look like scopes (missingScopes). Nothing else from the origin goes into a message. The failure carries missingScopes, and its message names them as Forgejo's token page does (scopeWords): "the token is missing User: Read". Sync errors say so too, for example a label import without the issue scope.

Connecting with such a token answers missing_scopes (422) with "This token is missing User: Read." and what a token needs. connectRefusal gives the API and the web form one code and one sentence for each refused connect. An OAuth grant that lacks a scope says so, instead of "the new token did not work".

The token form lists what a Forgejo or Gitea token needs: User: Read; Repository: Read and write; Issue: Read and write; nothing else. When the instance is known, it links to Forgejo 15's /user/settings/applications/tokens/new with the name and the scopes already chosen. Gitea and older Forgejo get the list only.

A token with all its scopes, a wrong token and an expired token behave as before. Every other refused connect keeps its code and its sentence. connectOrigin's error is now ConnectError | ScopeRefusal, so seven test call sites that did new Error(connected.error) now stringify it.

Tests

bun 1.3.13, a scrubbed environment (no agent variables):

  • test/origin-token-scopes.test.ts: 5 pass, 0 fail: the body parser (a matching body, an unrelated body, junk inside the brackets), the scope words, connect and the API refusing a token without read:user with nothing stored, and the form's list and link. test/fake-origin.ts gains userScopeMissing, which answers /user as Forgejo does.
  • With the origin, migrate and cross suites, in one bun test --timeout 60000 call: 431 pass, 0 fail across 32 files.
  • bun scripts/gate.ts and tsc --noEmit: clean.
the 32 files

origin-token-scopes, origins, origins-oauth, origins-operator, origins-reconnect, origins-security-auth, origins-security-provider, origins-security-push, origins-sync, origins-sync-settled, web-origins, web-origin-link, mcp-origins, mcp-origin-apps, push-through, trusted-hosts, trusted-hosts-ui, steps-lists, no-polling, refresh-meta, mirror-outbound, mirror-out, carried, migrate, migrate-after, tui-migrate, apps-feedback-mirror, and the five cross files reserved-names, studio, shards, check-list, hollow (each test/<name>.test.ts).

🤖 Generated with Claude Code

Forgejo and Gitea answer a token that lacks a required scope with 403 and a body that names the scope: `token does not have at least one of required scope(s): [read:user]`. The API client mapped every 403 to "the token may not do that on the origin" and did not read the body. Connect then said "That token was refused by the origin". That is the same sentence for a wrong token, an expired token and a narrow token, so a person could not tell which one they had. A narrow token is the usual case: Forgejo's token page asks for each scope, and a forge's whoami needs User: Read. `ApiClient` now reads the body of a 403 (4 KB at most) and keeps only the words that look like scopes (`missingScopes`). Nothing else from the origin goes into a message. The failure carries `missingScopes`, and its message names them as Forgejo's token page does (`scopeWords`): "the token is missing User: Read". Sync errors say so too, for example a label import without the issue scope. Connecting with such a token answers `missing_scopes` (422) with "This token is missing User: Read." and what a token needs. `connectRefusal` gives the API and the web form one code and one sentence for each refused connect. An OAuth grant that lacks a scope says so, instead of "the new token did not work". The token form lists what a Forgejo or Gitea token needs: User: Read; Repository: Read and write; Issue: Read and write; nothing else. When the instance is known, it links to Forgejo 15's `/user/settings/applications/tokens/new` with the name and the scopes already chosen. Gitea and older Forgejo get the list only. A token with all its scopes, a wrong token and an expired token behave as before. Every other refused connect keeps its code and its sentence. `connectOrigin`'s error is now `ConnectError | ScopeRefusal`, so seven test call sites that did `new Error(connected.error)` now stringify it. ### Tests bun 1.3.13, a scrubbed environment (no agent variables): - `test/origin-token-scopes.test.ts`: **5 pass, 0 fail**: the body parser (a matching body, an unrelated body, junk inside the brackets), the scope words, connect and the API refusing a token without `read:user` with nothing stored, and the form's list and link. `test/fake-origin.ts` gains `userScopeMissing`, which answers `/user` as Forgejo does. - With the origin, migrate and cross suites, in one `bun test --timeout 60000` call: **431 pass, 0 fail across 32 files**. - `bun scripts/gate.ts` and `tsc --noEmit`: clean. <details><summary>the 32 files</summary> origin-token-scopes, origins, origins-oauth, origins-operator, origins-reconnect, origins-security-auth, origins-security-provider, origins-security-push, origins-sync, origins-sync-settled, web-origins, web-origin-link, mcp-origins, mcp-origin-apps, push-through, trusted-hosts, trusted-hosts-ui, steps-lists, no-polling, refresh-meta, mirror-outbound, mirror-out, carried, migrate, migrate-after, tui-migrate, apps-feedback-mirror, and the five cross files reserved-names, studio, shards, check-list, hollow (each `test/<name>.test.ts`). </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Connecting a token that lacks a scope names the scope, and the token form lists what a token needs
Some checks are pending
preview / preview (pull_request) Blocked by required conditions
733fd29b90
Forgejo and Gitea answer a token that lacks a required scope with 403
and a body that names the scope: `token does not have at least one of
required scope(s): [read:user]`. The API client mapped every 403 to "the
token may not do that on the origin" and did not read the body. Connect
then said "That token was refused by the origin". That is the same
sentence for a wrong token, an expired token and a narrow token, so a
person could not tell which one they had.

ApiClient now reads the body of a 403 (4 KB at most) and keeps only the
words that look like scopes (`missingScopes`). Nothing else from the
origin goes into a message. The failure carries `missingScopes`, and its
message names them as Forgejo's token page does (`scopeWords`): "the
token is missing User: Read". Sync errors say so too (for example, a
label import without the issue scope).

Connecting with such a token answers `missing_scopes` (422) with "This
token is missing User: Read." and what a token needs. `connectRefusal`
gives the API and the web form one code and one sentence for each
refused connect. An OAuth grant that lacks a scope says so, instead of
"the new token did not work".

The token form lists what a Forgejo or Gitea token needs: User: Read;
Repository and Issue: Read and write; nothing else. When the instance is
known, it links to Forgejo 15's /user/settings/applications/tokens/new
with the name and the scopes already chosen. Gitea and older Forgejo get
the list only.

A token with all its scopes, a wrong token and an expired token behave
as before.

test/origin-token-scopes.test.ts: the body parser (a matching body, an
unrelated body, junk inside the brackets), the words, connect and the
API refusing a token without read:user with nothing stored, and the
form's list and link. test/fake-origin.ts: `userScopeMissing` answers
`/user` as Forgejo does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some checks are pending
preview / preview (pull_request) Blocked by required conditions
This pull request has changes conflicting with the target branch.
  • docs/ORIGINS.md
  • src/actions/origins.ts
  • src/http/api.ts
  • src/origins/types.ts
  • src/web/pages/origins.ts
  • test/fake-origin.ts
  • test/origin-token-scopes.test.ts
Some workflows are waiting to be reviewed.
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u fix/token-scope-words:tdobbertin-espo-fix/token-scope-words
git switch tdobbertin-espo-fix/token-scope-words

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff tdobbertin-espo-fix/token-scope-words
git switch tdobbertin-espo-fix/token-scope-words
git rebase main
git switch main
git merge --ff-only tdobbertin-espo-fix/token-scope-words
git switch tdobbertin-espo-fix/token-scope-words
git rebase main
git switch main
git merge --no-ff tdobbertin-espo-fix/token-scope-words
git switch main
git merge --squash tdobbertin-espo-fix/token-scope-words
git switch main
git merge --ff-only tdobbertin-espo-fix/token-scope-words
git switch main
git merge tdobbertin-espo-fix/token-scope-words
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
trevato-apps/forge!70
No description provided.