Public: anyone can read this; nobody can change it.

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

opened this view on origin

Check · Review · Land · Live — No review yet. Next: reviewer, no review yet.

Merges on the origin

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…

0 files · +0 −0

waiting checks and the preview wait for a writer to approve this head

Check on trevato-apps/forge: running, as of

Checks and the preview wait for a writer to approve this head

A guest

This pull comes from a guest: its preview runs without the app's variables or a forge token until a person who can write here approves it.

This pull cannot land as it is, whatever its review says, because it changes guarded paths: (the diff) (it could not be read).

opened this view on origin

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

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs