Feat: permission feature additions for projectcampus#873
Merged
Conversation
camelCase entity types (e.g. a fork's courseSection) derived a wrong physical table name (courseSections instead of course_sections), breaking the yjs relay's derived SQL. Snake_case first via toColumnName — identical output for every existing single-word entity type. Locked by an explicit convention test. From projectcampus (CELLA-CHANGES #9). Co-Authored-By: Claude Fable 5 <[email protected]>
The two ImmutableEntityCase[] assignments infer string[][] from their flatMap callbacks — an error under plain tsc while tsgo (repo default) accepts it, so CI stays green while 'npx tsc' fails. Explicit return annotations on the map callbacks fix the inference. From projectcampus (CELLA-CHANGES #8). Co-Authored-By: Claude Fable 5 <[email protected]>
The attachments organization_id/created_at index was removed from the drizzle schema (via #862/#863) without regenerating migrations; any fresh 'pnpm generate' emits this diff as a side effect. Landing it standalone so it stops photobombing unrelated migration work. Co-Authored-By: Claude Fable 5 <[email protected]>
Two failure modes from projectcampus (CELLA-CHANGES #14): - noRlsProductEntityNames hardcoded 'pages'; a fork without that module emits 'GRANT ... ON pages', which errors. Now filtered by the tables that actually exist. - That error was swallowed by 'EXCEPTION WHEN OTHERS THEN RAISE NOTICE', rolling back ownership, FORCE RLS and EVERY grant in one silent notice — a fresh DB then boots with zero table grants (every request 403s, pg 42501) or without enforced RLS. The handler now re-raises; the legitimate skip (roles not yet created) is already handled before the block. Regenerated rls_setup migration included. Co-Authored-By: Claude Fable 5 <[email protected]>
The engine is strict at runtime: the first membership whose (context, role) has no policy row for the checked subject throws — a request-time 500 that only surfaces when a real user with that role hits that subject. Projectcampus shipped several such gaps that survived until member-session e2e tests ran (CELLA-CHANGES #17). configurePermissions now asserts at config time that every subject with declared rows covers every role of every context in its ancestor chain; all-zero rows (contexts.x.role({})) express 'no access' explicitly. Subjects with no case are skipped (they fail fast with a clear engine error). configureAccessPolicies and configureWidePermissions (deliberately partial test fixtures) opt out. Note for forks: configs with gaps now fail on startup after upgrading — intended; add the missing all-zero rows. Co-Authored-By: Claude Fable 5 <[email protected]>
Contexts gain a draft state that defers member invites until publish, dormant for forks that never use it (publishedAt defaults to creation time — the same graceful-degradation pattern as nullableAncestors). Distinct from publicAt (public readability). From projectcampus (CELLA-CHANGES #1–04 / PR-1). - contextEntityColumns: nullable publishedAt timestamp, defaultNow(). - create-memberships: on a draft context, invites for non-top roles are recorded (inactive memberships + tokens) but emails are held and existing users are not direct-added; the most-privileged role (first in the vocabulary) stays live so staff can collaborate in drafts. - inactive_memberships.remindedAt: last email dispatch; reminder emails are throttled to once per 7 days via remindedAt ?? createdAt. - helpers/deferred-invites.ts: dispatchDeferredInvites({contextIds}) — called by a fork's publish operation; rotates invitation tokens (raw secrets are unrecoverable and may have expired), sends held emails, stamps remindedAt, honors the throttle. - me-queries findPendingInvitations: invites whose context is unpublished stay hidden from the invitee (staff-side pending lists unchanged). Known pre-existing behavior made visible by the throttle work: re-inviting a truly-new token invitee (no emails row) re-enters the new-user path where onConflictDoNothing suppresses the duplicate — no email is re-sent, but an orphan token row is minted per attempt. Co-Authored-By: Claude Fable 5 <[email protected]>
Cella lets each context pick a role subset, but several code paths still assumed one flat vocabulary. All changes are no-ops for single-vocabulary forks. From projectcampus (CELLA-CHANGES #5/06/10/12 / PR-2). - MenuStructureItem.carryRole + resolveParentMembershipRole: auto-created parent/associated memberships get the least-privileged fitting role ('member' when the vocabulary has it — identical to the previous hardcode), or carry the invited role when carryRole is set and valid in the parent vocabulary (e.g. courseSection student → course student). - create-memberships + update-membership: reject roles outside the target context's vocabulary with 400 invalid_role. - SelectRoleRadio: optional entityType prop restricts options to the context's vocabulary; invite-email-form passes it. - mockContextMembership picks its random role from the context's vocabulary (roles.all yields invalid combos for multi-vocabulary forks). Co-Authored-By: Claude Fable 5 <[email protected]>
'Invite > Email (Bulk)': paste any text, email addresses are regex-extracted,
deduped and lowercased, one role per batch, dispatched through the existing
invite mutation. Linked from the email tab. The invite form's default role now
derives from the target context's vocabulary ('member' when present, else the
least-privileged role) — required by multi-vocabulary forks, identical for
cella. From projectcampus (CELLA-CHANGES #11 / PR-4).
Co-Authored-By: Claude Fable 5 <[email protected]>
Product rows must be self-describing for read visibility: relationships between product entities are data (id columns, embedded id-arrays), never permission indirection. First-consumer experience (projectcampus comments) showed delegation taxes every sync surface with a host-aware variant — list SQL predicates, single-row checks, delta sync, SSE dispatch, revocation, unseen counts — and fails closed into silence. The embedding write-side invariant is enough lifecycle machinery; projectcampus replaced its only usage with copied-and-cascaded columns (CELLA-CHANGES #19/#22). Removes: the hierarchy host: option, delegateToHost/hostDelegation/hostRow from the engine and subjects, cascadeSoftDeleteHosted, CDC hosted-cascade suppression and e:<hosted> host counters. Entity EMBEDDINGS (id-array columns + CDC ref-counting) are unrelated and unchanged. BREAKING CHANGE: forks using host:/delegateToHost (raak: attachment hosted by task) must migrate before syncing — give the hosted entity its own membership read grants (raak has no row restrictions, so grants alone reproduce the previous behavior) and add an explicit same-transaction cascade in the host's delete operation. Co-Authored-By: Claude Fable 5 <[email protected]>
The channel axiom (projectcampus, CELLA-CHANGES #22): product-row read visibility is a function of the home context chain, memberships x the static policy matrix, and the row's own createdBy — never per-row audience data. Visibility variance belongs at the TYPE level (a product type gets its own policy matrix); the per-row exceptions ('own', fork-side draft flags) are createdBy-based and only ever widen, so revocation does not exist. Per-row restrictions proved to be the superlinear complexity term across every sync surface, and restrict() has zero consumers. Removes: restrict()/rowRestrictions, visibilityDepth/audienceRoles qualification in the engine check, the restricted-grant machinery in the collection-scope compiler and SQL row predicates, and restriction scenarios in the parity property test. Row conditions ('own') and publicRead stay. BREAKING CHANGE: forks calling restrict() must move visibility variance to type-level policy matrices before syncing (no known consumers). Co-Authored-By: Claude Fable 5 <[email protected]>
Multi-depth product scoping, from projectcampus (CELLA-CHANGES #15/#24/#16) — the static machinery that replaces the removed per-row restrict(): - Collection scope: membership grants at intermediate ancestor levels now contribute list scope, each compiled against its own level's id column. Two-level configs produce zero intermediate entries (behavior-identical); without this, forks with nested contexts list zero rows for mid-level members. `requested` narrowing keeps home-level semantics exactly. - subtreeRoles (optional, default undefined = current behavior): on product subjects, grants of non-listed roles speak only for rows HOMED at their own context level; listed roles (staff/admin elevation) keep subtree scope. Applies to every action including create; context subjects exempt. This is the type-level answer to per-row visibility: one static role set instead of per-row audience data, evaluable identically by the engine check, the SQL compiler and SSE dispatch. - Single-row subjects carry `row`, so 'own' conditions and publicSelf evaluate from real row data on get/update/delete paths. - The scope compiler is now topology-injectable like the engine, enabling deep-chain parity tests on synthetic hierarchies; new parity blocks cover intermediate grants and home/subtree scoping, engine ≍ SQL row-for-row. Co-Authored-By: Claude Fable 5 <[email protected]>
From projectcampus (CELLA-CHANGES #18/#20). The dispatch decision doubles as cacheToken issuance — appCache serves cached bytes to any valid token holder without re-running predicates — so dispatch must answer exactly the API's read question. It already ran the engine per subscriber but starved it of inputs. - The dispatch subject now carries the event's row (REPLICA IDENTITY FULL puts it on every event), evaluated by the same checkPermission as API reads; exported as a testable predicate. - Batch events are evaluated per row: a subscriber is pinged iff they can read at least one row the event speaks for (wire fields land with the CDC contract change). - Subscriber membership snapshots refresh live from membership events instead of staying frozen at connect time. - Unseen counts compose the same collection read predicate as list endpoints — shared counters are change signals, never per-user numbers. - Three-way parity: collection SQL, engine check and dispatch agree row-for-row; dispatcher behavior tests with fake subscribers. Co-Authored-By: Claude Fable 5 <[email protected]>
From projectcampus (CELLA-CHANGES #21/#23), completing the dispatch-mirror work: - Batch messages carry slim per-row permission fields (id, createdBy, context id columns, publicAt) so SSE dispatch decides per subscriber per row instead of on the first row alone — batches can span visibility boundaries. - Batch messages are now split per seq-context: seqs are per-context counters, so a transaction spanning contexts (one bulk create with mixed placements) previously produced a single message with a non-contiguous seq range — logged as 'sync integrity at risk' and semantically wrong for the client's range fetch. Each context's message now has its own contiguous range, token and reservations. Wire-contract change; the CDC worker and API deploy together (existing deployment model), no compat shims. Co-Authored-By: Claude Fable 5 <[email protected]>
Companion rule to the dispatch-mirror work (projectcampus CELLA-CHANGES #20/#22): shared sequences and counters are cursors and change signals; any number a user sees must come from a predicate-filtered read. - Batch-create pings no longer bump unseen counts by the seq-range width — a batch's width is not 'new for you' (it can span contexts or contain rows the subscriber cannot read). The client invalidates the unseen-counts query instead; the server computes the predicate-exact answer. - The catchup integrity check compared server-reported counts against the client's cached list totals — but caches are predicate-filtered per user, so any member who can't see every row mismatched forever, invalidating on every reconnect. It now compares server-count to the PREVIOUS server-count (an honest change signal) and invalidates only when something actually changed while the client wasn't watching. Co-Authored-By: Claude Fable 5 <[email protected]>
The name should carry the concept's precise meaning: roles listed here are ELEVATED — their product-subject grants cover the whole subtree below their context (staff/admin oversight), while everyone else's grants speak only for rows homed at their own level. 'elevated' also matches the existing elevation vocabulary in the policy docs (elevation rows vs self rows). Unreleased rename — the option shipped on this branch. Co-Authored-By: Claude Fable 5 <[email protected]>
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.
No description provided.