Public: anyone can read this; the crew can change it.

A private repository is absent from every public door, proven by one sweep #12413

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

I added `test/private-sweep.test.ts`, a test showing that none of 24 public doors give away a private repository. No door leaked, so no product code changed, including `src/web/standards.ts`, which the issue named in case a fix was needed.

shipped production is serving it deploy d003798

opened this

I added test/private-sweep.test.ts, a test showing that none of 24 public doors give away a private repository. No door leaked, so no product code changed, including src/web/standards.ts, which the issue named in case a fix was needed.

The test makes a private repository alice/zqxsecretvault. Its description, one issue and one commit message each carry a distinctive word of their own. Each door is fetched twice: once as a stranger and once as a signed-in person who isn't a member. The test then checks that none of the four words appear in any response body or header. The doors cover /explore, /explore.atom, /api/v1/explore, /search, /api/v1/search, /sitemap.xml, /robots.txt, /llms.txt, the .well-known files (agent card, MCP card, api-catalog, forge), /feed, /changelog.atom, the owner's profile page, a public repository's forks list and activity feed, the share cards, and the private repository's own page, issue and API paths. They all sit in one DOORS array, so a new door is one more line.

Some doors are asked by name: a search query, a share-card address, the repository's own path. These print back the name the person typed, so for those the test checks two things instead:

  • Other words: the description, issue and commit words must not appear.
  • Control: the status and body must match the response for a same-length name that never existed. Headers are left out of this comparison because the timing and etag headers change on every request.

The first version flagged those echoes as leaks, which is why the test works this way.

The owner does see the repository on /search and /api/v1/search, the doors that check who is asking. The owner also reaches its page and API path.

To check the test can actually fail, I ran a copy with the repository made public. It reported 24 leaks across 12 doors, including the sitemap, llms.txt, the profile page, /explore and the share cards.

What I left out:

  • Collaborators: "a member of the repo" is the owner. A collaborator added through an org or team is not tried.
  • Private fork: the forks list is checked on a public repository with no private fork.
  • Plan and estimate: the keep_plan and estimate tools weren't available in this run, so I posted no plan on the issue and recorded no estimate.

To try it: bun test test/private-sweep.test.ts (4 pass). bun scripts/gate.ts and bun x tsc --noEmit are clean.

🤖 Generated with Claude Code

Closes #12405

1 earlier comment · 1 event
commented

I added test/private-sweep.test.ts, a test showing that none of 24 public doors give away a private repository. No door leaked, so no product code changed, including src/web/standards.ts, which the issue named in case a fix was needed.

The test makes a private repository alice/zqxsecretvault. Its description, one issue and one commit message each carry a distinctive word of their own. Each door is fetched twice: once as a stranger and once as a signed-in person who isn't a member. The test then checks that none of the four words appear in any response body or header. The doors cover /explore, /explore.atom, /api/v1/explore, /search, /api/v1/search, /sitemap.xml, /robots.txt, /llms.txt, the .well-known files (agent card, MCP card, api-catalog, forge), /feed, /changelog.atom, the owner's profile page, a public repository's forks list and activity feed, the share cards, and the private repository's own page, issue and API paths. They all sit in one DOORS array, so a new door is one more line.

Some doors are asked by name: a search query, a share-card address, the repository's own path. These print back the name the person typed, so for those the test checks two things instead:

  • Other words: the description, issue and commit words must not appear.
  • Control: the status and body must match the response for a same-length name that never existed. Headers are left out of this comparison because the timing and etag headers change on every request.

The first version flagged those echoes as leaks, which is why the test works this way.

The owner does see the repository on /search and /api/v1/search, the doors that check who is asking. The owner also reaches its page and API path.

To check the test can actually fail, I ran a copy with the repository made public. It reported 24 leaks across 12 doors, including the sitemap, llms.txt, the profile page, /explore and the share cards.

What I left out:

  • Collaborators: "a member of the repo" is the owner. A collaborator added through an org or team is not tried.
  • Private fork: the forks list is checked on a public repository with no private fork.
  • Plan and estimate: the keep_plan and estimate tools weren't available in this run, so I posted no plan on the issue and recorded no estimate.

To try it: bun test test/private-sweep.test.ts (4 pass). bun scripts/gate.ts and bun x tsc --noEmit are clean.

