clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/fix-sync-enrollment-startup
lofi / docs / decisions / group-creator-authority-alpha53.md
group-creator-authority-alpha53.md 4.4K rendered
1# Group-creator authority under Jazz alpha.53 policy negation limits23Date: 2026-07-17\4Reviewed dependency: `jazz-tools@2.0.0-alpha.53`\5Decision: document permanent creator authority; grant direct creator delete; revisit on upgrade67## Problem89The `groupAccess` template gives a group's creator `$createdBy` update authority on the group row so10they can insert the first admin membership (membership management derives from group-update via11`allowedTo.update("groupId")`). That authority never lapses: a creator who is later demoted or12removed by other admins can restore their own admin membership at any time. Separately, a failed13first-admin insert used to strand an undeletable orphan group, because rollback deletion required14the admin membership that had just failed to insert.1516The intended fix was a **bootstrap window**: creator authority over the group row holds only while17the group has no admin membership. That requires a negated existence condition — "no admin18membership row exists for this group".1920## Exact alpha.53 engine behavior2122The policy IR (`schema.js` `PolicyExpr`) declares `{ type: "Not", expr }`, the permissions compiler23passes raw IR through, and `schema-permissions.js` serializes `Not` for the wasm core. The core,24however, **silently drops `Not` around existence conditions**. Probed against `createPolicyTestApp`25with a two-table group/members app, rule under test on `allowDelete`:2627| Probe | Expected | alpha.53 result |28| ------------------------------------------------- | ---------- | ------------------------------- |29| `Not(True)` / `Not(False)` | deny/allow | correct |30| `Not(Cmp column = literal)` | deny | correct |31| `Exists(...)` / `ExistsRel(...)` (pos + neg ctrl) | — | correct |32| `Not(Exists(...))`, no matching rows | allow | **denied** |33| `Not(ExistsRel(...))`, matching row present | deny | **allowed** |34| `And(True, Not(ExistsRel(...)))`, row present | deny | **allowed** |35| LEFT-join + `IsNull` anti-join via `ExistsRel` | allow | denied (join not null-extended) |3637`Not` over `Exists`/`ExistsRel` evaluates as if the `Not` were absent — a silent wrong-direction38result, not an error. The `RelExpr` LEFT-join anti-join alternative does not produce null-extended39rows in policy evaluation, so absence-of-rows is not expressible at all in this engine.4041## Decision4243- **Creator authority over the group row stays permanent and becomes a documented trust property**44 of the template: the creator can always update the group and therefore always restore their own45 admin membership. Products that need creator-proof handover cannot get it from this template on46 this engine.47- **The creator additionally receives direct `allowDelete` on their own group rows.** This grants no48 new authority — a creator can already self-bootstrap an admin membership and delete transitively —49 and it lets `createGroup` roll back cleanly when the first-admin insert fails, removing the50 orphan-group failure mode.51- **An engine canary test** (`access_security_test.ts`, "engine canary") asserts the broken52 behavior: a `Not(ExistsRel(...))` delete guard that a correct engine would deny. When a jazz-tools53 upgrade fixes negation, the canary fails, signalling that the bootstrap-window design in this54 record should replace permanent creator authority:55 `allowUpdate/allowDelete = anyOf([isAdmin, allOf([$createdBy, Not(Exists(admin membership))])])`,56 which also preserves creator recovery of admin-less groups.5758## Trust and fallback5960- The template's authority model is: fixed roles for members, plus a permanent creator superseat.61 The sharing UI and docs state this; it must not be presented as revocable admin membership.62- Upstream: the dropped-negation behavior is a jazz-tools core issue worth reporting; until then, no63 lofi policy may rely on `Not` around `Exists`/`ExistsRel`.6465## Evidence sources6667- Probe suite output recorded in this record's table (2026-07-17, Deno harness68 `createPolicyTestApp`, pinned npm cache for `jazz-tools@2.0.0-alpha.53`).69- `dist/permissions/index.js` (raw IR pass-through), `dist/schema-permissions.js` (`Not`70 serialization), `dist/ir.d.ts` (`PolicyExprV2.Not`, `RelExpr`).