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:
- The link node was written by the platform (
lastModifiedBy= System). Thelpartition 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. - It is neither revoked nor expired. An expiry is required and capped at 366 days.
- 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. - 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. - 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
link.publish(Governance) — inputstarget,audience,expiresAt; gatesManual("content-reviewed")and one signature.CreateLink(System, governed by the activity) first requires that a node EXISTS at the exact target (a System listing: a permission check proves an inherited right on a path, so a missing descendant of a readable space would pass it and content created there later would be served unreviewed), then re-checks at execution that the proposer can read the target and that a COUNTED signer holdsAllon it (the target's admin). It then writes thelroot (idempotent) and the link, which carries itsaudienceSubject. That is ONE write and no grant: the type's declared point read admits the audience.- The approval step-up (MeshWeaver#4305): once the receipt service ships, a signature is
admitted only with its signer's valid receipt —
link.publish's signature consumes it with no change to this standard. link.revoke: unattended for the publisher (delegatedBy) or the signer (approvedBy).RevokeLinkstampsrevokedAt, which ends the declared point read and the page, and removes a legacy audience grant when the link still carries one.link.repair-admission: one signature by a global administrator, no inputs.RepairLinkAdmission(System, governed) moves every link published under the old grant shape onto the declared point read. It writes each link'saudienceSubjectand removes its legacyViewergrant, idempotently. It rewrites ONLY a link the platform issued, checked on the current revision inside the write. A link a person wrote is never given System provenance; its legacy grant is still removed. Run it once on every instance that published public links before this change; until it runs, such a link still leaks into its audience's listings.
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.
- A public link reads only its target. The visitor's point read opens the link. The page shows the target's record and nothing of its sibling. The target, its sibling and its space stay unreadable, and a listing of the space finds nothing.
- No listing names a public link. The visitor's listing of
l(scope:descendants,scope:children, by node type) is EMPTY, its own link included. The same link as a listing row is refused. A second link of the SAME audience opens only by the point read of its own id, and a link of another audience is refused. The same holds INSIDE an audience: a signed-in user whose point read of asigned-inlink opens still lists nothing underl, and that link as a listing row is refused. Negative controls: restoring the legacy grant row on one link leaks it into the anonymous listing, and restoring the signed-in audience's legacy grant leaks that link into the signed-in user's listing. - Revoking it ends access. The page the visitor holds open turns to the refusal line. After that, the point read of the link is refused.
- The repair moves a link published under the legacy shape onto the declared point read. It
leaks before the repair and is gone from every listing after, its grant row removed and its point
read still open. It refuses to run without a global administrator's signature, and a link a
person planted in
lis left unchanged by it (still person-authored, still refused). - Negative control for the suite (
PublicLinkWithoutDeclarationTest): with the type's declaration removed, a published link is refused to its audience. The admission above is the declaration's doing, not a grant.
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.
- A group audience follows its memberships, on an open page. A link whose audience is
Groups/{org}opens for a member; a signed-in non-member is refused. When the member'sGroupMembershipnode is deleted, the page she holds OPEN turns to the refusal line, the projection beneath the gate does too, and her point read is refused. - A signed-in-only link refuses a virtual visitor and an API token. Both are refused at the page and, on its own, by the projection: it hands the viewer's whole context, flags included, to the declared point read. Positive control: the same link opens for a signed-in browser at both seams, and the API token carries that same user's id, so the refusal is the token flag's alone.
- The mutations that turn it red (run locally when the suite was written): stripping the
viewer's
IsVirtualandIsApiTokenflags inLinkProjection.ViewerContextOfreddens the virtual-visitor case, strippingIsApiTokenalone reddens the token case, and deciding the viewer's point read once (Take(1)) instead of following it reddens the group case. In all three the PAGE still refused or closed, because the outer gate did the work. Only the projection seam went red, which is why the suite reads both seams.
Where it lives
Essentials/Link—LinkContent,LinkRules(pure),LinkProjection(data),LinkLayoutAreas(the template),LinkTexts(en/de);TestsrunsLinkTests.Governance/Activity/Source/LinkExecutors.cs—CreateLink,RevokeLink,RepairLinkAdmission; the standardsGovernance/Standards/link.publish,link.revokeandlink.repair-admission.- The skill
/public-link(Governance/Skill/public-link) — it ships with Governance, because the procedure it teaches files aGovernance/Proposal; Essentials holds the link type and this guide, and does not depend on Governance.