You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The permission system currently holds three positions at once about whether write-shaped verbs differ:
Grants say they differ: the action vocabulary ships update-own/any and delete-own/any as separate verbs, and requireUpdatable/requireDeletable check them separately (packages/core/src/stack.ts:854–874).
Record-level permissions say they're one bit: checkWrite (the write: true boolean on a PermissionEntry) short-circuits both requires, and associate/dissociate ride requireUpdatable too (stack.ts:859, 870, 944, 949).
setPermissions says one verb is sacred: it ignores write entirely — owner or creator only (stack.ts:957–959) — precisely so a write-holder can't escalate.
Note the scope: this is about the served topology. For adapter-local, direct adapter access is full trust and the permission model means nothing (#45, settled). Record-level permissions exist exactly for the requester on the far side of the server — a write: true holder who does not have DB access. For them the bit is the only fence, so what it permits is a real policy surface.
And what it permits today is wildly asymmetric in blast radius:
verb (via write: true)
reversible?
update
Yes — versioned (snapshot first), schema-validated, restoreVersion undoes it
restoreVersion
Yes — update-shaped
associate / dissociate
No — unversioned, no snapshot, no audit trail (parked in #48's open questions); and association mutation carries privilege side effects (#51: references confer access)
delete (soft)
In principle — but there is no undelete API, so in practice no
delete (hard)
Never — ScopedStack.delete(id, opts) passes opts straight through (stack.ts:964–966), so write: true reaches { hard: true }: record, and its version history, permanently gone
A record shared "so you can edit it" is a record the grantee can permanently destroy.
Decided direction
Keep write as one coarse bit, and make its trust model true: anything a write-holder does, the owner can undo. Recoverability is the backstop, not per-verb fencing. This matches the project's stated scale (per-record sharing among a small cohesive group — nobody will maintain per-verb ACLs) and the stated versioning intent (rollback + recovery, not conflict fencing).
Concretely:
Hard delete is owner-only. Never reachable through the write bit (nor, tentatively, through delete-any grants — an irreversible verb has no place in a delegated-access vocabulary; flagging that sub-decision for confirmation at implementation). This is the one-line carve-out at stack.ts:964 and stands on its own regardless of the rest.
Soft delete becomes actually recoverable. The undelete API (spec already promises soft-delete recoverability; no API exists) is a prerequisite of this model, not polish — without it, "reversible by owner" is false for every deletion.
Spec states the model plainly:write = update + soft-delete + associate, all reversible by the owner; irreversible or privilege-bearing operations (hard delete, setPermissions) are owner/creator-only. The existing setPermissions carve-out stops being an unexplained special case and becomes the pattern's first instance.
Full alignment: record-level permissions adopt the grant verb vocabulary ({ access: 'entity', entityId, actions: ['read','update','delete','associate'] }, with read/write as sugar). Most conceptually coherent — one vocabulary at two scopes — but the PermissionEntry shape change ripples through wire, spec, and every ACL-editing surface, and buys expressiveness the target scale won't use. Rejected in discussion; revisitable if the scale intent ever changes, and nothing in the chosen direction forecloses it.
Dependencies / sequencing
Undelete API (backlog) — prerequisite, should be filed/built alongside or before the spec language lands.
Spec §Permissions: the recoverability trust model, verb table for write, owner/creator-only list
Tests: write-holder cannot hard-delete; owner can; write-holder soft-delete is undeletable by owner (once API exists); grant-holder with update-any but not delete-any still cannot delete (existing distinction preserved)
Refs #45 (topology scoping of the argument), #48 (association-versioning open question resolved toward versioning), #51 (privilege-creating associations gated independently), #58 (roster mutations already carved out for _group), #52/#53 (wire: hard-delete denial must round-trip as StackPermissionError).
Problem
The permission system currently holds three positions at once about whether write-shaped verbs differ:
update-own/anyanddelete-own/anyas separate verbs, andrequireUpdatable/requireDeletablecheck them separately (packages/core/src/stack.ts:854–874).checkWrite(thewrite: trueboolean on aPermissionEntry) short-circuits both requires, andassociate/dissociateriderequireUpdatabletoo (stack.ts:859, 870, 944, 949).setPermissionssays one verb is sacred: it ignoreswriteentirely — owner or creator only (stack.ts:957–959) — precisely so a write-holder can't escalate.Note the scope: this is about the served topology. For
adapter-local, direct adapter access is full trust and the permission model means nothing (#45, settled). Record-level permissions exist exactly for the requester on the far side of the server — awrite: trueholder who does not have DB access. For them the bit is the only fence, so what it permits is a real policy surface.And what it permits today is wildly asymmetric in blast radius:
write: true)updaterestoreVersionundoes itrestoreVersionassociate/dissociatedelete(soft)delete(hard)ScopedStack.delete(id, opts)passesoptsstraight through (stack.ts:964–966), sowrite: truereaches{ hard: true }: record, and its version history, permanently goneA record shared "so you can edit it" is a record the grantee can permanently destroy.
Decided direction
Keep
writeas one coarse bit, and make its trust model true: anything a write-holder does, the owner can undo. Recoverability is the backstop, not per-verb fencing. This matches the project's stated scale (per-record sharing among a small cohesive group — nobody will maintain per-verb ACLs) and the stated versioning intent (rollback + recovery, not conflict fencing).Concretely:
writebit (nor, tentatively, throughdelete-anygrants — an irreversible verb has no place in a delegated-access vocabulary; flagging that sub-decision for confirmation at implementation). This is the one-line carve-out at stack.ts:964 and stands on its own regardless of the rest.versionis for, and add opt-in optimistic concurrency so concurrent writers can't corrupt version history #48 open question, resolved in this direction), and privilege-creating associations stay gated by ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51's predicate independently ofwrite.write= update + soft-delete + associate, all reversible by the owner; irreversible or privilege-bearing operations (hard delete,setPermissions) are owner/creator-only. The existingsetPermissionscarve-out stops being an unexplained special case and becomes the pattern's first instance.associateridesupdate-*on the grant side, with ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51 gating the dangerous cases. The two-granularity design becomes a stated position instead of an accident.Alternative considered and set aside
Full alignment: record-level permissions adopt the grant verb vocabulary (
{ access: 'entity', entityId, actions: ['read','update','delete','associate'] }, withread/writeas sugar). Most conceptually coherent — one vocabulary at two scopes — but thePermissionEntryshape change ripples through wire, spec, and every ACL-editing surface, and buys expressiveness the target scale won't use. Rejected in discussion; revisitable if the scale intent ever changes, and nothing in the chosen direction forecloses it.Dependencies / sequencing
versionis for, and add opt-in optimistic concurrency so concurrent writers can't corrupt version history #48 open question) — prerequisite for the same reason.Work items
ScopedStack.delete: strip/rejecthardfor non-owners (decide: silently soften, orStackPermissionError— leaning error, silent downgrades hide intent)versionis for, and add opt-in optimistic concurrency so concurrent writers can't corrupt version history #48's parked question)write, owner/creator-only listupdate-anybut notdelete-anystill cannot delete (existing distinction preserved)Refs #45 (topology scoping of the argument), #48 (association-versioning open question resolved toward versioning), #51 (privilege-creating associations gated independently), #58 (roster mutations already carved out for
_group), #52/#53 (wire: hard-delete denial must round-trip asStackPermissionError).