🤖 Generated with Claude Code

Changes requested — before the latest push

The sweep is well built: one DOORS array, a control that compares a by-name door against a name that never existed, and the owner checks. But DOORS misses public doors that list repositories, and that is what the issue is about. Round-one fixes:

  1. Add the listing doors in src/http/api.ts: /api/v1/repos (line 1606), /api/v1/users/alice, /api/v1/users/alice/repos. A stranger and a non-member should not see the private repo there. The owner should (set roleAware for the listing doors).
  2. Add /mcp. It is a public door with tools that list and search repos. Call tools/list and a repo list or search tool through POST. The body() helper only does GET, so give a Door an optional method and body.
  3. Add other repo-listing doors: /alice.atom or similar profile feeds, /api/v1/repos/alice/open/issues plus the issue search, and any /explore pagination or cursor variant. Grep the router for GET routes that return repos or issues, and add the ones missing, so the claim 'every public door' is true.
  4. Box 2 says 'a member'. Add a collaborator (non-owner member) who sees the repo on a role-aware door, or say in the test header why only the owner is covered. As written, only the owner is tried.

No product code needs to change unless a new door leaks.

commented

A private repository is absent from every public door, now including the doors that list repositories and issues, and MCP. Only test/private-sweep.test.ts changes. The wider sweep found no new leak, so no product code changes.

Round-one review

  1. Listing doors added: /api/v1/repos, /api/v1/users/alice and /api/v1/users/alice/repos. The two repo lists are roleAware, and the owner sees the private repo on both. /api/v1/users/acme/repos is swept too.
  2. /mcp is swept: A Door now has an optional post (a JSON body) and a bearer flag, because /mcp takes a token and never a cookie. The doors are tools/list (with and without a token) and explore, search and issues called through tools/call. search is roleAware.
  3. More doors:

- Issues: /api/v1/repos/alice/open/issues and the private repo's own issue list, /alice/open/issues, and issue search on /api/v1/search and /search with kind=issues. - Explore: the owner: and sort: filters on the HTML, Atom and API forms. - Feeds: /api/v1/feed and the private repo's activity.atom. - Pages: /acme and the org repo's pages. - Pagination: a new test follows next_cursor on /api/v1/repos?limit=1, /api/v1/users/alice/repos?limit=1 and /api/v1/explore to the last page.

There is no profile Atom feed: the router has no /:owner.atom and only per-repo activity.atom, so that door doesn't exist to test.

  1. A member who isn't the owner: private repos have no per-person collaborators here. canRead in src/core/repos.ts lets a non-owner in only through a team of the owning org. So the test adds org acme, which owns a second private repo (zqxteamvault), and carol on its read team. The header explains this.

- Carol sees that repo on the repo lists, both searches, MCP search and its own pages, and does not see alice's repo. - A stranger and bob see neither.

Other changes to the test

  • alice is no longer a site admin. An admin can read everything, so before this the owner check could not tell ownership from admin rights.
  • The by-name control now covers any distinctive word a door is asked for, not only the repo name, and swaps each one for a word that never existed.

Checking the sweep catches leaks

  • I made both repositories public as a temporary change. The sweep then failed across the listing, search, issue, feed and org doors, including page two of /api/v1/repos reached by its cursor.
  • Explore lists nothing in this seed, so its doors only show that nothing leaks. They don't show a listed row being filtered out.

Checks run: bun scripts/gate.ts, bun x tsc --noEmit, and the 82 test files scripts/fast.ts named: 1375 pass, 0 fail.

🤖 Generated with Claude Code

Approved

Round two reads well: the sweep now covers the listing doors, MCP (with a bearer flag), issue lists, feeds, org pages and cursor-followed pages. The owner is no longer an admin, so the owner check proves ownership. A non-owner member (carol, on a read team of acme) sees the org private repo where the door is role-aware and does not see alice repo. The by-name control swaps each asked word for one that never existed. One test file, no dependency, no product change, all doors in one array. I could not run the test here because the clone lacks the file, so I judged the diff. Next, non-blocking: (1) explore lists nothing in this seed, so add a public repo to that door so a filtered row is proven; (2) add a private fork of a public repo to the forks door.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs