Public links

Design of record for MeshWeaver#4306. A page that lives in a space can be read only by whoever the space's grants admit. Sharing it used to mean copying it somewhere else and granting there — two copies that drift, and a grant wider than intended. A link shares the page itself.

Two shapes, one node type

Essentials/Link carries LinkContent { target, publicId?, audience?, audienceSubject?, expiresAt?, note, delegatedBy, approvedBy, governedBy, revokedAt, revokedBy }. A public link's node is readable by its audience, so the governed executor writes no note on it: the proposal's rationale stays on the activity its governedBy names.

Global link Public link
Where anywhere a person can write — a home, a space, a course l/{publicId}, the platform-owned l partition — so the URL is /l/{publicId} and no route exists for it
Written by anyone ONLY the governed executor CreateLink (link.publish)
Who opens it whoever can read the link node its audience: anonymous, signed-in (Public) or a group path
What it shows a link through to the target's own page — opened from the TARGET's hub, as the viewer the target node itself — its name and markdown body — read on the LINK's hub as System after the checks below
Grants none none. The type's DECLARED point read admits the audience to the link node at its own path, and no listing names the link

The delegated read

A public link is a read that outlives the session that created it, so every clause of it is checked where it is served — LinkProjection (the data half; LinkLayoutAreas is the template bound to it), at every render and on every change of the target:

  1. The link node was written by the platform (lastModifiedBy = System). The l partition grants nobody write access, and a person's write would restamp the author — so a link node a person managed to write serves nothing, whatever it says.
  2. It is neither revoked nor expired. An expiry is required and capped at 366 days.
  3. The person whose read it delegates (delegatedBy, the proposer) STILL holds Read on the target. Losing that right ends the link at the next render. The target's grants are never touched.
  4. The VIEWER may still point-read the link node itself. The declared point read (the link's audience subject, a group membership above all) is what admitted the viewer, and the target is read as System, so this is a live input like the delegator's right. A viewer removed from the audience while the page is open loses it at once, and the System read of the target closes for that viewer. The projection asks core's DeclaredPointRead.ObservePointRead, never the permission fold alone: the link carries no grant, so the fold denies every audience by design.
  5. Only then is the target read, and only the target node: no child, no satellite. Its markdown body is made INERT first — an executable fence (--execute / --render) becomes a plain code block, because the target's author wrote it and the view would otherwise run it on the visitor's first render AS THE VISITOR (LinkProjection.Inert, which fails closed: a body in which any fence would still execute is not shown). An area embedded in the body is fetched by the visitor's own client as the visitor, resolved against the TARGET's path — the projection writes every relative reference and link absolute under the target (LinkProjection.Anchored), so the page is a template emitted at once and nothing in it can resolve under the link — and it is refused to anyone the target's grants do not admit.

Every refusal the link page decides — expired, the delegator's right lapsed, not issued by the platform — renders the same line, so a visitor cannot tell those apart. A REVOKED link's declared point read admits nobody, so a fresh visit is refused before the page renders at all, exactly as for a link that never existed, and a page held open turns to the refusal line.

How the audience is admitted: a declared point read, never a grant

The Essentials/Link type declares, on its NodeTypeDefinition:

"pointRead": { "subjectProperty": "audienceSubject", "namespace": "l", "areas": ["", "Overview"], "revokedProperty": "revokedAt", "expiresProperty": "expiresAt" }

Core (PointReadDeclaration and DeclaredPointRead, see Doc/Architecture/AccessControl, section "Declared point reads") admits the link's audienceSubject to ONE read: the link node at its own path. That covers the node hub's own data read, the page subscription's delivery gate and the node stream. The delivery gate admits only a read of the link node itself or of the link page (areas: the default area and Overview), never a collection of the link's hub. It is honoured only because this type is platform-owned, only for a DIRECT child of l (the declared namespace, which only the platform writes) and only for a link the platform issued (last written by System). It never admits an API token, and a logged-out (virtual) visitor is never taken for a signed-in one. It is live: a revocation or the expiry cuts a node stream or page held open. It is not a grant row and never enters the permission fold.

No listing, query or scope walk consults it. A visitor's namespace:l scope:descendants is therefore EMPTY, own link included, and a link of the same audience is reachable only by the point read of its own id, which nothing hands out.

Why not a grant. The first shipped shape wrote a Viewer grant for the audience's shared principal (Anonymous, Public, a group) on every link. A grant serves a listing exactly as it serves a point read, so any member of an audience could list every link of that audience. A logged-out caller needed no link at all to list every anonymous one, and a link's id is its only secret. This was measured on a disposable mesh in the review of MeshWeaver.Plugins#3898.

Governed creation and revocation

Org-wide audiences: `Groups/

Group memberships resolve GLOBALLY — the permission evaluator reads every GroupMembership in the mesh and expands the viewer's transitive groups (core PermissionEvaluator, "Group access is resolved GLOBALLY"), so a group defined in one partition is honoured on a grant in another. An org-wide audience therefore needs no new access rule, only a home for the org's group: the Groups partition, one Group node per organisation (Groups/{org}). The governed standard group.org-create creates it: one signature by a GLOBAL ADMIN (re-checked by CreateOrgGroup), then the platform-owned Groups root, the Group node and an Admin grant on that group node alone for the proposer, who then adds members as GroupMembership children. A public link whose audience is Groups/{org} grants that group Viewer on the link node, exactly like any other audience.

group.org-create CREATES a group and never adds a manager to an existing one: a proposal for an org whose group someone else already administers is refused naming the grants, because Admin on a group edits its memberships and those reach every grant that names the group. The only existing group a run converges on is one whose ONLY grant already makes this proposer Admin (a repeated run), and then nothing is written. A group with NO manager grant is refused by name, never completed: it is a create interrupted between its two writes (the group, then the manager grant, each a create that fails on an existing node) or a manager removed by hand, and nothing tells which run left it, so completing it would make whoever proposes next its Admin (MeshWeaver.Plugins#4397). A platform administrator removes such a group, or restores its manager, before a new proposal. A proposal that names no person as proposer is refused before any write, and so is a create that finds grants already written under the absent group's _Access.

An org id is 1–64 ASCII letters, digits, - or _, and never SATELLITE-shaped (an underscore followed by an upper-case letter): Groups/_Policy and Groups/_Access are the partition's own governance slots, and the platform files every id of that shape as a satellite of Groups rather than as a group. The same shape is excluded from a link's public id — such a draw is discarded and drawn again — because a link filed as a satellite of l would leave the main-node listings the audit of published links reads.

The access boundaries, measured

The rules above are pinned over pure inputs by LinkTests and LinkExecutorTests. Whether the platform's access model really holds a visitor to them is measured separately, by Memex.Portal.Gui.Test/PublicLinkAccessBoundaryTest. It runs on a disposable mesh, with the shipped link sources and executors compiled in unchanged and the point-read declaration read from the shipped Link.json. The readings are the ones an anonymous visitor's requests meet: the RLS validator for a point read and for a listing row, the RLS-filtered listing, and the delivery gate a page subscription crosses. Every refusal sits beside a positive control.

The AUDIENCE is measured at the page by PublicLinkAudienceTest, at two seams for every case: the page as the viewer's client receives it (the delivery gate included), and the projection the page binds to, composed on the link's own hub under the viewer's context, beneath that gate. The gate refuses most of these viewers before the projection runs, so a reading at the page alone would not notice a projection that lost the viewer's flags or stopped following the audience.

Where it lives