@aurabase/realtime 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ npm i @aurabase/realtime
23
23
  ```ts
24
24
  import { createClient } from '@aurabase/aurabase-js'
25
25
 
26
- const aura = createClient('http://localhost:8080', 'my-project', 'aura_anon_xxx')
26
+ const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx')
27
27
  ```
28
28
 
29
29
  ### Connexion
@@ -162,21 +162,108 @@ room
162
162
  })
163
163
  .subscribe()
164
164
 
165
- // 'ok' | 'timeout' | 'error' — résout à l'ACK serveur si broadcast.ack est activé sur le channel.
165
+ // Résout à l'ACK serveur si broadcast.ack est activé ET le canal abonné (voir ci-dessous).
166
166
  const status = await room.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100, y: 200 } })
167
167
  ```
168
168
 
169
+ ##### Statuts rendus par `send()`
170
+
171
+ | Statut | Signification | Le message est-il parti ? |
172
+ | --- | --- | --- |
173
+ | `'ok'` | Sans `ack` : remis au transport. Avec `ack` : le serveur a répondu `broadcast_ack`. | oui |
174
+ | `'timeout'` | `ack` demandé sur un canal abonné, aucun accusé reçu en 5 s : l'accusé s'est **perdu** (lien coupé, serveur saturé). | oui |
175
+ | `'not_connected'` | Aucun transport lié, ou socket non-OPEN — le message a été **jeté**. | **non** |
176
+ | `'requires_subscription'` | Le serveur n'a **pas d'abonnement** pour ce canal : jamais souscrit, souscription **refusée** (RLS, `private_only`, quota) ou **révoquée** depuis (voir ci-dessous). | **non** |
177
+ | `'error'` | L'envoi a levé (sérialisation, transport en erreur). | non |
178
+
179
+ Avant la remédiation C18/N24, ces trois derniers cas rendaient tous `'error'` (et le
180
+ message jeté sur une socket fermée rendait même `'ok'`) : l'appelant ne pouvait ni les
181
+ distinguer, ni savoir si son message était parti.
182
+
183
+ ##### Toute écriture exige un canal ABONNÉ
184
+
185
+ `.subscribe()` d'abord, `.send()` / `.track()` ensuite. Le serveur refuse un broadcast émis
186
+ sur un canal auquel la connexion n'est pas abonnée
187
+ (`Non abonné au canal '…' — Subscribe requis avant Broadcast`), et le SDK ne l'émet donc
188
+ même pas :
189
+
190
+ ```ts
191
+ const room = aura.realtime.channel('room-1', { broadcast: { ack: true } })
192
+
193
+ await room.send({ type: 'broadcast', event: 'x', payload: {} })
194
+ // → 'requires_subscription' (immédiat) : rien n'est parti.
195
+
196
+ room.subscribe()
197
+ await room.send({ type: 'broadcast', event: 'x', payload: {} }) // → 'ok'
198
+ ```
199
+
200
+ Le même statut est rendu quand le SERVEUR **refuse** l'abonnement (canal privé sans droit
201
+ RLS, projet `private_only`, quota de canaux) ou le **révoque** ensuite (ré-évaluation RLS au
202
+ refresh de token) : le canal a alors reçu `CHANNEL_ERROR`, et toute écriture ultérieure rend
203
+ `'requires_subscription'` immédiatement — elle n'attend pas les 5 s d'un accusé que le
204
+ serveur n'émettra jamais. Écrivez donc depuis le callback de statut, comme chez Supabase :
205
+
206
+ ```ts
207
+ room.subscribe(async (statut) => {
208
+ if (statut !== 'SUBSCRIBED') return // CHANNEL_ERROR : le serveur a refusé ce canal
209
+ await room.send({ type: 'broadcast', event: 'x', payload: {} }) // → 'ok'
210
+ })
211
+ ```
212
+
213
+ Ce n'est pas une limitation arbitraire, c'est la condition d'un contrôle d'accès qui tienne :
214
+ les policies RLS d'un canal privé sont calculées **à l'abonnement** et mises en cache dans la
215
+ subscription de la connexion. Sans abonnement, le serveur n'a aucune policy à consulter — et
216
+ laisser passer reviendrait à n'appliquer le contrôle qu'aux clients qui s'abonnent (c'était
217
+ le défaut N28 : un client jamais abonné diffusait sur un canal privé). C'est aussi le modèle
218
+ de `@supabase/realtime-js` : phoenix exige le `join` avant tout `push`, et `send()` bascule
219
+ sur l'endpoint HTTP quand le canal n'est pas rejoint. Le pendant Aurabase de cet endpoint est
220
+ `httpBroadcast()`, ci-dessous — lui aussi soumis à la RLS sur les projets `private_only`.
221
+
222
+ ##### `private_only` — l'autorité sur la privauté est au SERVEUR
223
+
224
+ `{ private: true }` est une DEMANDE du client : elle déclenche le contrôle RLS pour *cette*
225
+ connexion. Elle ne rend pas le canal privé pour les autres — un client qui s'abonne au même
226
+ canal sans `private` reste, par défaut, un abonné public. Pour que la privauté soit imposée
227
+ à tous, activez le réglage de projet (table `realtime_config` du schéma `_platform`, hors de
228
+ portée des rôles `anon`/`authenticated`) :
229
+
230
+ ```sql
231
+ INSERT INTO "project_<id>_platform".realtime_config (key, value)
232
+ VALUES ('private_only', 'true')
233
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value;
234
+ ```
235
+
236
+ Tout abonnement public est alors refusé (`Canal public refusé : ce projet est configuré en
237
+ private_only`), et les chemins REST (`httpBroadcast`, roster de présence) ainsi que le flux
238
+ SSE passent par la même policy RLS. Même modèle que le réglage `private_only` de Supabase
239
+ Realtime (`PrivateOnly`).
240
+
169
241
  #### Broadcast HTTP (sans WebSocket)
170
242
 
171
243
  Émet un broadcast via REST, sans connexion WebSocket ouverte — utile côté serveur/edge.
172
244
 
173
245
  ```ts
