Skip to content
252 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The docs service has tables for artifacts beside Docs' pages, and one tested rule for who can read and change an artifact: its owner, shares on it or above it, its space, and everyone in the workspace or with the link.1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 aclChain,
6 aclRootOf,
7 agentAbilities,
8 agentFolioRole,
9 canShare,
10 effectiveRole,
11 explicitAccess,
12 folioPathOf,
13 folioReadableByAll,
14 folioReadableByWorkspace,
15 folioScope,
16 generalRoleCap,
17 inheritsSpace,
18 isPrivateFolio,
19 materialize,
20 roleOf,
21 type FolioAclNode,
22 type FolioGrant,
23 type Person,
24 type SpaceRules,
25} from "./access.ts";
26
27// People: Ana (team web), Bo (no team), Cy (team design), Wes (workspace owner).
28const ana: Person = { user_id: "ana", owner: false, teams: new Set(["web"]) };
29const bo: Person = { user_id: "bo", owner: false, teams: new Set() };
30const cy: Person = { user_id: "cy", owner: false, teams: new Set(["design"]) };
31const wes: Person = { user_id: "wes", owner: true, teams: new Set() };
32
33// Spaces.
34const open: SpaceRules = { kind: "workspace", team: null, default_role: "edit", members: [] };
35const openView: SpaceRules = { kind: "workspace", team: null, default_role: "view", members: [] };
36const team: SpaceRules = { kind: "team", team: "web", default_role: "comment", members: [] };
37const membersOnly: SpaceRules = { kind: "private", team: null, default_role: null, members: [{ principal: "user:cy", role: "manage" }] };
38const SPACES: Record<string, SpaceRules> = { open, openView, team, membersOnly };
39
40function node(id: string, over: Partial<FolioAclNode> = {}): FolioAclNode {
41 return { id, owner: "user:ana", parent_id: null, space_id: null, inherit: true, general_access: "none", general_role: null, created_at: "2026-10-01T00:00:00Z", ...over };
42}
43
44/** The role a person has on `id`, given every node and grant. */
45function roleIn(nodes: FolioAclNode[], grants: Record<string, FolioGrant[]>, id: string, person: Person, visited = false) {
46 const byId = new Map(nodes.map((n) => [n.id, n]));
47 const chain = aclChain(id, byId);
48 const root = chain[chain.length - 1]!;
49 const space = root.space_id ? SPACES[root.space_id]! : null;
50 return effectiveRole(chain, new Map(Object.entries(grants)), space ? roleOf(space, person) : null, person, { visited });
51}
52
53test("private: only the owner, and not the workspace owner", () => {
54 const nodes = [node("f")];
55 assert.equal(roleIn(nodes, {}, "f", ana), "manage");
56 assert.equal(roleIn(nodes, {}, "f", bo), null);
57 assert.equal(roleIn(nodes, {}, "f", wes), null);
58 const byId = new Map(nodes.map((n) => [n.id, n]));
59 assert.equal(isPrivateFolio(aclChain("f", byId), new Map()), true);
60});
61
62test("an open space gives every member its base role; owners manage", () => {
63 const nodes = [node("f", { space_id: "open", owner: "user:cy" })];
64 assert.equal(roleIn(nodes, {}, "f", ana), "edit");
65 assert.equal(roleIn(nodes, {}, "f", bo), "edit");
66 assert.equal(roleIn(nodes, {}, "f", cy), "manage");
67 assert.equal(roleIn(nodes, {}, "f", wes), "manage");
68 const byId = new Map(nodes.map((n) => [n.id, n]));
69 assert.equal(isPrivateFolio(aclChain("f", byId), new Map()), false);
70});
71
72test("a team space: its team gets the base role; others nothing; workspace owners manage", () => {
73 const nodes = [node("f", { space_id: "team", owner: "user:cy" })];
74 assert.equal(roleIn(nodes, {}, "f", ana), "comment");
75 assert.equal(roleIn(nodes, {}, "f", bo), null);
76 assert.equal(roleIn(nodes, {}, "f", wes), "manage");
77});
78
79test("a members-only space: only its members, never the workspace owner", () => {
80 const nodes = [node("f", { space_id: "membersOnly", owner: "user:cy" })];
81 assert.equal(roleIn(nodes, {}, "f", cy), "manage");
82 assert.equal(roleIn(nodes, {}, "f", ana), null);
83 assert.equal(roleIn(nodes, {}, "f", wes), null);
84});
85
86test("grants to a person, an agent and a team", () => {
87 const nodes = [node("f")];
88 const grants = { f: [{ principal: "user:bo", role: "comment" as const }, { principal: "team:design", role: "edit" as const }, { principal: "agent:ag1", role: "edit" as const }] };
89 assert.equal(roleIn(nodes, grants, "f", bo), "comment");
90 assert.equal(roleIn(nodes, grants, "f", cy), "edit");
91 // An agent grant gives no person anything.
92 assert.equal(roleIn(nodes, grants, "f", wes), null);
93 const byId = new Map(nodes.map((n) => [n.id, n]));
94 assert.equal(isPrivateFolio(aclChain("f", byId), new Map(Object.entries(grants))), false);
95});
96
97test("a grant raises a space role but never lowers it", () => {
98 const nodes = [node("f", { space_id: "openView", owner: "user:cy" })];
99 assert.equal(roleIn(nodes, { f: [{ principal: "user:bo", role: "edit" }] }, "f", bo), "edit");
100 assert.equal(roleIn(nodes, { f: [{ principal: "user:ana", role: "view" }] }, "f", ana), "view");
101 const open2 = [node("f", { space_id: "open", owner: "user:cy" })];
102 assert.equal(roleIn(open2, { f: [{ principal: "user:ana", role: "view" }] }, "f", ana), "edit");
103});
104
105test("general access: workspace gives every member its role, never manage", () => {
106 const nodes = [node("f", { general_access: "workspace", general_role: "comment" })];
107 assert.equal(roleIn(nodes, {}, "f", bo), "comment");
108 assert.equal(roleIn(nodes, {}, "f", wes), "comment");
109 const capped = [node("f", { general_access: "workspace", general_role: "manage" })];
110 assert.equal(roleIn(capped, {}, "f", bo), "edit");
111 assert.equal(generalRoleCap(null), "view");
112});
113
114test("general access: link gives its role only to people who opened it", () => {
115 const nodes = [node("f", { general_access: "link", general_role: "view" })];
116 assert.equal(roleIn(nodes, {}, "f", bo), null);
117 assert.equal(roleIn(nodes, {}, "f", bo, true), "view");
118});
119
120test("nested docs inherit their parent's grants, space and general access", () => {
121 const nodes = [
122 node("top", { space_id: "team", owner: "user:cy" }),
123 node("mid", { space_id: "team", owner: "user:cy", parent_id: "top" }),
124 node("leaf", { space_id: "team", owner: "user:cy", parent_id: "mid" }),
125 ];
126 const grants = { top: [{ principal: "user:bo", role: "view" as const }] };
127 assert.equal(roleIn(nodes, grants, "leaf", ana), "comment");
128 assert.equal(roleIn(nodes, grants, "leaf", bo), "view");
129 assert.equal(roleIn(nodes, grants, "leaf", wes), "manage");
130 const byId = new Map(nodes.map((n) => [n.id, n]));
131 assert.deepEqual(
132 aclChain("leaf", byId).map((n) => n.id),
133 ["leaf", "mid", "top"],
134 );
135 assert.equal(inheritsSpace(aclChain("leaf", byId)), true);
136});
137
138test("a parent's owner keeps full access to what others add under it", () => {
139 const nodes = [node("top"), node("child", { parent_id: "top", owner: "user:bo" })];
140 assert.equal(roleIn(nodes, {}, "child", ana), "manage");
141 assert.equal(roleIn(nodes, {}, "child", bo), "manage");
142 assert.equal(roleIn(nodes, {}, "child", cy), null);
143});
144
145test("restricting stops the space, the parent's grants and general access", () => {
146 const nodes = [
147 node("top", { space_id: "open", owner: "user:cy", general_access: "workspace", general_role: "view" }),
148 node("secret", { space_id: "open", owner: "user:cy", parent_id: "top", inherit: false }),
149 node("under", { space_id: "open", owner: "user:cy", parent_id: "secret" }),
150 ];
151 const grants = { top: [{ principal: "user:bo", role: "edit" as const }], secret: [{ principal: "user:ana", role: "comment" as const }] };
152 assert.equal(roleIn(nodes, grants, "secret", bo), null);
153 assert.equal(roleIn(nodes, grants, "secret", ana), "comment");
154 assert.equal(roleIn(nodes, grants, "under", ana), "comment");
155 assert.equal(roleIn(nodes, grants, "under", wes), null);
156 assert.equal(roleIn(nodes, grants, "under", cy), "manage");
157 // A restricted top-level folio in a space leaves the space's people out too.
158 const top = [node("t", { space_id: "open", owner: "user:cy", inherit: false })];
159 assert.equal(roleIn(top, {}, "t", ana), null);
160 assert.equal(roleIn(top, {}, "t", wes), null);
161});
162
163test("the access root and path of a new folio", () => {
164 assert.equal(aclRootOf({ id: "a", inherit: true, parent_id: null }, null), "a");
165 assert.equal(aclRootOf({ id: "b", inherit: true, parent_id: "a" }, "a"), "a");
166 assert.equal(aclRootOf({ id: "c", inherit: false, parent_id: "b" }, "a"), "c");
167 assert.equal(folioPathOf("a", null), "/a/");
168 assert.equal(folioPathOf("b", "/a/"), "/a/b/");
169});
170
171test("materialize writes the owner, ancestor owners and grants up to the access root, highest role each", () => {
172 const nodes = [
173 node("top", { owner: "user:ana" }),
174 node("child", { parent_id: "top", owner: "user:bo" }),
175 node("restricted", { parent_id: "child", owner: "user:bo", inherit: false }),
176 ];
177 const byId = new Map(nodes.map((n) => [n.id, n]));
178 const grants = new Map<string, FolioGrant[]>([
179 ["top", [{ principal: "user:cy", role: "view", granted_at: "2026-10-02T00:00:00Z" }]],
180 ["child", [{ principal: "user:cy", role: "edit", granted_at: "2026-10-03T00:00:00Z" }]],
181 ]);
182 const rows = materialize(["top", "child", "restricted"], byId, grants);
183 const of = (id: string) => Object.fromEntries(rows.filter((r) => r.folio_id === id).map((r) => [r.principal, `${r.role}@${r.via}`]));
184 assert.deepEqual(of("top"), { "user:ana": "manage@owner", "user:cy": "view@top" });
185 assert.deepEqual(of("child"), { "user:bo": "manage@owner", "user:cy": "edit@child", "user:ana": "manage@top" });
186 assert.deepEqual(of("restricted"), { "user:bo": "manage@owner" });
187 assert.equal(explicitAccess(aclChain("child", byId), grants).get("user:cy")?.since, "2026-10-03T00:00:00Z");
188});
189
190test("the index scope is the space's only when access is exactly the space's", () => {
191 const byId = (nodes: FolioAclNode[]) => new Map(nodes.map((n) => [n.id, n]));
192 const plain = byId([node("a", { space_id: "open" })]);
193 assert.equal(folioScope(aclChain("a", plain), new Map()), "space:open");
194 assert.equal(folioScope(aclChain("a", plain), new Map([["a", [{ principal: "user:bo", role: "view" }]]])), "folio:a");
195 const general = byId([node("a", { space_id: "open", general_access: "workspace", general_role: "view" })]);
196 assert.equal(folioScope(aclChain("a", general), new Map()), "folio:a");
197 const nested = byId([node("a", { space_id: "open" }), node("b", { space_id: "open", parent_id: "a", owner: "user:bo" })]);
198 assert.equal(folioScope(aclChain("b", nested), new Map()), "space:open");
199 const restricted = byId([node("a", { space_id: "open" }), node("b", { space_id: "open", parent_id: "a", inherit: false })]);
200 assert.equal(folioScope(aclChain("b", restricted), new Map()), "folio:b");
201 const priv = byId([node("p")]);
202 assert.equal(folioScope(aclChain("p", priv), new Map()), "folio:p");
203});
204
205test("readable by the whole workspace: open spaces and workspace general access only", () => {
206 const byId = (nodes: FolioAclNode[]) => new Map(nodes.map((n) => [n.id, n]));
207 assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { space_id: "open" })])), open), true);
208 assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { space_id: "team" })])), team), false);
209 assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { space_id: "membersOnly" })])), membersOnly), false);
210 assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a")])), null), false);
211 assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { general_access: "workspace", general_role: "view" })])), null), true);
212 // A link is never the workspace's, even for people who opened it.
213 assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { general_access: "link", general_role: "view" })])), null), false);
214 // Restricted inside an open space: not the workspace's.
215 const restricted = byId([node("a", { space_id: "open" }), node("b", { space_id: "open", parent_id: "a", inherit: false })]);
216 assert.equal(folioReadableByWorkspace(aclChain("b", restricted), open), false);
217});
218
219test("readable by an audience only when every person in it can read", () => {
220 const nodes = new Map([["f", node("f", { owner: "user:ana" })]]);
221 const chain = aclChain("f", nodes);
222 const grants = new Map([["f", [{ principal: "user:bo", role: "view" as const }]]]);
223 assert.equal(folioReadableByAll(chain, grants, null, [ana, bo]), true);
224 assert.equal(folioReadableByAll(chain, grants, null, [ana, bo, cy]), false);
225 const link = new Map([["l", node("l", { general_access: "link", general_role: "view" })]]);
226 assert.equal(folioReadableByAll(aclChain("l", link), new Map(), null, [ana, bo]), false);
227 assert.equal(
228 folioReadableByAll(aclChain("l", link), new Map(), null, [ana, bo], (p) => p.user_id === "bo"),
229 true,
230 );
231});
232
233test("an agent is capped by its asker and narrowed by its audience", () => {
234 // The agent holds an edit grant, its asker only view: it may only read.
235 const nodes = [node("f", { owner: "user:cy" })];
236 const grants = { f: [{ principal: "agent:ag1", role: "edit" as const }, { principal: "user:bo", role: "view" as const }] };
237 const asker = roleIn(nodes, grants, "f", bo);
238 assert.equal(asker, "view");
239 assert.equal(agentFolioRole(asker, true), "view");
240 assert.deepEqual(agentAbilities(agentFolioRole(asker, true), "edit"), { read: true, suggest: false, edit: false });
241 // Someone in the conversation can't read it: the agent can't either.
242 assert.equal(agentFolioRole("manage", false), null);
243 // Someone who can't read it gets nothing from the agent's grant.
244 assert.equal(agentFolioRole(roleIn(nodes, grants, "f", ana), true), null);
245});
246
247test("who may share", () => {
248 assert.equal(canShare("manage"), true);
249 assert.equal(canShare("edit"), false);
250 assert.equal(canShare("edit", true), true);
251 assert.equal(canShare(null, true), false);
252});