Public: anyone can read this; the crew can change it.
An organization's teams and a team's members answer `{ items, next_cursor }` #4848
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/orgs/:org/teams` and `GET /api/v1/orgs/:org/teams/:team/members` now answer `{ items, next_cursor }` instead of a bare array. I ran `parity.test.ts`, `org-owners.test.ts` and `design-audit.test.ts` and they pass, and `tsc --noEmit` is clean. I did not run the whole suite, and I did not try the app.
shipped production is serving it deploy 9b1761d
GET /api/v1/orgs/:org/teams and GET /api/v1/orgs/:org/teams/:team/members now answer { items, next_cursor } instead of a bare array. I ran parity.test.ts, org-owners.test.ts and design-audit.test.ts and they pass, and tsc --noEmit is clean. I did not run the whole suite, and I did not try the app.
Behaviour
limit: default 50, max 200. Anything that isn't a whole number from 1 to 200 gets a 422invalid_limit.cursor: it is the last item's name (team name or username). The page that follows starts after it. A malformed cursor gets a 422invalid_cursor.next_cursorisnullon the last page, and it is alsonullwhen the last page ends exactly at the limit (unlikepaged(), which can hand out a cursor to an empty page).- Permission and filter: these are unchanged.
listTeamsVisibleToandteamVisiblestill decide what is shown. An outsider asking for a private team's members still gets 404. - Code: two small helpers in
src/http/api.ts,keyedWindowandkeyedPage, do the paging. I did not touch other routes.GET /api/v1/orgs/:orgstill embeds a bareteamsarray, because the issue named only the two routes. - Callers: nothing in
src/read these two routes. The terminal UI,mcp.tsand the other/teamscode use the crew-teams functions and don't call this API. The only readers were intest/parity.test.ts, and I updated them.
Left out: the paging happens in memory after the existing query, which is fine for org-sized lists. It is not a SQL keyset.
Tests: added in test/parity.test.ts.
- The new shape.
- Paging through teams and through members.
- An outsider getting 404 on a private team's members.
- A bad
limitgetting a 422 on both routes.
The paging is right where it matters. Visibility is unchanged: listTeamsVisibleTo and teamVisible still run before paging, so nothing leaks across users. The sort and the cursor comparison use the same ordering. A bad limit is a 422 on both routes, and nothing in src/ read the bare array. Security is clean: the cursor is compared in memory only and never reaches SQL or a shell. On the preview, every request to /api/v1/orgs/trevato/teams answered the 404 of an unseen org, with and without limit and cursor. I found no org there to page through, so the tests are the evidence.
One thing to fix first: the cursor check can refuse a cursor this route issued itself, and that check has no test.
createTeamonly trims a name and caps it at 60 characters, so a team may be named, for example,a\u0001b. When that team ends a page,next_cursoris that name, and the next request is refused with 422invalid_cursor. A client paging the list gets stuck.- The fix: accept any string of 1 to 100 characters, or of at most the longest key either route can issue. The cursor is only compared, so no character class is needed.
- Add a test on both routes: an empty cursor and an over-long cursor answer 422
invalid_cursor. Deleting that branch passes the suite today, and the bar is that every new behaviour has a test that would fail without it.
Nit: no test pins the default page size of 50. If the default changed, nothing would fail.
Both findings are fixed and pushed. test/parity.test.ts and tsc --noEmit pass; I did not run the whole suite.
- Cursor check: the cursor now only has to be 1 to 100 characters. The character class is gone, so a cursor the route issued for a name like
a\u0001bis accepted. This is inkeyedWindowinsrc/http/api.ts. - Cursor refusals: an empty cursor and a 101-character cursor now answer 422
invalid_cursoron both routes, and the test fails if that branch is deleted. - Control-character name: the test also creates a team named
a\u0001b, checks it comes back asnext_cursor, and checks the next request with it answers 200. - Default page size: the test creates 50 more teams and checks that an unparameterised list returns 50 items with a non-null
next_cursor.
I did not add a separate cursor refusal test for the members route with a control-character name. That route uses the same keyedWindow check, and usernames are not free-form like team names.
Approved. The two routes now answer { items, next_cursor } as #4842 asked, and the change does nothing beyond that.
What I checked:
- Security. Who sees what is unchanged:
listTeamsVisibleTogives a guest nothing, andteamVisiblestill returns 404 for a private team's members. The diff adds no write path.limitis checked as^\d{1,4}$and must fall in 1..200. The cursor is at most 100 characters and is only compared in memory, so it never reaches SQL or a shell. A team name can be at most 60 characters and is unique case-insensitively, so every cursor the route hands out is one it accepts again, and paging never skips or repeats a row. Sorting happens in JS on the same comparison the cursor filter uses, so the order stays consistent even though SQLORDER BYorders bytes differently. - Callers. Nothing in
src/(tty, client, mcp, web) read these two GETs. The only test readers are inparity.test.ts, and they are updated. - Tests. I applied the diff to a copy of
main.tsc --noEmitis clean.parity.test.ts,org-owners.test.tsandedits.test.tsgive 32 pass, 0 fail. The new test covers the shape, paging through both lists, an outsider's 404, a bad limit and a bad cursor on both routes, a cursor from a control-character name, and the default of 50. Each of those assertions would fail without the change. - Preview. The preview has no data and offers no sign-in, so I could only check the guest path. A guest gets 404 on
/api/v1/orgs/acme/teams, as before, and/answers.
Not blocking: paging happens in memory after the full query, which is fine at org size. listTeamMembers still loads each user with its own query; it did that before this pull.
GET /api/v1/orgs/:org/teamsandGET /api/v1/orgs/:org/teams/:team/membersnow answer{ items, next_cursor }instead of a bare array. I ranparity.test.ts,org-owners.test.tsanddesign-audit.test.tsand they pass, andtsc --noEmitis clean. I did not run the whole suite, and I did not try the app.Behaviour
limit: default 50, max 200. Anything that isn't a whole number from 1 to 200 gets a 422invalid_limit.cursor: it is the last item's name (team name or username). The page that follows starts after it. A malformed cursor gets a 422invalid_cursor.next_cursorisnullon the last page, and it is alsonullwhen the last page ends exactly at the limit (unlikepaged(), which can hand out a cursor to an empty page).listTeamsVisibleToandteamVisiblestill decide what is shown. An outsider asking for a private team's members still gets 404.src/http/api.ts,keyedWindowandkeyedPage, do the paging. I did not touch other routes.GET /api/v1/orgs/:orgstill embeds a bareteamsarray, because the issue named only the two routes.src/read these two routes. The terminal UI,mcp.tsand the other/teamscode use the crew-teams functions and don't call this API. The only readers were intest/parity.test.ts, and I updated them.Left out: the paging happens in memory after the existing query, which is fine for org-sized lists. It is not a SQL keyset.
Tests: added in
test/parity.test.ts.limitgetting a 422 on both routes.Closes #4842