diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YSPPENX7WWNRHXH88REJST-the-only-place-a-tailscale-l.md b/.lane/memory/apps/host/src/gateway.ts/01M4YSPPENX7WWNRHXH88REJST-the-only-place-a-tailscale-l.md index 3d57e30f..860c8b84 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YSPPENX7WWNRHXH88REJST-the-only-place-a-tailscale-l.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YSPPENX7WWNRHXH88REJST-the-only-place-a-tailscale-l.md @@ -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 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md index 2c6bfff7..7a8a863f 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md @@ -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 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YW201R68V6926M1HHVZ1X8-runs-synchronously-on-window.md b/.lane/memory/apps/host/src/gateway.ts/01M4YW201R68V6926M1HHVZ1X8-runs-synchronously-on-window.md index de18f214..b8884fd7 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YW201R68V6926M1HHVZ1X8-runs-synchronously-on-window.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YW201R68V6926M1HHVZ1X8-runs-synchronously-on-window.md @@ -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 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md index 1b222e34..7364626c 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md @@ -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 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5APK9YCQH467YNMKAC8BT5B-a-move-to-a-cloud-workspace.md b/.lane/memory/apps/host/src/gateway.ts/01M5APK9YCQH467YNMKAC8BT5B-a-move-to-a-cloud-workspace.md index 23488a48..2f570f3e 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5APK9YCQH467YNMKAC8BT5B-a-move-to-a-cloud-workspace.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5APK9YCQH467YNMKAC8BT5B-a-move-to-a-cloud-workspace.md @@ -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 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5AZEMYSS1RF0S1P6KWJ87X9-the-directory-lobby-events-a.md b/.lane/memory/apps/host/src/gateway.ts/01M5AZEMYSS1RF0S1P6KWJ87X9-the-directory-lobby-events-a.md new file mode 100644 index 00000000..8621a65f --- /dev/null +++ b/.lane/memory/apps/host/src/gateway.ts/01M5AZEMYSS1RF0S1P6KWJ87X9-the-directory-lobby-events-a.md @@ -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 diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index c6ffd85f..5a57780f 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -157,10 +157,25 @@ function listed(seen: Set): 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 { + 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 { + 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. */ @@ -304,7 +319,7 @@ let known = new Set(); async function lobbyProjects(own?: Listing[]): Promise { 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(); @@ -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 = ""; @@ -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"); } @@ -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); } diff --git a/docs/architecture.md b/docs/architecture.md index 04072267..85cb8ee5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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