Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function lobbyChanged
created: 2026-10-15T03:29:13Z
norm: '1'
sig: 1cb5bac0fdcbff95
body_hash: 0e7c895b2f365a42
raw_hash: 46486da0319983f8
vouched: 2026-10-15T04:10:29Z
body_hash: 2ed34b66ea0f3722
raw_hash: 24bbbf285f57751f
vouched: 2026-10-19T21:00:51Z
lines: 196-212
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-15T04:08:49Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 4b03543044edfeb1
raw_hash: 3f6580dfd95cf9e6
vouched: 2026-10-19T21:08:59Z
body_hash: 0dbbb4a75790fa93
raw_hash: 9d6d9723ef47a7dd
vouched: 2026-10-19T21:09:44Z
lines: 266-519
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ anchor: function lobbyChanged
created: 2026-10-15T04:10:21Z
norm: '1'
sig: 1cb5bac0fdcbff95
body_hash: 0e7c895b2f365a42
raw_hash: 46486da0319983f8
body_hash: 2ed34b66ea0f3722
raw_hash: 24bbbf285f57751f
vouched: 2026-10-19T21:00:52Z
lines: 230-243
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-15T06:00:20Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 4b03543044edfeb1
raw_hash: 3f6580dfd95cf9e6
vouched: 2026-10-19T21:08:59Z
body_hash: 0dbbb4a75790fa93
raw_hash: 9d6d9723ef47a7dd
vouched: 2026-10-19T21:09:44Z
lines: 281-561
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-19T18:25:49Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 4b03543044edfeb1
raw_hash: 3f6580dfd95cf9e6
vouched: 2026-10-19T21:08:59Z
body_hash: 0dbbb4a75790fa93
raw_hash: 9d6d9723ef47a7dd
vouched: 2026-10-19T21:09:44Z
lines: 379-692
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5AZEMYSS1RF0S1P6KWJ87X9
anchor: function everyone
created: 2026-10-19T21:00:34Z
norm: '1'
sig: 2fcdcab40f17ca79
body_hash: 91bdaf775e3af9ab
raw_hash: 4824fdf4d5799c3d
lines: 165-172
---

the directory, lobby events, and older peers carry every channel, so everything the owner's app lists from them must pass the owner's project access check
34 changes: 29 additions & 5 deletions apps/host/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,25 @@ function listed(seen: Set<string>): Listing[] {
);
}

/** Every channel the owner's app lists: this host's, reachable peers', and the directory's. */
function everyone(own: Listing[]): Listing[] {
const reachable = [...own, ...peers.listings()];
return [...reachable, ...listed(new Set(reachable.map((value) => value.id)))];
/**
* Every channel the owner's app lists: this host's, then reachable peers' and the directory's in
* projects the owner can access. The directory, lobby events, and hosts from before project access
* carry every channel, so the owner's host filters them too.
*/
async function everyone(own: Listing[]): Promise<Listing[]> {
const reachable = peers.listings();
const others = [
...reachable,
...listed(new Set([...own, ...reachable].map((value) => value.id))),
];
return [...own, ...await visible(others)];
}

async function visible(others: Listing[]): Promise<Listing[]> {
const checks = await Promise.all(
others.map((value) => access.canAccess(catalog.user, channelProject(value))),
);
return others.filter((_, index) => checks[index]);
}

/** A peer's person sees this host's channels in projects they can access; the owner sees all. */
Expand Down Expand Up @@ -304,7 +319,7 @@ let known = new Set<string>();
async function lobbyProjects(own?: Listing[]): Promise<void> {
if (!directory.url()) return;
own ||= await local();
projectOf = new Map(everyone(own).map((value) => [value.id, channelProject(value)]));
projectOf = new Map((await everyone(own)).map((value) => [value.id, channelProject(value)]));
known = new Set(projectOf.values());
for (const value of await projects.list()) known.add(projectId(name, value.path, value.repo));
lobbyChanged();
Expand All @@ -328,6 +343,8 @@ function lobbyChanged(): void {
if (window.focused !== false) found.away = false;
}
lobby.update(known, here);
// A project the owner lost access to keeps its lobby until the next publication, but not its people.
presenceChanged();
}

let presenceSent = "";
Expand Down Expand Up @@ -753,6 +770,9 @@ async function handle(
));
const hosted = known && known.hosted ? known : undefined;
if (hosted) {
if (!(await visible([hosted])).length) {
throw new Error("You don't have access to this channel's project");
}
if (request.request.op === "kill" && client.user !== hosted.owner) {
throw new Error("Only the channel's owner can kill it");
}
Expand All @@ -762,6 +782,10 @@ async function handle(
}
const machine = !client.peer && peers.find(request.channel);
if (!machine) throw new Error("No reachable host runs that channel");
const listing = peers.listings().find((value) => value.id === request.channel)!;
if (!(await visible([listing])).length) {
throw new Error("You don't have access to this channel's project");
}
const target = await remote(client, machine.name);
return target.channel(request.channel, request.request, watch, trace);
}
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,10 @@ reachable peer (`shares-changed`) to read the directory again, since a lobby can
who just got access. After each directory read, a host whose shares changed resends each peer
the channels its person can access and ends their watches on channels they no longer can. Each
peer's listing is filtered for its person, and is resent when a peer's GitHub login arrives late.
The owner's host also filters everything else it shows its app, since the directory, lobby
events, and hosts from before project access carry every channel: peers' and the directory's
listings, lobby subscriptions and their presence, and hosted or relayed channels it would open.
A project the owner loses access to leaves the sidebar and project picker on the next listing.

**Known limitation: project access is cooperative.** It holds between hosts running unmodified
Ace, not against a teammate who tampers with theirs. The team services accept one credential
Expand Down
Loading