feat: guest turn for peer (#3640)

This commit is contained in:
Daniel Salazar
2026-08-25 07:51:02 -07:00
committed by GitHub
parent 6f1548e26a
commit 3c866c645b
14 changed files with 1736 additions and 28 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ Use the Peer API to build peer-to-peer applications without the need for a serve
<div class="info">
Peer connections require authentication. On websites, Puter.js will prompt the user to authenticate if needed.
Hosting a session requires authentication — on websites, Puter.js will prompt the user if needed. Guests can join without an account: pass `anonToken`, plus a `turnGrant` from the host so the connection can still use Puter's relays. See [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/).
</div>
+3 -1
View File
@@ -9,7 +9,7 @@ Connects to a peer server and returns a [`PuterPeerConnection`](/Objects/puterpe
<div class="info">
On websites, Puter.js may prompt the user to authenticate before connecting.
On websites, Puter.js may prompt the user to authenticate before connecting. To let someone join without an account, pass `anonToken` — and a `turnGrant` from the host, so the connection can still use Puter's relays. See [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/).
</div>
@@ -32,6 +32,8 @@ A string invite code created by `puter.peer.serve()`.
- `iceServers` (`RTCIceServer[]`) Custom ICE servers (STUN/TURN) to use instead of the Puter-managed relays.
- `forceRelay` (`boolean`) Whether to force connections to route through a relay instead of attempting peer-to-peer (default). Metering charges may apply.
- `anonToken` (`String`) Join without a Puter session. Any uuid — it identifies this guest for the duration of the session, and no sign-in prompt is shown. The host sees the guest as `anonymous`, so anything you want to call them is yours to send over the connection.
- `turnGrant` (`String`) A grant from [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/). Lets a guest use the Puter-managed relays on the host's account. Without one, a guest connects only where a direct connection is possible; with `forceRelay`, a guest needs one.
## Return value
+94
View File
@@ -0,0 +1,94 @@
---
title: puter.peer.createGuestGrant()
description: Let guests without a Puter account use Puter's TURN relays on your account.
platforms: [websites, apps]
---
Creates a **guest grant**: a short-lived token that lets people without a Puter session use the Puter-managed TURN relays. Hand it to the people you invite alongside the invite code, and they pass it to [`puter.peer.connect()`](/Peer/connect/) as `turnGrant`.
Without a grant, a guest can still join a session — but only over direct connections. Relay credentials are what make a connection work when one side is behind a NAT or firewall that blocks direct traffic, and minting them requires an account. The grant is how your account vouches for the guest.
<div class="info">
Relay traffic a guest sends is metered against **your** account, at the same rate as your own. Anyone holding the grant can mint credentials until it expires, so share it with the session you meant to host, and let it expire rather than reusing one indefinitely.
</div>
## Syntax
```js
const { grant, expiresAt } = await puter.peer.createGuestGrant();
```
## Parameters
None.
## Return value
A `Promise` that resolves to an object with:
- `grant` (`String`) The grant to give your guests.
- `expiresAt` (`Number`) When the grant stops being accepted, in seconds since the epoch. Past this point, redeeming it fails with `peer_grant_expired` and you issue a new one.
Rejects if the caller isn't authenticated, or if the deployment doesn't offer guest relay access.
## Example
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>Host a session guests can join</h3>
<button id="host">Start hosting</button>
<pre id="out" style="background:#f4f4f4; padding:10px;"></pre>
<script>
const out = document.getElementById('out');
document.getElementById('host').addEventListener('click', async () => {
// Hosting requires a Puter account; joining will not.
const server = await puter.peer.serve();
const { grant, expiresAt } = await puter.peer.createGuestGrant();
// Everything a guest needs, in one link.
const link = new URL(location.href);
link.hash = new URLSearchParams({
code: server.inviteCode,
grant,
}).toString();
out.textContent =
`Invite link:\n${link}\n\n` +
`Good until ${new Date(expiresAt * 1000).toLocaleTimeString()}`;
server.addEventListener('connection', (event) => {
out.textContent += `\n${event.user?.username ?? 'someone'} joined`;
event.conn.addEventListener('message', (e) => {
out.textContent += `\nmessage: ${e.data}`;
});
});
});
// The guest side of the same page: join with the code and grant from
// the link, no sign-in prompt.
(async () => {
const params = new URLSearchParams(location.hash.slice(1));
const code = params.get('code');
const grant = params.get('grant');
if ( !code ) return;
const conn = await puter.peer.connect(code, {
anonToken: crypto.randomUUID(),
turnGrant: grant,
});
conn.addEventListener('open', () => {
out.textContent += '\nJoined as a guest';
conn.send('hello from a guest');
});
})();
</script>
</body>
</html>
```
+9
View File
@@ -11,8 +11,17 @@ Fetches TURN relay credentials ahead of time so that peer connections can start
```js
await puter.peer.ensureTurnRelays();
await puter.peer.ensureTurnRelays(options);
```
## Parameters
#### `options` (optional)
`options` is an object with the following properties:
- `turnGrant` (`String`) A grant from [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/), to preload relays as a guest with no Puter session. Credentials are minted against the account that issued the grant.
## Return value
A `Promise` that resolves when relay details are cached. If relays cannot be loaded, Puter.js will fall back to default ICE servers when connecting.
+3
View File
@@ -28,6 +28,9 @@ const server = await puter.peer.serve(options);
- `iceServers` (`RTCIceServer[]`) Custom ICE servers (STUN/TURN) to use instead of the Puter-managed relays.
- `forceRelay` (`boolean`) Whether to force connections to route through a relay instead of attempting peer-to-peer (default). Metering charges will increase.
- `anonToken` (`String`) Host without a Puter session. Any uuid; no sign-in prompt is shown. An anonymous host has no account to attribute relay usage to, so it cannot issue guest grants and gets no relays of its own.
To let people join your session without accounts of their own, keep hosting authenticated and give them a grant — see [`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/).
## Return value
+11
View File
@@ -136,6 +136,17 @@ Recipients are emailed by default and opt out with the unsubscribe link the mail
Over these, **the share still succeeds** — only the announcement is dropped. The recipient's notification is kept up to date either way, and folds several senders into one ("alice and bob shared 5 items with you"), so nothing is lost; it just doesn't interrupt them again. Emails are additionally batched: everything triggered for one recipient within a 90-second window goes as a single digest message. Recipients can also refuse shares outright — from one sender, or from everyone — which fails that sender's `share` call with `recipient_not_accepting_shares`. Both are managed from **Settings → Security → Blocked people**.
### Peer connections
| Limit | Paid | Free | Anonymous |
| --- | --- | --- | --- |
| Relay credentials per minute | 30 | 10 | 5 |
| Guest grants issued per minute | 30 | 10 | 5 |
Signalling details are public deployment config and bounded per network instead of per account, at 3,000 reads/min.
Guests are bounded per *host*: everyone holding grants from the same account shares **60 relay-credential requests/min**. Relay traffic a guest sends is metered against the account that issued the grant, so treat a grant as something that spends your allowance — issue it for the session you meant to host, and let it expire rather than reusing one indefinitely.
### Everything at once
Every driver call also passes one shared per-account budget of **8,000 calls/min** before the per-API limits above. It exists to catch a runaway loop, not to shape normal traffic — a client that sees a 429 from it is looping.
+8
View File
@@ -687,6 +687,14 @@ let sidebar = [
source: '/Peer/connect.md',
path: '/Peer/connect',
},
{
title: '<code>createGuestGrant()</code>',
page_title: '<code>puter.peer.createGuestGrant()</code>',
title_tag: 'puter.peer.createGuestGrant()',
icon: '/assets/img/function.svg',
source: '/Peer/createGuestGrant.md',
path: '/Peer/createGuestGrant',
},
{
title: '<code>ensureTurnRelays()</code>',
page_title: '<code>puter.peer.ensureTurnRelays()</code>',