174
- // POST /v1/realtime/{projectId}/broadcast — pas besoin de connect().
246
+ // POST /v1/realtime/broadcast — pas besoin de connect().
175
247
  const { error } = await aura.realtime.httpBroadcast('room-1', 'cursor-pos', { x: 10, y: 20 })
176
248
  if (error) console.error(error) // 4xx / rate-limit surfacés proprement
177
249
  ```
178
250
 
179
- Le nom de canal est préfixé par le projet automatiquement s'il ne l'est pas déjà.
251
+ **Le canal doit porter le projet.** Le serveur exige `channel` préfixé par
252
+ `"<project_id>:"` et répond sinon `403 « Le canal n'appartient pas à ce projet »`
253
+ (`handlers/broadcast.rs`).
254
+
255
+ - **Avec une session** : le préfixe est ajouté ici, lu dans le claim `project_id` du jeton.
256
+ Un canal déjà qualifié n'est pas préfixé deux fois.
257
+ - **Sans session — le cas serveur/edge** : le SDK n'a aucun identifiant de projet (l'URL est
258
+ la seule désignation du tenant côté client), et il ne peut donc rien préfixer.
259
+ `httpBroadcast` **refuse** alors localement, sans appel réseau
260
+ (`error.code === 'realtime_channel_non_qualifie'`), plutôt que d'aller chercher un `403`
261
+ dont le message ne nomme pas la forme attendue. Qualifiez le canal vous-même :
262
+
263
+ ```ts
264
+ // Serveur / edge : clé service_role, aucune session utilisateur.
265
+ await aura.realtime.httpBroadcast(`${PROJECT_ID}:room-1`, 'cursor-pos', { x: 10, y: 20 })
266
+ ```
180
267
 
181
268
  ### Canaux privés (`setAuth`)
182
269