detached 160bcbb

docs: say that owner rights also need no branch prefixes

Fixes the session error text, the web and token revoke help, the CLI skill and
the features.go comment (the pre-check is the main guard, the post-check a
backstop).
commit
160bcbb875ec211b2c7603af2efc3184dc98f52f
author
brice · · 2026-10-12 16:14 UTC
parent
e0dab23
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +9 -7
M internal/adapters/client/features.go +4 -2
@@ -24,8 +24,10 @@ const (
2424 FeatureSession = "session"
2525 // FeatureTokenBranchPrefixes is Token.branch_prefixes and
2626 // CreateTokenRequest.branch_prefixes (`ermite token create --branch-prefix`).
27- // A server without it ignores the field: `token create` checks the
28- // token it gets back.
27+ // A server without it ignores the field and would create an unlimited
28+ // token: `token create --branch-prefix` reads the features first and
29+ // refuses when it is missing (the main guard), and also checks the token it
30+ // gets back (a backstop).
2931 FeatureTokenBranchPrefixes = "token.branch_prefixes"
3032 )
3133
@@ -24,8 +24,10 @@ const (
24 FeatureSession = "session"24 FeatureSession = "session"
25 // FeatureTokenBranchPrefixes is Token.branch_prefixes and25 // FeatureTokenBranchPrefixes is Token.branch_prefixes and
26 // CreateTokenRequest.branch_prefixes (`ermite token create --branch-prefix`).26 // CreateTokenRequest.branch_prefixes (`ermite token create --branch-prefix`).
27- // A server without it ignores the field: `token create` checks the27+ // A server without it ignores the field and would create an unlimited
28- // token it gets back.28+ // token: `token create --branch-prefix` reads the features first and
29+ // refuses when it is missing (the main guard), and also checks the token it
30+ // gets back (a backstop).
29 FeatureTokenBranchPrefixes = "token.branch_prefixes"31 FeatureTokenBranchPrefixes = "token.branch_prefixes"
30 )32 )
31 33
M internal/app/cli_token.go +1 -1
@@ -287,7 +287,7 @@ func newTokenRevokeCmd(env *cliEnv) *cobra.Command {
287287 Use: "revoke <id>",
288288 Short: "Revoke a token",
289289 Long: "Revoke a token by id (see `ermite token list`). It stops working immediately.\n\n" +
290- "A token with every scope and no --repo list has the rights of the owner (it can open web\n" +
290+ "A token with every scope, no --repo list and no --branch-prefix has the rights of the owner (it can open web\n" +
291291 "sessions). Revoking one also signs every browser out, like `ermite web logout --all`:\n" +
292292 "web sessions are not tied to the token that opened them. Sign in again with `ermite web login`.",
293293 Args: cobra.ExactArgs(1),
@@ -287,7 +287,7 @@ func newTokenRevokeCmd(env *cliEnv) *cobra.Command {
287 Use: "revoke <id>",287 Use: "revoke <id>",
288 Short: "Revoke a token",288 Short: "Revoke a token",
289 Long: "Revoke a token by id (see `ermite token list`). It stops working immediately.\n\n" +289 Long: "Revoke a token by id (see `ermite token list`). It stops working immediately.\n\n" +
290- "A token with every scope and no --repo list has the rights of the owner (it can open web\n" +290+ "A token with every scope, no --repo list and no --branch-prefix has the rights of the owner (it can open web\n" +
291 "sessions). Revoking one also signs every browser out, like `ermite web logout --all`:\n" +291 "sessions). Revoking one also signs every browser out, like `ermite web logout --all`:\n" +
292 "web sessions are not tied to the token that opened them. Sign in again with `ermite web login`.",292 "web sessions are not tied to the token that opened them. Sign in again with `ermite web login`.",
293 Args: cobra.ExactArgs(1),293 Args: cobra.ExactArgs(1),
M internal/app/cli_web.go +1 -1
@@ -23,7 +23,7 @@ func newWebCmd(env *cliEnv) *cobra.Command {
2323 Use: "web",
2424 Short: "Sign the owner in to the web interface of a remote server",
2525 Long: "Manage the owner's browser sessions on a remote server. They need the server's admin token, or a\n" +
26- "token that holds every scope and is not limited to repositories.",
26+ "token that holds every scope and is limited neither to repositories nor to branches.",
2727 }
2828 web.AddCommand(newWebLoginCmd(env), newWebLogoutCmd(env))
2929 return web
@@ -23,7 +23,7 @@ func newWebCmd(env *cliEnv) *cobra.Command {
23 Use: "web",23 Use: "web",
24 Short: "Sign the owner in to the web interface of a remote server",24 Short: "Sign the owner in to the web interface of a remote server",
25 Long: "Manage the owner's browser sessions on a remote server. They need the server's admin token, or a\n" +25 Long: "Manage the owner's browser sessions on a remote server. They need the server's admin token, or a\n" +
26- "token that holds every scope and is not limited to repositories.",26+ "token that holds every scope and is limited neither to repositories nor to branches.",
27 }27 }
28 web.AddCommand(newWebLoginCmd(env), newWebLogoutCmd(env))28 web.AddCommand(newWebLoginCmd(env), newWebLogoutCmd(env))
29 return web29 return web
M internal/usecase/sessions.go +2 -2
@@ -74,13 +74,13 @@ func NewSessions(ctx context.Context, store ports.SessionStore, clock ports.Cloc
7474 }
7575
7676 // requireOwner is the access rule of the session API: root, or a stored token
77-// with every scope and no repository list.
77+// with every scope, no repository list and no branch prefixes.
7878 func requireOwner(p domain.Principal) error {
7979 switch {
8080 case p.IsAnonymous():
8181 return domain.ErrUnauthenticated
8282 case !p.HasOwnerRights():
83- return fmt.Errorf("%w: managing web sessions needs the admin token or a token with every scope and no repository list", domain.ErrForbidden)
83+ return fmt.Errorf("%w: managing web sessions needs the admin token or a token with every scope, no repository list and no branch prefixes", domain.ErrForbidden)
8484 }
8585 return nil
8686 }
@@ -74,13 +74,13 @@ func NewSessions(ctx context.Context, store ports.SessionStore, clock ports.Cloc
74 }74 }
75 75
76 // requireOwner is the access rule of the session API: root, or a stored token76 // requireOwner is the access rule of the session API: root, or a stored token
77-// with every scope and no repository list.77+// with every scope, no repository list and no branch prefixes.
78 func requireOwner(p domain.Principal) error {78 func requireOwner(p domain.Principal) error {
79 switch {79 switch {
80 case p.IsAnonymous():80 case p.IsAnonymous():
81 return domain.ErrUnauthenticated81 return domain.ErrUnauthenticated
82 case !p.HasOwnerRights():82 case !p.HasOwnerRights():
83- return fmt.Errorf("%w: managing web sessions needs the admin token or a token with every scope and no repository list", domain.ErrForbidden)83+ return fmt.Errorf("%w: managing web sessions needs the admin token or a token with every scope, no repository list and no branch prefixes", domain.ErrForbidden)
84 }84 }
85 return nil85 return nil
86 }86 }
M skills/ermite/CLI.md +1 -1
@@ -81,5 +81,5 @@ Merge request JSON: `repo`, `number`, `title`, `description`, `source_branch`, `
8181 | `serve` | configured by `ERMITE_*` environment variables only (see the project README) |
8282 | `version [--json]` | client version; with `ERMITE_URL`, the server version and features |
8383 | `signing-key show` | public key and `allowed_signers` line to check squash commits with `git verify-commit` |
84-| `web login [--json]` | one-time sign-in link (about 60 s); needs the admin token or a token with every scope and no repository list; the link is a secret |
84+| `web login [--json]` | one-time sign-in link (about 60 s); needs the admin token or a token with every scope, no repository list and no branch prefix; the link is a secret |
8585 | `web logout --all` | signs every browser out |
@@ -81,5 +81,5 @@ Merge request JSON: `repo`, `number`, `title`, `description`, `source_branch`, `
81 | `serve` | configured by `ERMITE_*` environment variables only (see the project README) |81 | `serve` | configured by `ERMITE_*` environment variables only (see the project README) |
82 | `version [--json]` | client version; with `ERMITE_URL`, the server version and features |82 | `version [--json]` | client version; with `ERMITE_URL`, the server version and features |
83 | `signing-key show` | public key and `allowed_signers` line to check squash commits with `git verify-commit` |83 | `signing-key show` | public key and `allowed_signers` line to check squash commits with `git verify-commit` |
84-| `web login [--json]` | one-time sign-in link (about 60 s); needs the admin token or a token with every scope and no repository list; the link is a secret |84+| `web login [--json]` | one-time sign-in link (about 60 s); needs the admin token or a token with every scope, no repository list and no branch prefix; the link is a secret |
85 | `web logout --all` | signs every browser out |85 | `web logout --all` | signs every browser out |

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close