From 75d3d53db0809ab938a1e274c44ccebf1ec9e85e Mon Sep 17 00:00:00 2001 From: Reynaldi Chernando Date: Thu, 1 Oct 2026 00:10:17 +0700 Subject: [PATCH] refine --- src/docs/src/Teams.md | 5 +- src/docs/src/Teams/listDirectory.md | 3 +- src/docs/src/Teams/listMembers.md | 102 ----------- src/docs/src/examples.js | 6 - .../examples/teams-list-members.html | 33 ---- src/docs/src/recipes/add-team-support.md | 113 ++++++++++++ src/docs/src/recipes/teams-read-membership.md | 170 ------------------ src/docs/src/sidebar.js | 8 - 8 files changed, 116 insertions(+), 324 deletions(-) delete mode 100644 src/docs/src/Teams/listMembers.md delete mode 100644 src/docs/src/playground/examples/teams-list-members.html create mode 100644 src/docs/src/recipes/add-team-support.md delete mode 100644 src/docs/src/recipes/teams-read-membership.md diff --git a/src/docs/src/Teams.md b/src/docs/src/Teams.md index 878453ca7..8e06af99e 100644 --- a/src/docs/src/Teams.md +++ b/src/docs/src/Teams.md @@ -14,7 +14,7 @@ A team is a Puter account that pays for other accounts. Members are ordinary Put **Team context.** `list()` tells you whether the signed-in user belongs to a team, and is also how you detect whether the deployment has Teams at all: it rejects with `not_found` where the feature is off, and resolves to an empty array where it is on and the user has no team. Called from an app, it only includes teams whose owner opened the directory to apps (see below); others are simply omitted. -**Member and colleague lookup.** `listDirectory()` returns the team's active members so your app can suggest people by name instead of asking users to type usernames. `listMembers()` returns the same roster; called with the user's own session or API token it also carries `orgOwned` and `createdAt`, but an app only ever gets `username`. Both are opt-in per team: until an owner opens the directory to apps, an app gets `team_not_found`, which is indistinguishable from having no team. +**Member and colleague lookup.** `listDirectory()` returns the team's active members so your app can suggest people by name instead of asking users to type usernames. It is opt-in per team: until an owner opens the directory to apps, an app gets `team_not_found`, which is indistinguishable from having no team. **Sharing with a team.** Anything shared with a team reaches every member with one grant, including anyone added later. Pass the team's `uid` as the recipient of [`puter.fs.share()`](/FS/share/); there is no string form, since a bare string is always read as an email or username. @@ -23,10 +23,9 @@ A team is a Puter account that pays for other accounts. Members are ordinary Put ## Functions - **[`puter.teams.list()`](/Teams/list/)** - List the teams the signed-in user belongs to, and detect whether Teams is available at all -- **[`puter.teams.listMembers()`](/Teams/listMembers/)** - List a team's accounts, with owner-only seat details - **[`puter.teams.listDirectory()`](/Teams/listDirectory/)** - List a team's members, where the owner has opened the directory to apps -All three are keyset-paginated and take the same options; see each method page for the paging forms and the full error list. +Both are keyset-paginated and take the same options; see each method page for the paging forms and the full error list. ## Examples diff --git a/src/docs/src/Teams/listDirectory.md b/src/docs/src/Teams/listDirectory.md index e34df1ea0..6888ceb3c 100644 --- a/src/docs/src/Teams/listDirectory.md +++ b/src/docs/src/Teams/listDirectory.md @@ -10,8 +10,7 @@ Returns the team's member directory — the colleagues of the user your app is running for, active accounts only. It is consent-gated: the team's owner has to open the directory to apps, and until they do it rejects with `team_not_found` for every caller, indistinguishable from the team not existing. The same -consent decides whether an app sees the team in [`list()`](/Teams/list/) and -may call [`listMembers()`](/Teams/listMembers/). +consent decides whether an app sees the team in [`list()`](/Teams/list/). The membership is always the signed-in user's, never the app's: an app can only see the directory of a team its user belongs to. diff --git a/src/docs/src/Teams/listMembers.md b/src/docs/src/Teams/listMembers.md deleted file mode 100644 index d6c751f02..000000000 --- a/src/docs/src/Teams/listMembers.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: puter.teams.listMembers() -description: List a team's accounts. -platforms: [websites, apps, nodejs, workers] ---- - -
The Teams API is in beta. Method shapes, limits, and behavior may change between releases.
- -Returns a team's accounts, owner included, whether provisioned by the team or -joined from an existing account. - -Called from an app, it needs the same consent as -[`listDirectory()`](/Teams/listDirectory/): until the team's owner opens the -directory to apps it rejects with `team_not_found`, indistinguishable from the -team not existing. An app also sees only active accounts: suspended seats and -seats that have not activated yet are left out, and each `TeamMember` carries -only `username` — no `orgOwned`, `createdAt`, or `uuid`. Called with the -user's own session or API token, it lists every account whatever the -directory setting, with the full `TeamMember` shape. - -The membership is always the signed-in user's, never the app's: an app can -only see the members of a team its user belongs to. - -## Syntax - -```js -puter.teams.listMembers(uid) -puter.teams.listMembers(uid, options) -``` - -## Parameters - -#### `uid` (String) (required) - -The team's `uid`, from [`list()`](/Teams/list/). - -#### `options` (Object) (optional) - -The standard list options. All four are optional, and they decide the shape of what resolves: - -| Call | Resolves to | -| -- | -- | -| No options | The whole set as an array, fetched page by page under the hood | -| `{ limit }` | An array, capped at one page | -| `{ cursor }` or `{ includeTotal: true }` | One `{ items, cursor? }` page. `cursor` is absent on the last page | -| `{ stream: true }` | An async iterator of `{ items, cursor? }` pages | - -This route is keyset-paginated, so `offset` is not accepted — passing it throws `invalid_request`. Pass `cursor` to resume from a position. - -## Return value - -A `Promise` that resolves to an array of -`TeamMember` objects, or to a -`{ items, cursor? }` page when a pagination option is given. With -`stream: true` it returns an async iterator of pages instead. - -#### `TeamMember` - -| Field | Type | Description | -| -- | -- | -- | -| `username` | `string` | The member's Puter username. | -| `orgOwned` | `boolean` (optional) | Whether the team provisioned and pays for this account, as opposed to a pre-existing account that joined it. Absent when an app calls: it gets `username` only. | -| `createdAt` | `string` (optional) | When the account joined the team. Absent when an app calls. | -| `uuid` | `string` (optional) | The account's stable identifier. Present only when the team owner calls with their own session or API token. | - -## Errors - -A rejection carries an `Error` with a stable `code`: - -| Code | Meaning | -| -- | -- | -| `invalid_request` | Refused before reaching the server — a blank `uid`, or an `offset` on a keyset list. | -| `token_missing` | No authentication token was presented. | -| `token_auth_failed` | The token presented did not authenticate. | -| `forbidden` | Called with a scoped access token. | -| `account_is_not_verified` | The caller's email has not been confirmed. | -| `not_found` | Teams are turned off on this deployment. | -| `team_not_found` | No such team, the caller is not a member of it, or — for an app — the owner has not opened the directory to apps. | -| `too_many_requests` | The rate limit was exceeded. See [Rate Limits & Quotas](/rate-limits-and-quotas/). | - -## Examples - -List a team's accounts - -```html - - - - - - -``` diff --git a/src/docs/src/examples.js b/src/docs/src/examples.js index 27e39fe0b..1ff2e27ec 100644 --- a/src/docs/src/examples.js +++ b/src/docs/src/examples.js @@ -962,12 +962,6 @@ const examples = [ slug: 'teams-list', source: '/playground/examples/teams-list.html', }, - { - title: 'List team members', - description: 'List the accounts belonging to a team with Puter.js teams API. Run and modify this example instantly in your browser.', - slug: 'teams-list-members', - source: '/playground/examples/teams-list-members.html', - }, { title: 'Offer colleagues in a picker', description: 'Build a colleague picker from a team directory with Puter.js teams API. Run and experiment with this example in the playground.', diff --git a/src/docs/src/playground/examples/teams-list-members.html b/src/docs/src/playground/examples/teams-list-members.html deleted file mode 100644 index 16b74f18a..000000000 --- a/src/docs/src/playground/examples/teams-list-members.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - diff --git a/src/docs/src/recipes/add-team-support.md b/src/docs/src/recipes/add-team-support.md new file mode 100644 index 000000000..95bbad257 --- /dev/null +++ b/src/docs/src/recipes/add-team-support.md @@ -0,0 +1,113 @@ +--- +title: Add Team Support +description: "Learn how to add team support with Puter.js, so your app can get the user's team and its members." +tags: [teams, auth] +order: 60 +--- + +
The Teams API is in beta. Method shapes, limits, and behavior may change between releases.
+ +On Puter, a user can create a team and add accounts to it. The user who creates +the team becomes its owner. When someone on a +team signs in to your app, you can read which team they belong to and who else +is on it. This lets your app greet them with their team's name and suggest +colleagues in places like an assignee dropdown or a share dialog, instead of +asking for a typed username. + +## Check whether the user is on a team + +To get the teams the signed-in user belongs to, call +[`list()`](/Teams/list/). An empty array means the user is not on a team: + +```js +const teams = await puter.teams.list(); + +if (teams.length === 0) { + console.log('Not on a team'); +} else { + console.log(`On ${teams.length} team(s)`); +} +``` + +## Show which team + +Each team has a `name` and a `handle`, and either one may be `null`: + +```js +const [team] = await puter.teams.list(); + +console.log(team.name ?? team.handle ?? 'Unnamed team'); +console.log(team.createdAt); // '2026-09-28T10:04:00Z' +console.log(team.isOwner); // true if this user owns the team +``` + +To refer to a team, use `team.uid`. The +[`listDirectory()`](/Teams/listDirectory/) method takes it, and it is the value +to store. The owner can rename the `handle`, and a deleted team's +handle becomes free for another team to take, so only `uid` always points to +the same team. + +## List the users in a team + +To get the active users in a team, call +[`listDirectory()`](/Teams/listDirectory/) with the team's `uid`. Each entry has a `username` and a `uuid`. The `uuid` +stays the same when the user changes their username, so store the `uuid` if +you save a reference to someone: + +```js +const entries = await puter.teams.listDirectory(team.uid); + +for (const { username, uuid } of entries) { + console.log(username, uuid); +} +``` + +With this list, your app can suggest team members wherever the user picks +people. For example, when the user [shares a file](/recipes/share-a-file/), +your app can suggest usernames from their team, so the user does not have to +type a username from memory. + +## Check that the team has turned on its directory + +Your app can see a team only after the team owner turns on its directory. It is +off by default. Until the owner turns it on, [`list()`](/Teams/list/) leaves +the team out of its result, and +[`listDirectory()`](/Teams/listDirectory/) rejects with `team_not_found`. The +same calls made from the user's own Puter session still return the team. + +Each team has a `directoryEnabled` flag. Check it before listing the users in a team, so +you can tell the user what to do: + +```js +const [team] = await puter.teams.list(); + +if (team.directoryEnabled) { + const entries = await puter.teams.listDirectory(team.uid); + console.log(entries); +} else { + console.log('Ask the team owner to turn on the directory.'); +} +``` + +## List a large team + +By default, [`listDirectory()`](/Teams/listDirectory/) returns the whole list +as an array. For a large team, pass `stream: true` to read it one page at a +time. Each page has an `items` array: + +```js +for await (const page of puter.teams.listDirectory(team.uid, { stream: true })) { + for (const { username } of page.items) console.log(username); +} +``` + +To load one page at a time yourself, such as behind a "Load more" button, pass +`limit` and `cursor`. The result has a `cursor` for the next page while more +pages remain: + +```js +const page = await puter.teams.listDirectory(team.uid, { limit: 50, cursor: null }); + +console.log(page.items); +console.log(page.cursor); +``` diff --git a/src/docs/src/recipes/teams-read-membership.md b/src/docs/src/recipes/teams-read-membership.md deleted file mode 100644 index aedb25a54..000000000 --- a/src/docs/src/recipes/teams-read-membership.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: Read the User's Team -description: "Find out whether the signed-in user belongs to a team, show which one, and list their colleagues so your app can suggest them." -tags: [teams, auth] -order: 60 ---- - -**Use this when** your app should adapt to the team the signed-in user belongs -to rather than treating everyone as a lone user. A few things this unlocks: - -- A **task board** that offers colleagues in an assignee dropdown instead of - asking for a typed username. -- A **document editor** whose share dialog suggests the people you actually work - with. -- A **dashboard** that greets someone with their team's name and keeps each - team's data under its own key. -- An **onboarding screen** that can tell "you have no team yet" apart from "this - Puter has no teams at all" — and say something useful either way. - -Everything here is read-only. Creating a team, adding accounts to it and paying -for them are done by the team owner from their own Puter account, never from an -app, so `puter.teams` gives an app three calls: [`list()`](/Teams/list/), -[`listMembers()`](/Teams/listMembers/) and -[`listDirectory()`](/Teams/listDirectory/). - -## Check whether the user is on a team - -`list()` returns the teams the signed-in user belongs to: - -```js -const teams = await puter.teams.list(); - -if (teams.length === 0) { - console.log('Not on a team'); -} else { - console.log(`On ${teams.length} team(s)`); -} -``` - -An empty array means *this user has no team*. It does not mean the deployment -has no teams: where the feature is turned off there is no `/teams` route at -all, so the call rejects with `not_found` instead of resolving. Handle the two -separately, or a user on a Puter without teams looks identical to one who -simply has not joined anything: - -```js -let teams = []; -try { - teams = await puter.teams.list(); -} catch (e) { - if (e.code !== 'not_found') throw e; - // Teams aren't available here — hide the team parts of your UI. -} -``` - -Failures reject with an error carrying a `code`, so `e.code` is what you branch -on. - -## Show which team - -A team carries a `name` and a `handle`, and either may be `null`: - -```js -const [team] = await puter.teams.list(); - -console.log(team.name ?? team.handle ?? 'Unnamed team'); -console.log(team.createdAt); // '2026-09-28T10:04:00Z' -console.log(team.isOwner); // true if this user owns the team -``` - -Pass `team.uid` to the other two methods, and store that if you store anything. -Never key on `handle`: it is a label the owner can rename, and deleting a team -releases it for someone else to take, so a saved handle can later resolve to a -different team. `uid` is the stable reference. - -## List the user's colleagues - -`listMembers()` takes the team's `uid`. Any member may call it: - -```js -const [team] = await puter.teams.list(); -const members = await puter.teams.listMembers(team.uid); - -for (const member of members) { - console.log(member.username); -} -``` - -From an app each entry carries `username` and nothing else — no email, no -activation state, no usage. The extra fields you will see in the type -(`orgOwned`, `createdAt`, `uuid`) are filled in only when the user's own Puter -session makes the call, so write your UI against `username` alone. - -## Offer colleagues in a picker - -`listDirectory()` is the one built for suggesting people. It returns each -colleague's `username` together with a `uuid` that survives a rename, which is -what you want to store against a share or an assignment: - -```js -const entries = await puter.teams.listDirectory(team.uid); - -for (const { username, uuid } of entries) { - addOption(username, uuid); -} -``` - -Accounts that are suspended, or that never signed in for the first time, are -left out, so the list is people your user can actually reach. - -## The team has to opt in - -An app sees a team only once that team's owner has turned its directory on. -This is off by default, and it is the single most common reason these calls -come back empty from an app but full from the user's own Puter session. - -The behaviour differs per call, which is worth knowing when you are debugging: - -- `list()` **omits** teams that have not opted in. It does not throw — a user - on one closed team gets `[]`. -- `listMembers()` and `listDirectory()` **reject** with `team_not_found`. - -`directoryEnabled` on the team tells you which case you are in, so you can say -something useful instead of showing an empty list: - -```js -const [team] = await puter.teams.list(); - -if (!team.directoryEnabled) { - console.log('Ask the team owner to turn on the directory.'); -} else { - const entries = await puter.teams.listDirectory(team.uid); -} -``` - -## Longer lists - -All three methods share the same three forms. By default you get the whole list -as an array, which is what every example above uses: - -```js -const members = await puter.teams.listMembers(team.uid); -``` - -Passing `cursor` or `includeTotal` switches to a page envelope instead: - -```js -const page = await puter.teams.listMembers(team.uid, { limit: 50, cursor: null }); - -page.items; // this page's members -page.cursor; // present only while more pages remain -``` - -And `stream: true` hands back an async iterator of those envelopes, for walking -a large team without holding it all at once: - -```js -for await (const page of puter.teams.listMembers(team.uid, { stream: true })) { - for (const member of page.items) console.log(member.username); -} -``` - -## Notes - -- These routes are keyset-paginated, so `offset` is rejected rather than - silently ignored. Pass `cursor` to resume from a position. -- Everything is scoped to the signed-in person's own membership, so an app - installed by a member of one team can never read another team. -- A user may belong to more than one team. `list()[0]` is fine for a demo, but - let the user choose if your app acts on a specific one. diff --git a/src/docs/src/sidebar.js b/src/docs/src/sidebar.js index d16cb372c..2e63b6bdf 100755 --- a/src/docs/src/sidebar.js +++ b/src/docs/src/sidebar.js @@ -1199,14 +1199,6 @@ let sidebar = [ source: '/Teams/list.md', path: '/Teams/list', }, - { - title: 'listMembers()', - page_title: 'puter.teams.listMembers()', - title_tag: 'puter.teams.listMembers()', - icon: '/assets/img/function.svg', - source: '/Teams/listMembers.md', - path: '/Teams/listMembers', - }, { title: 'listDirectory()', page_title: 'puter.teams.listDirectory()',