Skip to content

check:role-word's per-file ratchet makes it impossible to document ANY new route whose body carries better-auth's role field — measured while writing #10050 #10533

Description

@os-zhuang

Hit head-on while writing the POST /organization/add-member docs for #10050. Reporting rather than acting: the only mechanical remedy is a baseline expansion, which check-role-word.mjs marks ⛔ MAINTAINER-ONLY.

The mechanism

scripts/check-role-word.mjs is a per-file ratchet over content/docs and skills:

  • WORD = /\brole(?:s)?\b/gi, counted over the whole file — fenced code blocks included.
  • a file not in scripts/role-word-baseline.json containing the word → fail;
  • a baselined file whose count increases → fail (New occurrences are banned (ADR-0090 D3)), and the remedy message marks --update as maintainer-only;
  • a baselined file whose count decreases → also fail, but that one is the author's own ratchet-down.

There is no exemption mechanism — no SKIP_PATHS, no per-kind allowlist, no fence skipping.

The consequence

Because the ratchet is per file and exact, no file anywhere under content/docs has spare budget. So the corpus cannot gain a single new occurrence of the word — which means a route whose wire parameter is literally named role cannot have its request shape documented, in any file, by any author who is not the maintainer.

POST /api/v1/auth/organization/add-member is exactly that route. Its handler reads body.role (readRole() in packages/plugins/plugin-auth/src/organization-add-member.ts), and unlike userId / organizationId / teamId it has no snake_case alias — the wire name is role and nothing else.

This is not a hypothetical class of one. The same block applies to /organization/update-member-role and /organization/invite-member's tier field, and — a second-order bite — the route path/organization/update-member-role itself matches \brole\b, so that URL cannot be written into the docs either.

What #10050's PR did instead

Documented the field indirectly — "the body also carries the membership tier, under better-auth's own column name for it", linking to the existing #membership-tiers-are-a-closed-list section whose invitation example already spends one of the baselined occurrences and shows the literal key.

That keeps content/docs/permissions/authentication.mdx at exactly its baselined 4 and the gate green, and it is not wrong — but a reference page that describes a required parameter without naming it is a real quality cost, and the next author documenting a membership route pays it again.

Why this is a decision, not a bug

The gate is behaving as designed; ADR-0090 D3 deliberately makes the word reserved. The question is whether the design has a hole for the case it already names as legitimate. check-role-word.mjs's own header lists the legitimate KINDS, first among them:

vocabulary owned upstream (better-auth's sys_member.role)

That is precisely this case — and the ratchet has no way to express "legitimate kind, new occurrence". It can only express "frozen count".

Options

  1. Expand the baseline for content/docs/permissions/authentication.mdx by 1–3 and document the field literally. Cheapest; weakens the ratchet by exactly the occurrences granted, and sets a precedent each future membership route re-litigates.
  2. Add an escape for upstream-owned vocabulary — e.g. count only occurrences outside a <!-- role-word: upstream -->-marked span, or exempt fenced blocks whose info string marks them as a vendor wire payload. Keeps the ratchet's teeth for prose (which is what D3 is actually about) while letting a wire contract be written down.
  3. Leave it. Accept that better-auth-owned membership routes get tier-field-free prose, and pin the workaround so it is at least consistent.

Recommendation: 2, narrowly scoped to fenced code blocks. D3's target is ObjectStack prose reaching for the word where permission_set / position / business_unit is meant; a JSON body showing a third party's literal key is not that, and the gate's own header already says so. 1 is a reasonable stopgap if 2 is not worth the checker complexity now.

Refs

#10050 (where this was measured) · ADR-0090 D3 · scripts/check-role-word.mjs · scripts/role-word-baseline.json

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions