@vxil/realtime 0.1.0 → 0.2.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
@@ -16,13 +16,14 @@ Or zero-install in the browser — the same client is served as a self-contained
16
16
 
17
17
  ## The one rule: connect tokens, not API keys
18
18
 
19
- Your Vxil API key is a **server-side secret — never put it in a browser bundle**. A browser connects with a short-lived, scoped **connect token** instead: your backend (which holds the key) mints one via `POST /v1/realtime/tokens` and hands it to the client. `RealtimeClient` takes that minting step as a `tokenProvider` callback and re-mints automatically whenever the cached token nears expiry.
19
+ A **server** key is a secret — never put it in a browser bundle. A browser connects with a short-lived, scoped **connect token** instead, minted via `POST /v1/realtime/tokens` either by your backend (which holds the server key) or, with no backend of your own, by the browser itself using a **public `end_user_required` key plus the signed-in user's session**: that mint runs in end-user mode and the token subject is forced to the verified user. `RealtimeClient` takes the minting step as a `tokenProvider` callback and re-mints automatically whenever the cached token nears expiry.
20
20
 
21
21
  ```ts
22
22
  import { RealtimeClient } from '@vxil/realtime';
23
23
 
24
24
  const rt = new RealtimeClient({
25
- // Implemented against YOUR backend — the browser never sees the API key.
25
+ // Here: your backend's endpoint (it holds the server key). With a public
26
+ // end_user_required key + the user's session, use the @vxil/sdk glue below.
26
27
  tokenProvider: async ({ channel }) => {
27
28
  const res = await fetch('/api/realtime-token', {
28
29
  method: 'POST',
@@ -40,7 +41,7 @@ room.on('message.created', (frame) => console.log(frame.data));
40
41
  room.send({ type: 'message', text: 'hello' }); // buffered while offline, flushed on reconnect
41
42
  ```
42
43
 
43
- The `tokenProvider` result may be any one of `connect_url` (full URL), `connect_path` (as returned by the tokens endpoint), or a raw `token`; include `expires_at` to enable pre-expiry re-minting. Server-side (Node) callers can use the `vxil.realtime.tokenProvider({ user_id })` glue from `@vxil/sdk` — it returns a function matching this shape.
44
+ The `tokenProvider` result may be any one of `connect_url` (full URL), `connect_path` (as returned by the tokens endpoint), or a raw `token`; include `expires_at` to enable pre-expiry re-minting. The `vxil.realtime.tokenProvider({ user_id })` glue from `@vxil/sdk` returns a function matching this shape — on a server with a server key, or in the browser with a public `end_user_required` key and `endUserToken` set to the user's session (the subject is then the verified user, whatever `user_id` says).
44
45
 
45
46
  ## What it adds over a raw WebSocket
46
47
 
package/dist/index.d.ts CHANGED
@@ -8,10 +8,14 @@ export type TokenProviderResult = {
8
8
  /** ISO timestamp; enables pre-expiry re-mint. Absent ⇒ re-mint every attempt. */
9
9
  expires_at?: string;
10
10
  };
11
- /** Your app implements this against YOUR backend (which holds the tenant API
12
- * key and calls POST /v1/realtime/tokens). Server-side (Node) callers can use
13
- * the `vxil.realtime.tokenProvider({ user_id })` glue from @vxil/sdk — it
14
- * returns a function matching this type structurally. */
11
+ /** Returns a connect token minted by POST /v1/realtime/tokens. The
12
+ * `vxil.realtime.tokenProvider({ user_id })` glue from @vxil/sdk returns a
13
+ * function matching this type structurally, and works in two set-ups: on a
14
+ * server with a server key, or in a browser / mobile app with a PUBLIC
15
+ * `end_user_required` key plus the signed-in user's session — that mint runs
16
+ * in end-user mode and the token subject is forced to the verified user, so
17
+ * no backend of your own is needed. Your own backend endpoint works too. The
18
+ * one rule: never put a SERVER key in a browser bundle. */
15
19
  export type TokenProvider = (ctx: {
16
20
  channel: string;
17
21
  }) => Promise<TokenProviderResult>;
@@ -62,8 +66,13 @@ export interface PresenceEvent {
62
66
  type FrameListener = (frame: RealtimeFrame) => void;
63
67
  type StateListener = (state: ChannelState, detail?: {
64
68
  attempt?: number;
65
- error?: unknown;
69
+ error?: unknown; /** the server close code on a terminal close */
70
+ code?: number;
66
71
  }) => void;
72
+ /** Server close code: the end-user's auth session was revoked — terminal, the
73
+ * client never reconnects on it (a fresh subscribe() after sign-in does). The
74
+ * 24 h socket-lifetime close (4001) reconnects like any other close. */
75
+ export declare const CLOSE_SESSION_REVOKED = 4002;
67
76
  type PresenceListener = (d: PresenceEvent) => void;
68
77
  export declare class Channel {
69
78
  readonly name: string;
package/dist/index.js CHANGED
@@ -28,6 +28,10 @@
28
28
  // pass a constructor — `import WebSocket from 'ws'; new RealtimeClient({
29
29
  // ..., WebSocket })` — instead of failing with a silent `WebSocket is not
30
30
  // defined`. (The `ws` package is NOT a dependency of this package.)
31
+ /** Server close code: the end-user's auth session was revoked — terminal, the
32
+ * client never reconnects on it (a fresh subscribe() after sign-in does). The
33
+ * 24 h socket-lifetime close (4001) reconnects like any other close. */
34
+ export const CLOSE_SESSION_REVOKED = 4002;
31
35
  const DEFAULT_BASE = 'https://api.vxil.com';
32
36
  function resolveCtor(injected) {
33
37
  const WS = injected ?? globalThis.WebSocket;
@@ -293,7 +297,17 @@ export class Channel {
293
297
  clearTimeout(this.flushFallback);
294
298
  this.flushFallback = undefined;
295
299
  }
296
- this.scheduleReconnect(new Error(`websocket closed (code ${ev.code ?? 'unknown'})`));
300
+ const error = new Error(`websocket closed (code ${ev.code ?? 'unknown'})`);
301
+ if (ev.code === CLOSE_SESSION_REVOKED) {
302
+ // The end-user's auth session was revoked server-side (sign-out,
303
+ // revoke-all, erase). Reconnecting would loop forever against a mint
304
+ // that now 401s — terminal instead; subscribe() again after sign-in.
305
+ this.userClosed = true;
306
+ this.settleOpen(error);
307
+ this.setState('closed', { code: ev.code, error });
308
+ return;
309
+ }
310
+ this.scheduleReconnect(error);
297
311
  });
298
312
  // In browsers every 'error' is followed by 'close' — counting both would
299
313
  // double-step the backoff, so only 'close' drives the state machine.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/realtime",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,