feat: Implement shared scopes with role enforcement (#90)

This commit is contained in:
Marcelo Mendonça Soares 2026-07-03 01:25:15 -03:00 • committed by GitHub
commit 8acfa7f269
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 4868 additions and 485 deletions

View file

@ -21,6 +21,7 @@ class CrdtSyncEndpoint extends Endpoint {
userId: UuidValue.withValidation(session.authenticated!.userIdentifier),
inbound: changes,
once: once,
mode: CrdtSyncPeerMode.authoritative,
);
}
}

View file

@ -3,6 +3,9 @@ class: CrdtMergeChange
sealed: true
extends: BaseHlc
fields:
### The scope this change belongs to.
uuidScopeId: UuidValue
### The table receiving the change.
tableName: String

View file

@ -1,15 +1,12 @@
### CRDT nodes table.
###
### This table stores all nodes that have been seen.
### This table stores stable CRDT replica identities that have been seen.
class: CrdtNode
table: crdt_nodes
database: all
fields:
### The scope that owns the node.
scope: CrdtScope?, relation(name=scope_nodes, onDelete=Cascade)
### Unique identifier for the node.
### Stable unique identifier for the replica/database install.
uuidNodeId: UuidValue, default=random_v7, unique
### The last HLC timestamp when the node has synchronized.
lastReceivedHlc: Hlc?
### Last HLC generated by the local replica when this row is the current node.
lastHlc: Hlc?

View file

@ -12,5 +12,5 @@ fields:
### The current node id of the scope.
currentNode: CrdtNode?, relation(optional)
### The nodes associated with the scope.
nodes: List<CrdtNode>?, relation(name=scope_nodes)
### The nodes associated with the scope and their per-scope checkpoints.
nodes: List<CrdtScopeNode>?, relation(name=scope_nodes)

View file

@ -0,0 +1,31 @@
### Authorization membership for shared CRDT scopes.
###
### `database: all`, so the table exists on every node and membership resolves
### with the same code on both ends. The server is the authoritative writer; a
### client copy (once populated) is a read-only cache for UI and offline role
### checks.
###
### Personal-scope membership is implicit: a user always belongs to the scope
### whose UUID equals their auth user UUID. Rows in this table represent shared
### scopes only.
class: CrdtScopeMember
table: crdt_scope_members
database: all
fields:
### Shared scope this membership grants access to.
scope: CrdtScope?, relation(onDelete=Cascade)
### Auth user UUID that may access the scope.
userUuid: UuidValue
### CRDT access role for this shared-scope membership.
role: CrdtScopeRole
indexes:
### Leads with userUuid so the per-cycle membership resolution
### (`WHERE userUuid = ?`) is index-covered, while still enforcing one row
### per (scope, user). `isMember` (`WHERE userUuid AND scopeId`) is covered
### too.
crdt_scope_member_unique_idx:
fields: userUuid, scopeId
unique: true

View file

@ -0,0 +1,19 @@
### A CRDT node's participation and checkpoint state within one scope.
class: CrdtScopeNode
table: crdt_scope_nodes
database: all
fields:
### Scope this participation row belongs to.
scope: CrdtScope?, relation(name=scope_nodes, onDelete=Cascade)
### Stable replica identity participating in the scope.
node: CrdtNode?, relation(onDelete=Cascade)
### Latest HLC from this node acknowledged for this scope.
lastReceivedHlc: Hlc?
indexes:
### Enforces one checkpoint row per (scope, node).
crdt_scope_node_unique_idx:
fields: scopeId, nodeId
unique: true

View file

@ -0,0 +1,8 @@
### CRDT access role for a member of a shared scope.
###
### The role gates CRDT writes only. Reads remain membership-wide.
enum: CrdtScopeRole
serialized: byName
values:
- readOnly
- readWrite

View file

@ -1,7 +1,9 @@
### Session handshake sent once by each peer before the first sync batch.
### Session handshake sent once by each peer before any scope work.
###
### [localNodeId] identifies the sending peer. [syncTablesHash] is the
### deterministic schema hash both sides must agree on before merging data.
### [localNodeId] is this peer's persistent CRDT replica identity, shared
### across all scopes. It is announced once on connect rather than per scope.
### [syncTablesHash] is the deterministic schema hash both sides must agree on
### before merging data.
class: CrdtSyncConnect
extends: CrdtSyncStreamEvent
fields:

View file

@ -0,0 +1,11 @@
### A single scope a peer announces in a [CrdtSyncScopeSet].
###
### Carries the scope UUID and the authoritative [role] the receiving user
### holds in it. The personal scope is announced with [CrdtScopeRole.readWrite].
class: CrdtScopeGrant
fields:
### Scope UUID being announced.
uuidScopeId: UuidValue
### Authoritative CRDT access role of the receiving user in this scope.
role: CrdtScopeRole

View file

@ -0,0 +1,11 @@
### Authoritative or informational scope set exchanged at the start of a cycle.
###
### The server sends the authoritative membership set (with roles). The client
### sends an empty set; the server never widens access from follower-reported
### state.
class: CrdtSyncScopeSet
extends: CrdtSyncStreamEvent
fields:
### Scopes this peer reports for the next sync cycle, each with the receiving
### user's authoritative role.
scopes: List<CrdtScopeGrant>

View file

@ -1,8 +1,10 @@
### One-time checkpoint sent by each peer after connect to exchange required
### information for the sync session.
### Per-scope checkpoint sent by each peer before the first visit to a scope.
class: CrdtSyncSinceHlc
extends: CrdtSyncStreamEvent
fields:
### Scope this checkpoint belongs to.
uuidScopeId: UuidValue
### Per-node checkpoints that describe which changes the sender already has.
###
### Each checkpoint is an [Hlc] whose `nodeId` is the source node being

View file

@ -4,3 +4,4 @@ serialized: byName
values:
- ownershipCollision
- missingDomainRow
- unauthorizedWrite