fix(settings): derive API-key scope groups and tool counts from the scope catalogue (#1924)

The API-key settings panel carried a hand-copied list of scope groups that
had drifted to 24 of the 30 scopes in API_KEY_SCOPES: articles:read/write,
companies:write and the three reconciliation scopes were missing, so a key
minted in the dashboard could not call gnubok_create_company, the article
tools, the seven reconciliation tools or the matching v1 endpoints. The
per-scope "N verktyg" counts in the panel and in the API_KEY_SCOPES
descriptions were hand-maintained and wrong (reports:read said 18, actual
30; bookkeeping:write said 11, actual 22).

- Move the pure scope catalogue (API_KEY_SCOPES, scope lists, SCOPE_GROUPS,
  TOOL_SCOPE_MAP) into lib/auth/scope-catalog.ts with no server imports, so
  the client-side panel can bundle it. api-keys.ts re-exports everything,
  so existing imports are unchanged.
- SCOPE_GROUPS becomes a list of { domain, label, scopes } covering every
  scope (reconciliation has three), shared by the panel and the OAuth
  consent page. scopeKind() replaces the ad hoc suffix checks.
- TOOL_COUNT_BY_SCOPE is derived from TOOL_SCOPE_MAP at module load; the
  hand-written counts are removed from the catalogue descriptions.
- The panel renders groups and cards from the catalogue; i18n keys are
  derived from domain and scope id. The "(REST API)" heading suffix is
  computed from the counts instead of baked into the labels.
- New unit test asserts every scope belongs to exactly one group and that
  counts equal TOOL_SCOPE_MAP occurrences.
- New sv/en strings for the articles, companies:write and reconciliation
  scopes.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-26 13:35:42 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent f338850bd0
commit b8605aabfc
8 changed files with 625 additions and 552 deletions
+8 -10
View File
@@ -13,6 +13,7 @@ import {
API_KEY_SCOPES,
DEFAULT_OAUTH_SCOPES,
SCOPE_GROUPS,
scopeKind,
validateScopes,
type ApiKeyScope,
} from '@/lib/auth/api-keys'
@@ -838,13 +839,10 @@ function renderScopeCheckboxes(
for (const group of SCOPE_GROUPS) {
const rows: string[] = []
if (group.read && ceiling.has(group.read)) {
rows.push(scopeRow(group.read, preChecked.has(group.read), 'read'))
renderedInGroups.add(group.read)
}
if (group.write && ceiling.has(group.write)) {
rows.push(scopeRow(group.write, preChecked.has(group.write), 'write'))
renderedInGroups.add(group.write)
for (const scope of group.scopes) {
if (!ceiling.has(scope)) continue
rows.push(scopeRow(scope, preChecked.has(scope), scopeKind(scope)))
renderedInGroups.add(scope)
}
if (rows.length > 0) {
groups.push(
@@ -853,11 +851,11 @@ function renderScopeCheckboxes(
}
}
// Defense in depth: the catalogue test guarantees full group coverage, so
// this bucket is empty unless a scope ships without a group.
const remaining = ALL_SCOPES.filter(s => ceiling.has(s) && !renderedInGroups.has(s))
if (remaining.length > 0) {
const rows = remaining.map((s) =>
scopeRow(s, preChecked.has(s), s.endsWith(':write') || s.endsWith(':manage') || s.endsWith(':approve') ? 'write' : 'read')
)
const rows = remaining.map((s) => scopeRow(s, preChecked.has(s), scopeKind(s)))
groups.push(
`<div class="scope-group"><div class="scope-group-title">Övriga</div>${rows.join('')}</div>`
)