Cloudflare OS implements a permission graph where collaborators gain access through user edges (direct grants with roles) or share-link edges (redeemed keys); role-based use/build access controls restrict what operations each collaborator can invoke, while lazy revocation re-evaluates graph reachability at every access attempt rather than cascading deletions. Share links are immutable first-class nodes stored as hashed 128-bit keys with soft revocation; multiple copies (aliases) of the same link collapse to a single permission edge per collaborator, and transitively revoke when the link's creator loses access.
A use collaborator may only call getUiBundle(), connectToGadget() (mainline only, no chatId), getMetadata()/subscribeToMetadata() (restricted to id/title/owner/role), and subscribeToPresence(). Every other Overseer method throws Unauthorized.[1] For use collaborators, subscribeToConsoleLogs() and subscribeToActions() return inert subscriptions that never deliver data rather than throwing Unauthorized, because the editor speculatively opens both from top-level hooks before switching to the use-only view.[1] A caller may never grant a role higher than their own effective role. Only the owner and build collaborators can invoke sharing methods; sharing is not in the use allowlist.[1] Share link keys are 128-bit random values; the server stores only the HMAC-SHA-256 hash (using the domain-separation constant SHARE_KEY_HMAC_KEY) and never the raw key, so a database leak does not expose valid share keys.[1] A share link's raw key is shown to the creator only once at mint time and is never stored server-side; re-copying a link mints a new key rather than reproducing the old one.[1] In the shareKeys table, the first key's hash serves as the link ID and carries the link's metadata; subsequent copies store only an alias pointing back to that ID.[1] Share key redemption and gadget opening happen atomically in a single RPC call openGadget(id, shareKey), allowing subsequent calls to be pipelined on the returned Overseer stub without a separate redemption round-trip.[1] A shared gadget does not appear on a collaborator's home page until they first open it, at which point UserDurableObject.recordSharedGadgetOpen() creates a record caching the gadget's title and owner's profile. The lastActive timestamp is updated on each subsequent open.[1] Dismissing a shared gadget from the home page removes the local record from the collaborator's user account but does not revoke access; opening the gadget again via URL causes it to reappear.[1] When a collaborator's access is revoked, the stale record remains on their home page until they try to open it, at which point open() returns a workspace access-denied error. The system does not proactively remove the record from their account.[1] The permission graph has two edge types: User edge (records a specific sharer by profile.id, role, timestamp, optional note) and Share-link edge (records redemption of a specific link by keyId, with the role taken from the link). A collaborator retains access as long as they have at least one valid edge.[1] Edges and share links created before roles were introduced have no role field and are treated as build for backwards compatibility.[1] Share links are first-class nodes in the permission graph. If a link's creator loses access, the link is transitively revoked, removing anyone who gained access solely through it.[1] The owner is the implicit root of the permission graph, is never stored in the collaborators table, and cannot be removed. All permission chains must ultimately trace back to the owner.[1] Revoking a share link sets the link's revoked flag rather than deleting it; all edges referencing the link remain intact to avoid dangling references. Link copies (aliases) are deleted outright since no edge ever references an alias.[1] Because the permission graph is never destructively pruned, revocation is reversible: re-adding a removed collaborator restores their full subtree of downstream grants without any additional steps.[1] packages/workshop-backend/src/sharing.ts implements a lazy revocation model: removing a collaborator or revoking a share link never deletes records or cascades to downstream edges — access is re-evaluated at every open() call by checking graph reachability from the owner.[2] Because records are never deleted in packages/workshop-backend/src/sharing.ts's lazy revocation model, revocation is reversible: re-adding a removed collaborator restores access for them and transitively for everyone they shared with.[2] Raw share keys are never stored server-side in packages/workshop-backend/src/sharing.ts; only their HMAC-SHA-256 hex digest (keyed with a fixed 256-bit personalization constant SHARE_KEY_HMAC_KEY) is persisted as the storage ID.[2] A ShareLinkRecord in packages/workshop-backend/src/sharing.ts uses soft revocation: revoking a link sets revoked: true rather than deleting the record, preserving permission-graph edges and allowing future restoration.[2] A ShareKeyAliasRecord in packages/workshop-backend/src/sharing.ts is an additional key for an existing share link (created when the user copies a link). It carries no metadata; redeeming it resolves to the parent ShareLinkRecord so all copies of a link behave identically.[2] SharingManager.getEffectiveRole() in packages/workshop-backend/src/sharing.ts always returns "build" for the gadget owner without consulting the permission graph.[2] SharingManager.hasAnyShares() in packages/workshop-backend/src/sharing.ts returns true if there is any collaborator reachable from the owner in the permission graph, OR any un-revoked share link — because un-revoked links can still be redeemed by new users.[2] When redeeming a share key alias in packages/workshop-backend/src/sharing.ts, the permission edge is recorded against the alias's parent link ID (not the alias key's own ID), so multiple copies of a link collapse to a single grant per collaborator.[2] In packages/workshop-backend/src/sharing.ts, a CollaboratorRecord stores a denormalized profile snapshot in its profile field to allow display without hitting the user's Durable Object.[2] The SharingStorage interface in packages/workshop-backend/src/sharing.ts requires a byAlias non-unique index on the shareKeys collection, keyed by the alias field (parent link ID), to efficiently enumerate all copies of a given link.[2]
The prohibitAllSharing observation flag was implemented to prevent gadget content from being shared; BigQuery observations were subsequently marked with this flag.[3]
The sharing model was extended with explicit roles — 'build' vs. 'use' — giving workspace owners control over whether a recipient can edit or only run a gadget.[4] Sharing now enforces that recipients have direct access to all underlying gatekeepers observed by the gadget before a share link can be accepted.[5] ObserverConfigModal.tsx implements observer verification logic that handles both legacy and current grant structures; legacy account grants remain in circulation and must be accounted for to avoid verification failures. ObserverConfigModal.test.tsx includes a test covering observer verification against legacy grant structures, ensuring the verification path remains exercised when new grant types are added or the observer config modal is modified. docs/observers.md and docs/sharing.md document observer verification behavior and session-restart semantics when observer scopes are widened. When an observer's granted scope is widened after initial verification, packages/workshop-backend/src/overseer.ts and sharing.ts restart the session so expanded permissions apply immediately without requiring a manual reconnect. Observer verification in packages/workshop-backend/src/observability.ts miscounted verified scopes, producing false-positive grants that incorrectly permitted access; the coverage-bookkeeping defect has been corrected.
Sources