Skip to content

feat(permissions): domainId by reference + catalog lifecycle - #57

Merged
2000game merged 1 commit into
mainfrom
feat/permissions-ergonomics-25
Jul 9, 2026
Merged

2000game merged 1 commit into
mainfrom
feat/permissions-ergonomics-25

Conversation

@2000game

@2000game 2000game commented Jul 9, 2026

Copy link
Copy Markdown
Member

What

Two permissions-ergonomics work items from #25.

1. group_role domainId by reference

ct.groupRole({ group: "<key>", role: "<name>", grants: [...] }) — a grant is now declarable with zero numeric ids (right name + group key + role name). The resolver maps the (group, role) pair to its numeric pairing domainId at plan time. The numeric id: escape hatch remains fully supported (backward compat, mirroring #49's scope escape hatch).

  • Group half resolves against managed state ∪ declared (same as ref.group). A same-run, not-yet-created group is a hard error (its pairing id only exists once the group does) — apply the group first or pass a numeric id.
  • Role half matches the role name against the group's role list (slug-primary, exact-name secondary — identical to every other master-data catalog).
  • group_type_role by reference (groupType: "<name>") already shipped in feat: portable configs — logical references instead of numeric CT ids (shared resolver) #20 and is unchanged. Note the brief mentioned { groupType, role }, but the documented/shipped domain semantics are that a group_type_role domainId is the group type's own id (role is not part of that domain), so I left it groupType-only rather than change working semantics.

2. Catalog lifecycle

  • Provenance: a reserved top-level $meta key in catalog.json records { capturedFrom, ctVersion, capturedAt, rightCount }. catalog.ts splits it off at load so no consumer (resolveAuthId, ct get permissions-catalog, grant adoption) ever sees it as a right.
  • Regeneration — one command: npm run regenerate:permission-catalog (scripts/regenerate-permission-catalog.ts, run via tsx). Logs in with a login token, calls the legacy POST /index.php?q=churchauth/ajax func=getMasterData, flattens data.auth_table[module][right] into the exact existing schema, stamps the instance CT version into $meta, and rewrites the file. Read-only against the instance; no new runtime deps (built-in fetch).
  • ct plan warnings (never failures):
    • instance CT version ≠ catalog $meta.ctVersion → warn.
    • a live grant carries an authId unknown to the catalog → warn naming the authId + domain, and keep it out of the diff so ct apply never revokes a right it cannot even name. Previous behavior: such a grant had no desired counterpart and landed in toDelete (a silent revoke of an unnameable right) — now excluded every run (idempotent).

⚠️ Pinned assumption needing one-time live confirmation (eqrm-dev)

The semantic question #25 flags — is a group_role domainId the role-definition id or the per-(group, role) pairing id? — is settled by the existing code/docs, not by any fixture/HAR in the repo (none exists for it). Pinned assumption, implemented against and locked by a unit test + a prominent ASSUMPTION (verify on eqrm-dev) comment at the top of src/resolve/resolver.ts:

  1. A group_role domain is keyed by CT's internal (group, role) pairing id — not the group id, not the shared role-definition id.
  2. That pairing id is exposed on GET /groups/{groupId}/roles as each row's id field, matched by role name.

Neither the endpoint nor the field is confirmed live. If a check shows the pairing id lives elsewhere, flip the two constants (GROUP_ROLE_ENDPOINT, GROUP_ROLE_PAIRING_FIELD) at the top of resolver.ts — call sites don't change. Until confirmed, numeric id: is the guaranteed-correct path.

Testing

npm test → 380 passed, 4 skipped (52 files). npm run typecheck and npm run lint clean.

New/updated coverage:

  • resolver: group_role pair → pairing domainId (cached per group), role-not-found, unmanaged group, same-run pending group.
  • plan: declared-by-reference vs adopted live rows → plan no-op (idempotency, no live instance); unknown-authId warned + never revoked; version-mismatch warned; matching-version not warned.
  • catalog: $meta never exposed as a right, provenance recorded, KNOWN_AUTH_IDS exposed.
  • portable-refs: group_role by reference resolves; same-run group still rejected.

Note: npm run format:check reports pre-existing repo-wide prettier drift (66 files, incl. many untouched by this PR) — unrelated to this change; not reformatted to avoid noise.

Closes #25

group_role domains are now declarable by (group, role) reference: the resolver
maps the pair to its pairing domainId at plan time by matching the role name
against the group's role list (GET /groups/{groupId}/roles). The exact source is
an ASSUMPTION pinned in a unit test + a prominent resolver comment, awaiting a
one-time live check on eqrm-dev; the numeric id: escape hatch remains supported.

Catalog lifecycle: a reserved $meta key records the captured CT version, a
scripted one-command regeneration (npm run regenerate:permission-catalog) rebuilds
catalog.json from the live churchauth getMasterData endpoint, and ct plan now warns
(never fails) on a version mismatch or an unknown-authId live grant — the latter is
kept out of the diff so apply never revokes a right it cannot name.
@2000game
2000game force-pushed the feat/permissions-ergonomics-25 branch from e3c30aa to 61060c1 Compare July 9, 2026 11:48
@2000game
2000game merged commit 9527a62 into main Jul 9, 2026
1 check passed
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.

feat: permissions ergonomics — domainId by reference, grant adoption, catalog lifecycle

1 participant