feat(identity): implement ADR-0068 unified user-context contract (D1–D4)#2312
Merged
Conversation
Implements the unified EvalUser contract so a predicate authored once
evaluates identically across server-formula, server-RLS, and client.
Identities are modelled as roles (roles: string[]), not booleans.
D1 — EvalUser contract (@objectstack/spec)
- New eval-user.zod.ts: EvalUser schema, canonical roles[], deprecated
singular `role`, derived isPlatformAdmin alias, built-in role
constants/metadata, mapMembershipRole(), createEvalUser() factory.
- formula: buildScope mounts current_user / user / ctx.user / os.user as
the SAME EvalUser object; deprecated `role` scalar kept read-only for
back-compat during migration.
D2 — Built-in roles + normalization
- plugin-security: bootstrap-builtin-roles seeds sys_role rows.
- plugin-auth: customSession emits one canonical roles[]; removes the
`role='admin'` overwrite footgun; platform_admin derived.
- runtime: normalize better-auth owner/admin/member -> org_*; derive
platform_admin from an UNSCOPED admin_full_access grant.
D3 — Role-definition authority
- sys_role gains a `managed_by` provenance field (system/declared/user).
D4 — Role-catalog validation (anti-hallucination)
- formula/validate.ts: closed-enum roleCatalog check rejects unknown
role names referenced in predicates.
Tests: formula 206, plugin-auth 120, plugin-security 143, runtime 432.
Full `turbo build` 77/77 (incl. all DTS typechecks).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 99 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ample ADR-0068 D4 follow-up. The embedded `describe()` example shipped the non-canonical, non-catalog pattern `os.user.role == "admin"`; users copy-paste these. Switch to `'org_admin' in current_user.roles` so the shipped example matches the canonical contract and passes role-catalog validation.
The TypeScript Type Check CI job runs check:api-surface, which flagged 26 additive exports (EvalUser contract, built-in role constants/metadata, createEvalUser/mapMembershipRole) with 0 breaking. Regenerate the committed snapshot so the guard passes.
CodeQL flagged js/polynomial-redos (high) on ROLE_EXISTS_RE: the
unbounded greedy [^,]* + lazy [^)]*? scanners backtrack O(n^2) on
adversarial input like repeated 'user.roles.exists('. Bound both
quantifiers ({0,64}/{0,128} — CEL exists bodies are tiny) and exclude
'=' from the pre-operator class so the run is linear and unambiguous.
Capture groups unchanged; 206 formula tests pass; a 720k-char
pathological probe now returns in ~14ms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR-0068 — a unified
EvalUsercontract so a predicate authored once evaluates identically across server-formula, server-RLS, and client. Identities are modelled as roles (roles: string[]), not booleans.Decisions implemented
@objectstack/spec): neweval-user.zod.ts(canonicalroles[], deprecated singularrole, derivedisPlatformAdminalias, built-in role constants/metadata,mapMembershipRole(),createEvalUser()).formulamountscurrent_user/user/ctx.user/os.useras the same object; deprecatedrolekept read-only for migration.plugin-securityseedssys_rolerows;plugin-authcustomSessionemits one canonicalroles[]and removes therole='admin'overwrite footgun;runtimenormalizes better-authowner/admin/member→org_*and derivesplatform_adminfrom an unscopedadmin_full_accessgrant.sys_rolegains amanaged_byprovenance field.formula/validate.tsclosed-enum check rejects unknown role names in predicates.Verification
turbo build77/77 (incl. all DTS typechecks).Rollout notes
'org_admin' in current_user.roles). No production RLS/permission predicate references a role by literal, and no caller suppliesroleCatalogyet, so D4 is opt-in/dormant — enabling it is safe without a grace period. Re-run the audit before wiringroleCataloginto a live validator path.auth-proxy-plugin.tsstill describes the removedrole='admin'synthesis (comment-only; tracked separately).🤖 Generated with Claude Code