Skip to content

The owner may set the owner role's colour (GRYT-906, GRYT-907) - #133

Merged
sivert-io merged 1 commit into
mainfrom
claude/GRYT-906-owner-colour
Sep 4, 2026
Merged

The owner may set the owner role's colour (GRYT-906, GRYT-907)#133
sivert-io merged 1 commit into
mainfrom
claude/GRYT-906-owner-colour

Conversation

@sivert-io

@sivert-io sivert-io commented Sep 4, 2026

Copy link
Copy Markdown
Member

Two changes to the role editor's server half. The client halves are client#386 (opened alongside this).

The owner role's colour saves

server:roles:definitions:save refused every edit to the owner role:

if (roleId === OWNER_ROLE_ID) {
  socket.emit("server:error", { error: "forbidden", message: "The owner role cannot be edited." });
  return;
}

The reason holds for name, rank and permissions — services/permissions falls back to the owner role when a lookup fails, and it is the only thing between a mistake in that editor and a server nobody can administer. A colour is not part of that: it grants nothing, moves nobody, and locks nobody out. Refusing it was the blanket rule catching something it was not written for.

The exception is deliberately narrow:

  • Every other field must be absent, not merely ignored. A payload carrying a rank or a permission beside the colour is refused whole rather than half-applied.
  • Everything written back comes from the row as it stands, so no path through here changes anything but the colour.
  • It sits before the two rank checks, because it has to — the owner role's rank is the owner's own, so existing.rank >= auth.rank refuses the owner editing their own role.

What to look at

  • The extra gate, and whether you agree with it. manage_roles alone would let a delegated admin recolour the owner role so the owner reads as an ordinary member. Small, but social engineering for no benefit, so this also requires auth.rank >= existing.rank — in practice, the owner. If you would rather any manage_roles holder could do it, delete that block; the test names the case.
  • The !saved branch. updateRoleDefinition returns null if the row went between the read and the write. Emitting server:roles:definition:updated there would have clients drawing a colour the server does not hold.

roleEditorState sends identityTiers

The editor offers an "As a guest" default role with a footnote underneath explaining the setting may never be used, because it had no way to tell. getAcceptedIdentityTiers() already exists and is already on /api/server-info; this puts it on the payload the tab already reads, so the client can disable the control and say why. Read-only, admin-only — it goes to a handler already gated on manage_roles.

Verification

yarn test (713 tests), yarn build and npx eslint . all pass; the two eslint warnings are pre-existing and in files this does not touch.

ownerRoleColour.test.ts is new — 19 cases, and most of them are about the hole being no wider than intended. Mutation-tested, all four caught:

mutation
colour-only guard stops checking the other fields caught
colour path writes the payload's permissions caught
rank gate on the owner colour removed caught
colour written without normalising caught

🤖 Generated with Claude Code

Two changes to the role editor's server half.

**The owner role's colour saves.** `server:roles:definitions:save` refused
every edit to it outright. The reason holds for name, rank and permissions —
`services/permissions` falls back to the owner role when a lookup fails, and it
is the only thing between a mistake in that editor and a server nobody can
administer. A colour is not part of that. It grants nothing, moves nobody, and
locks nobody out.

So a colour-only exception, and the shape of it is deliberately narrow:

- Every other field must be absent, not merely ignored. A payload carrying a
  rank or a permission beside the colour is refused whole rather than
  half-applied.
- Everything written back comes from the row as it stands, so there is no path
  through here that changes anything but the colour.
- It sits before the two rank checks, because it has to: the owner role's rank
  is the owner's own, so `existing.rank >= auth.rank` refuses the owner editing
  their own role.
- Which is why it carries its own gate. `manage_roles` alone would let a
  delegated admin recolour the owner so the owner reads as an ordinary member.
  Rank is the check the rest of the file uses for "you are not above this".

`ownerRoleColour.test.ts` is 19 cases and most of them are about the hole being
no wider than intended.

**`roleEditorState` sends `identityTiers`.** The editor offers a guest default
role with a footnote underneath explaining the setting may never be used,
because it had no way to tell. `getAcceptedIdentityTiers()` already exists and
is already on `/api/server-info`; this puts it on the payload the tab reads, so
the client can turn the control off and say why instead. The client half is
GRYT-907.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sivert-io
sivert-io merged commit 69c7e52 into main Sep 4, 2026
2 checks passed
@sivert-io
sivert-io deleted the claude/GRYT-906-owner-colour branch September 4, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant