@relyvolabs/web-sdk 0.0.0-stage → 0.1.0

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
@@ -1,3 +1,108 @@
1
- # Temporary Holding Version
1
+ # @relyvolabs/web-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Browser client SDK for Relyvo: realtime chat and video calls. The browser holds
4
+ only short-lived credentials minted by your backend — never an API key.
5
+
6
+ ```ts
7
+ import { RelyvoChat, RelyvoCall } from "@relyvolabs/web-sdk";
8
+
9
+ const chat = new RelyvoChat(credentials);
10
+ await chat.connect();
11
+ chat.on("message", ({ message, senderName }) => render(message, senderName));
12
+ await chat.send({ content: "hello", clientMessageId: crypto.randomUUID() });
13
+
14
+ const call = await RelyvoCall.connect({ serverUrl, token });
15
+ await call.enableCamera();
16
+ await call.enableMicrophone();
17
+ ```
18
+
19
+ Your backend creates both credential sets with
20
+ [`relyvo-server-sdk`](https://github.com/relyvolabs/relyvo-server-sdk):
21
+
22
+ | Feature | Server-side call | What the browser receives |
23
+ | --- | --- | --- |
24
+ | Video | `video.createToken(...)` | `serverUrl` + participant `token` |
25
+ | Chat | `chat.upsertUser`, `chat.createUserToken`, realtime tokens | `wsUrl`, connection `token`, `channel`, `subscriptionToken` (+ typing channel) |
26
+
27
+ ## Chat runs without REST
28
+
29
+ Chat operations are RPC frames on the Centrifugo WebSocket the client already
30
+ holds:
31
+
32
+ ```
33
+ browser ──WS──► Centrifugo ──proxy──► chat-api ──► PostgreSQL
34
+ ```
35
+
36
+ `send`, `history`, `markRead` and `channels` all travel this path, so the browser
37
+ never calls Relyvo REST and no CORS allowance is required. Identity and project
38
+ scope come from the connection token; the SDK never sends a project or user id.
39
+
40
+ ## API
41
+
42
+ ### `new RelyvoChat(credentials, options?)`
43
+
44
+ | Credential | Meaning |
45
+ | --- | --- |
46
+ | `wsUrl` | Centrifugo WebSocket URL |
47
+ | `token` | Centrifugo connection token |
48
+ | `channel` | Conversation channel (`project:<id>:channel:<id>`) |
49
+ | `subscriptionToken` | Subscription token for that channel |
50
+ | `typingChannel` / `typingSubscriptionToken` | Optional typing sibling |
51
+
52
+ Methods: `connect()`, `on(event, handler)`, `send()`, `history()`, `markRead()`,
53
+ `channels()`, `disconnect()`.
54
+
55
+ ### Token refresh
56
+
57
+ Connection and subscription tokens are short-lived. Give the SDK callbacks that
58
+ ask your backend for replacement tokens; the transport reconnects on its own.
59
+
60
+ ```ts
61
+ const chat = new RelyvoChat(credentials, {
62
+ getConnectionToken: async () => {
63
+ const response = await fetch("/api/chat/token");
64
+ return (await response.json()).token;
65
+ },
66
+ getSubscriptionToken: async (channel) => {
67
+ const response = await fetch(`/api/chat/subscription?channel=${encodeURIComponent(channel)}`);
68
+ return (await response.json()).token;
69
+ },
70
+ });
71
+ ```
72
+
73
+ The callbacks run when a token expires, never before, and they must reach a
74
+ server: a browser cannot mint these tokens.
75
+
76
+ Events: `connected`, `disconnected`, `message`, `typing`, `error`.
77
+
78
+ ### `RelyvoCall.connect({ serverUrl, token }, options?)`
79
+
80
+ Methods: `enableCamera()`, `enableMicrophone()`, `shareScreen()`, `on(event, handler)`,
81
+ `disconnect()`. `call.room` exposes the underlying LiveKit room when you need
82
+ something this wrapper does not cover.
83
+
84
+ Events: `connected`, `disconnected`, `participantConnected`,
85
+ `participantDisconnected`, `trackSubscribed`, `trackUnsubscribed`,
86
+ `connectionQualityChanged`, `mediaDevicesError`.
87
+
88
+ ## Errors
89
+
90
+ Every failure is a `RelyvoError` with a stable `code`: transport failures use
91
+ `transport_error` / `connect_failed`, and a rejected RPC carries the chat
92
+ service's own code (`forbidden`, `not_found`, `rate_limited`, …).
93
+
94
+ ## Requirements
95
+
96
+ - `centrifuge` and `livekit-client` are peer dependencies; install the versions
97
+ your app already uses so there is one copy of each.
98
+ - Video and screen sharing need a secure context (`https://` or `localhost`).
99
+
100
+ ## Status
101
+
102
+ `0.1.0`: chat (send, history, read, channels, typing) and call connect with
103
+ camera/microphone/screen control. Attachments, edits and deletes are not exposed
104
+ yet.
105
+
106
+ ## Licence
107
+
108
+ Proprietary. Not for redistribution.
package/dist/index.cjs ADDED
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // src/index.ts
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ RelyvoCall: () => RelyvoCall,
26
+ RelyvoChat: () => RelyvoChat,
27
+ RelyvoError: () => RelyvoError,
28
+ VERSION: () => VERSION,
29
+ isRelyvoError: () => isRelyvoError
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/chat.ts
34
+ var import_centrifuge = require("centrifuge");
35
+
36
+ // src/errors.ts
37
+ var RelyvoError = class extends Error {
38
+ constructor(code, message, cause) {
39
+ super(message, cause === void 0 ? void 0 : { cause });
40
+ /** Stable machine code. RPC failures carry the chat service's code. */
41
+ __publicField(this, "code");
42
+ /** Transport-level cause, when there is one. */
43
+ __publicField(this, "cause");
44
+ this.name = "RelyvoError";
45
+ this.code = code;
46
+ this.cause = cause;
47
+ }
48
+ };
49
+ function isRelyvoError(error) {
50
+ return error instanceof RelyvoError;
51
+ }
52
+
53
+ // src/chat.ts
54
+ var defaultFactory = (wsUrl, options) => new import_centrifuge.Centrifuge(wsUrl, {
55
+ token: options.token,
56
+ ...options.getToken === void 0 ? {} : { getToken: options.getToken }
57
+ });
58
+ var RelyvoChat = class {
59
+ constructor(credentials, options = {}) {
60
+ __publicField(this, "credentials");
61
+ __publicField(this, "client");
62
+ __publicField(this, "connectTimeoutMs");
63
+ __publicField(this, "getSubscriptionToken");
64
+ __publicField(this, "handlers", {
65
+ connected: /* @__PURE__ */ new Set(),
66
+ disconnected: /* @__PURE__ */ new Set(),
67
+ message: /* @__PURE__ */ new Set(),
68
+ typing: /* @__PURE__ */ new Set(),
69
+ error: /* @__PURE__ */ new Set()
70
+ });
71
+ __publicField(this, "typingSubscription");
72
+ __publicField(this, "conversationSubscription");
73
+ this.credentials = credentials;
74
+ this.connectTimeoutMs = options.connectTimeoutMs ?? 1e4;
75
+ this.getSubscriptionToken = options.getSubscriptionToken;
76
+ this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {
77
+ token: credentials.token,
78
+ ...options.getConnectionToken === void 0 ? {} : { getToken: options.getConnectionToken }
79
+ });
80
+ this.client.on("connected", () => this.emit("connected"));
81
+ this.client.on("disconnected", (ctx) => this.emit("disconnected", ctx?.reason));
82
+ this.client.on(
83
+ "error",
84
+ (ctx) => this.emit("error", new RelyvoError("transport_error", String(ctx?.error ?? "connection error")))
85
+ );
86
+ }
87
+ /** Opens the socket and subscribes to the conversation and typing channels. */
88
+ async connect() {
89
+ const connected = new Promise((resolve, reject) => {
90
+ const timer = setTimeout(
91
+ () => reject(new RelyvoError("connect_timeout", "the realtime connection timed out")),
92
+ this.connectTimeoutMs
93
+ );
94
+ this.handlers.connected.add(() => {
95
+ clearTimeout(timer);
96
+ resolve();
97
+ });
98
+ this.handlers.error.add((error) => {
99
+ clearTimeout(timer);
100
+ reject(error);
101
+ });
102
+ });
103
+ this.client.connect();
104
+ await connected;
105
+ this.conversationSubscription = this.client.newSubscription(
106
+ this.credentials.channel,
107
+ this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken)
108
+ );
109
+ this.conversationSubscription.on("publication", (ctx) => this.handlePublication(ctx?.data));
110
+ await this.conversationSubscription.subscribe();
111
+ if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {
112
+ this.typingSubscription = this.client.newSubscription(
113
+ this.credentials.typingChannel,
114
+ this.subscriptionTokenOptions(
115
+ this.credentials.typingChannel,
116
+ this.credentials.typingSubscriptionToken
117
+ )
118
+ );
119
+ this.typingSubscription.on("publication", (ctx) => this.handleTyping(ctx?.data));
120
+ await this.typingSubscription.subscribe();
121
+ }
122
+ }
123
+ on(event, handler) {
124
+ this.handlers[event].add(handler);
125
+ return () => this.handlers[event].delete(handler);
126
+ }
127
+ /** Sends a message. The same clientMessageId returns the original message. */
128
+ async send(input) {
129
+ return this.rpc("chat.send", {
130
+ channel_id: this.channelId,
131
+ client_message_id: input.clientMessageId,
132
+ content: input.content,
133
+ type: input.type ?? "text",
134
+ ...input.metadata === void 0 ? {} : { metadata: input.metadata }
135
+ });
136
+ }
137
+ /** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
138
+ history(input = {}) {
139
+ return this.rpc("chat.history", {
140
+ channel_id: this.channelId,
141
+ ...input.before === void 0 ? {} : { cursor: input.before },
142
+ ...input.limit === void 0 ? {} : { limit: input.limit },
143
+ ...input.direction === void 0 ? {} : { direction: input.direction }
144
+ });
145
+ }
146
+ /** Marks the channel read up to a message. */
147
+ async markRead(messageId) {
148
+ await this.rpc("chat.read", { channel_id: this.channelId, message_id: messageId });
149
+ }
150
+ /** Lists the channels this user belongs to. */
151
+ channels(input = {}) {
152
+ return this.rpc("chat.channels", input).then((page) => page.items ?? []);
153
+ }
154
+ disconnect() {
155
+ this.conversationSubscription?.unsubscribe();
156
+ this.typingSubscription?.unsubscribe();
157
+ this.client.disconnect();
158
+ }
159
+ /** Channel id extracted from the subscription channel name. */
160
+ get channelId() {
161
+ const parts = this.credentials.channel.split(":");
162
+ const id = parts[parts.length - 1];
163
+ if (!id) {
164
+ throw new RelyvoError("invalid_credentials", "the channel name does not contain an id");
165
+ }
166
+ return id;
167
+ }
168
+ subscriptionTokenOptions(channel, token) {
169
+ return {
170
+ token,
171
+ ...this.getSubscriptionToken === void 0 ? {} : { getToken: () => this.getSubscriptionToken(channel) }
172
+ };
173
+ }
174
+ async rpc(method, data) {
175
+ let response;
176
+ try {
177
+ response = await this.client.rpc(method, data);
178
+ } catch (cause) {
179
+ throw new RelyvoError("transport_error", `rpc ${method} failed`, cause);
180
+ }
181
+ const envelope = response?.data;
182
+ if (!envelope || envelope.ok !== true) {
183
+ throw new RelyvoError(
184
+ envelope?.error?.code ?? "rpc_failed",
185
+ envelope?.error?.message ?? `rpc ${method} failed`
186
+ );
187
+ }
188
+ return envelope.data;
189
+ }
190
+ handlePublication(payload) {
191
+ if (!payload || payload.type !== "chat.message.created") return;
192
+ const data = payload.data ?? {};
193
+ if (data.channel_id !== this.channelId || !data.message) return;
194
+ const event = {
195
+ message: data.message,
196
+ channelId: data.channel_id,
197
+ senderId: data.sender?.id ?? data.message.sender_id,
198
+ senderName: data.sender?.display_name
199
+ };
200
+ this.emit("message", event);
201
+ }
202
+ handleTyping(payload) {
203
+ if (!payload || payload.type !== "chat.typing") return;
204
+ const data = payload.data ?? {};
205
+ if (data.channel_id !== this.channelId || !data.user_id) return;
206
+ const event = {
207
+ channel_id: data.channel_id,
208
+ user_id: data.user_id,
209
+ display_name: data.display_name
210
+ };
211
+ this.emit("typing", event);
212
+ }
213
+ emit(event, ...args) {
214
+ for (const handler of this.handlers[event]) {
215
+ handler(...args);
216
+ }
217
+ }
218
+ };
219
+
220
+ // src/video.ts
221
+ var import_livekit_client = require("livekit-client");
222
+ var defaultFactory2 = () => new import_livekit_client.Room({ adaptiveStream: true, dynacast: true });
223
+ var EVENT_NAMES = {
224
+ connected: import_livekit_client.RoomEvent.Connected,
225
+ disconnected: import_livekit_client.RoomEvent.Disconnected,
226
+ participantConnected: import_livekit_client.RoomEvent.ParticipantConnected,
227
+ participantDisconnected: import_livekit_client.RoomEvent.ParticipantDisconnected,
228
+ trackSubscribed: import_livekit_client.RoomEvent.TrackSubscribed,
229
+ trackUnsubscribed: import_livekit_client.RoomEvent.TrackUnsubscribed,
230
+ connectionQualityChanged: import_livekit_client.RoomEvent.ConnectionQualityChanged,
231
+ mediaDevicesError: import_livekit_client.RoomEvent.MediaDevicesError
232
+ };
233
+ var RelyvoCall = class _RelyvoCall {
234
+ constructor(room) {
235
+ __publicField(this, "room");
236
+ __publicField(this, "connected", false);
237
+ this.room = room;
238
+ }
239
+ /** Connects to the room the token was minted for. */
240
+ static async connect(credentials, options = {}) {
241
+ if (!credentials.serverUrl || !credentials.token) {
242
+ throw new RelyvoError("invalid_credentials", "serverUrl and token are required");
243
+ }
244
+ const call = new _RelyvoCall((options.createRoom ?? defaultFactory2)());
245
+ try {
246
+ await call.room.connect(credentials.serverUrl, credentials.token);
247
+ } catch (cause) {
248
+ throw new RelyvoError("connect_failed", "the media connection could not be established", cause);
249
+ }
250
+ call.connected = true;
251
+ return call;
252
+ }
253
+ /** Publishes or stops the camera. */
254
+ async enableCamera(enabled = true) {
255
+ this.requireConnected();
256
+ await this.room.localParticipant.setCameraEnabled(enabled);
257
+ }
258
+ /** Publishes or stops the microphone. */
259
+ async enableMicrophone(enabled = true) {
260
+ this.requireConnected();
261
+ await this.room.localParticipant.setMicrophoneEnabled(enabled);
262
+ }
263
+ /** Starts or stops screen sharing where the browser supports it. */
264
+ async shareScreen(enabled = true) {
265
+ this.requireConnected();
266
+ if (enabled && typeof navigator !== "undefined" && !navigator.mediaDevices?.getDisplayMedia) {
267
+ throw new RelyvoError("unsupported", "this browser does not support screen sharing");
268
+ }
269
+ await this.room.localParticipant.setScreenShareEnabled(enabled);
270
+ }
271
+ /** Subscribes to a room event. Returns an unsubscribe function. */
272
+ on(event, handler) {
273
+ const name = EVENT_NAMES[event];
274
+ this.room.on(name, handler);
275
+ return () => {
276
+ const target = this.room;
277
+ target.off?.(name, handler);
278
+ };
279
+ }
280
+ /** Leaves the room and releases the local devices. */
281
+ disconnect() {
282
+ if (!this.connected) return;
283
+ this.connected = false;
284
+ void this.room.disconnect();
285
+ }
286
+ requireConnected() {
287
+ if (!this.connected) {
288
+ throw new RelyvoError("not_connected", "the call is not connected");
289
+ }
290
+ }
291
+ };
292
+
293
+ // src/index.ts
294
+ var VERSION = "0.1.0";
295
+ // Annotate the CommonJS export names for ESM import in node:
296
+ 0 && (module.exports = {
297
+ RelyvoCall,
298
+ RelyvoChat,
299
+ RelyvoError,
300
+ VERSION,
301
+ isRelyvoError
302
+ });
303
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/chat.ts","../src/errors.ts","../src/video.ts"],"sourcesContent":["/**\n * Relyvo browser client SDK.\n *\n * The browser holds only short-lived credentials minted by your backend: a\n * Centrifugo connection token for chat and a LiveKit participant token for\n * media. Chat operations travel as RPC frames on the WebSocket, so this SDK\n * never calls Relyvo REST and never needs a CORS allowance.\n *\n * @example\n * ```ts\n * const chat = new RelyvoChat(credentials);\n * await chat.connect();\n * chat.on(\"message\", ({ message }) => console.log(message.content));\n * await chat.send({ content: \"hello\", clientMessageId: crypto.randomUUID() });\n * ```\n */\n\nexport { RelyvoChat } from \"./chat.js\";\nexport type {\n CentrifugeClient,\n CentrifugeFactory,\n CentrifugeSubscription,\n RelyvoChatOptions,\n} from \"./chat.js\";\nexport { RelyvoError, isRelyvoError } from \"./errors.js\";\nexport { RelyvoCall } from \"./video.js\";\nexport type { RelyvoCallOptions, RoomFactory, RoomLike } from \"./video.js\";\nexport type {\n CallCredentials,\n CallEvent,\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n ChatMessageType,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\nexport const VERSION = \"0.1.0\";\n","import { Centrifuge } from \"centrifuge\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type {\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\n/** The subset of the Centrifuge client this SDK depends on. */\nexport interface CentrifugeSubscription {\n subscribe(): Promise<unknown>;\n unsubscribe(): void;\n on(event: \"publication\" | \"subscribed\" | \"error\", handler: (ctx: any) => void): void;\n}\n\n/** Centrifuge fetches a replacement token through getToken when one expires. */\nexport interface ConnectionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface SubscriptionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface CentrifugeClient {\n connect(): void;\n disconnect(): void;\n on(event: \"connected\" | \"disconnected\" | \"error\", handler: (ctx: any) => void): void;\n newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;\n rpc(method: string, data: unknown): Promise<{ data: unknown }>;\n}\n\nexport type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;\n\nconst defaultFactory: CentrifugeFactory = (wsUrl, options) =>\n new Centrifuge(wsUrl, {\n token: options.token,\n ...(options.getToken === undefined ? {} : { getToken: options.getToken }),\n }) as unknown as CentrifugeClient;\n\nexport interface RelyvoChatOptions {\n /** Injectable transport, used by tests. */\n createCentrifuge?: CentrifugeFactory;\n connectTimeoutMs?: number;\n /**\n * Called when the connection token expires. Return a fresh Centrifugo\n * connection token from your backend; the transport reconnects by itself.\n */\n getConnectionToken?: () => Promise<string>;\n /**\n * Called when a subscription token expires. Receive the channel name and\n * return a fresh subscription token for it.\n */\n getSubscriptionToken?: (channel: string) => Promise<string>;\n}\n\ntype RpcEnvelope<T> = {\n ok?: boolean;\n data?: T;\n error?: { code?: string; message?: string };\n};\n\n/**\n * Realtime chat over the Centrifugo connection.\n *\n * Every operation is an RPC frame on the WebSocket the client already holds, so\n * the browser never calls Relyvo REST and needs no CORS allowance. Identity comes\n * from the connection token; this class never sends a project or user id.\n */\nexport class RelyvoChat {\n readonly credentials: ChatCredentials;\n\n private readonly client: CentrifugeClient;\n private readonly connectTimeoutMs: number;\n private readonly getSubscriptionToken?: (channel: string) => Promise<string>;\n private readonly handlers: { [K in keyof ChatEventHandler]: Set<ChatEventHandler[K]> } = {\n connected: new Set(),\n disconnected: new Set(),\n message: new Set(),\n typing: new Set(),\n error: new Set(),\n };\n private typingSubscription?: CentrifugeSubscription;\n private conversationSubscription?: CentrifugeSubscription;\n\n constructor(credentials: ChatCredentials, options: RelyvoChatOptions = {}) {\n this.credentials = credentials;\n this.connectTimeoutMs = options.connectTimeoutMs ?? 10_000;\n this.getSubscriptionToken = options.getSubscriptionToken;\n this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {\n token: credentials.token,\n ...(options.getConnectionToken === undefined\n ? {}\n : { getToken: options.getConnectionToken }),\n });\n\n this.client.on(\"connected\", () => this.emit(\"connected\"));\n this.client.on(\"disconnected\", (ctx) => this.emit(\"disconnected\", ctx?.reason));\n this.client.on(\"error\", (ctx) =>\n this.emit(\"error\", new RelyvoError(\"transport_error\", String(ctx?.error ?? \"connection error\"))),\n );\n }\n\n /** Opens the socket and subscribes to the conversation and typing channels. */\n async connect(): Promise<void> {\n const connected = new Promise<void>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new RelyvoError(\"connect_timeout\", \"the realtime connection timed out\")),\n this.connectTimeoutMs,\n );\n\n this.handlers.connected.add(() => {\n clearTimeout(timer);\n resolve();\n });\n this.handlers.error.add((error) => {\n clearTimeout(timer);\n reject(error);\n });\n });\n\n this.client.connect();\n await connected;\n\n this.conversationSubscription = this.client.newSubscription(\n this.credentials.channel,\n this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken),\n );\n this.conversationSubscription.on(\"publication\", (ctx) => this.handlePublication(ctx?.data));\n await this.conversationSubscription.subscribe();\n\n if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {\n this.typingSubscription = this.client.newSubscription(\n this.credentials.typingChannel,\n this.subscriptionTokenOptions(\n this.credentials.typingChannel,\n this.credentials.typingSubscriptionToken,\n ),\n );\n this.typingSubscription.on(\"publication\", (ctx) => this.handleTyping(ctx?.data));\n await this.typingSubscription.subscribe();\n }\n }\n\n on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void {\n this.handlers[event].add(handler);\n return () => this.handlers[event].delete(handler);\n }\n\n /** Sends a message. The same clientMessageId returns the original message. */\n async send(input: {\n content: string;\n clientMessageId: string;\n type?: string;\n metadata?: Record<string, unknown>;\n }): Promise<{ message: ChatMessage; created: boolean }> {\n return this.rpc(\"chat.send\", {\n channel_id: this.channelId,\n client_message_id: input.clientMessageId,\n content: input.content,\n type: input.type ?? \"text\",\n ...(input.metadata === undefined ? {} : { metadata: input.metadata }),\n });\n }\n\n /** Reads history backwards from a cursor, or forwards with `direction: \"after\"`. */\n history(input: { before?: string; limit?: number; direction?: \"before\" | \"after\" } = {}): Promise<ChatMessagePage> {\n return this.rpc(\"chat.history\", {\n channel_id: this.channelId,\n ...(input.before === undefined ? {} : { cursor: input.before }),\n ...(input.limit === undefined ? {} : { limit: input.limit }),\n ...(input.direction === undefined ? {} : { direction: input.direction }),\n });\n }\n\n /** Marks the channel read up to a message. */\n async markRead(messageId: string): Promise<void> {\n await this.rpc(\"chat.read\", { channel_id: this.channelId, message_id: messageId });\n }\n\n /** Lists the channels this user belongs to. */\n channels(input: { cursor?: string; limit?: number } = {}): Promise<ChatChannel[]> {\n return this.rpc<{ items: ChatChannel[] }>(\"chat.channels\", input).then((page) => page.items ?? []);\n }\n\n disconnect(): void {\n this.conversationSubscription?.unsubscribe();\n this.typingSubscription?.unsubscribe();\n this.client.disconnect();\n }\n\n /** Channel id extracted from the subscription channel name. */\n private get channelId(): string {\n const parts = this.credentials.channel.split(\":\");\n const id = parts[parts.length - 1];\n if (!id) {\n throw new RelyvoError(\"invalid_credentials\", \"the channel name does not contain an id\");\n }\n return id;\n }\n\n private subscriptionTokenOptions(\n channel: string,\n token: string,\n ): SubscriptionTokenOptions {\n return {\n token,\n ...(this.getSubscriptionToken === undefined\n ? {}\n : { getToken: () => this.getSubscriptionToken!(channel) }),\n };\n }\n\n private async rpc<T>(method: string, data: unknown): Promise<T> {\n let response: { data: unknown };\n try {\n response = await this.client.rpc(method, data);\n } catch (cause) {\n throw new RelyvoError(\"transport_error\", `rpc ${method} failed`, cause);\n }\n\n const envelope = response?.data as RpcEnvelope<T> | undefined;\n if (!envelope || envelope.ok !== true) {\n throw new RelyvoError(\n envelope?.error?.code ?? \"rpc_failed\",\n envelope?.error?.message ?? `rpc ${method} failed`,\n );\n }\n\n return envelope.data as T;\n }\n\n private handlePublication(payload: any): void {\n if (!payload || payload.type !== \"chat.message.created\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.message) return;\n\n const event: MessageEvent = {\n message: data.message,\n channelId: data.channel_id,\n senderId: data.sender?.id ?? data.message.sender_id,\n senderName: data.sender?.display_name,\n };\n this.emit(\"message\", event);\n }\n\n private handleTyping(payload: any): void {\n if (!payload || payload.type !== \"chat.typing\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.user_id) return;\n\n const event: TypingEvent = {\n channel_id: data.channel_id,\n user_id: data.user_id,\n display_name: data.display_name,\n };\n this.emit(\"typing\", event);\n }\n\n private emit<K extends keyof ChatEventHandler>(event: K, ...args: Parameters<ChatEventHandler[K]>): void {\n for (const handler of this.handlers[event]) {\n (handler as (...values: unknown[]) => void)(...args);\n }\n }\n}\n","/** One error type for every failure this SDK produces. */\nexport class RelyvoError extends Error {\n /** Stable machine code. RPC failures carry the chat service's code. */\n readonly code: string;\n /** Transport-level cause, when there is one. */\n readonly cause?: unknown;\n\n constructor(code: string, message: string, cause?: unknown) {\n super(message, cause === undefined ? undefined : { cause });\n this.name = \"RelyvoError\";\n this.code = code;\n this.cause = cause;\n }\n}\n\nexport function isRelyvoError(error: unknown): error is RelyvoError {\n return error instanceof RelyvoError;\n}\n","import { Room, RoomEvent } from \"livekit-client\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type { CallCredentials, CallEvent } from \"./types.js\";\n\n/** The subset of the LiveKit Room this SDK depends on. */\nexport interface RoomLike {\n connect(url: string, token: string): Promise<void>;\n disconnect(): Promise<void> | void;\n on(event: string, handler: (...args: any[]) => void): unknown;\n localParticipant: {\n setCameraEnabled(enabled: boolean): Promise<void>;\n setMicrophoneEnabled(enabled: boolean): Promise<void>;\n setScreenShareEnabled(enabled: boolean): Promise<void>;\n };\n}\n\nexport type RoomFactory = () => RoomLike;\n\nconst defaultFactory: RoomFactory = () =>\n new Room({ adaptiveStream: true, dynacast: true }) as unknown as RoomLike;\n\n/** LiveKit event names, mapped so callers do not import the media library. */\nconst EVENT_NAMES: Record<CallEvent, RoomEvent> = {\n connected: RoomEvent.Connected,\n disconnected: RoomEvent.Disconnected,\n participantConnected: RoomEvent.ParticipantConnected,\n participantDisconnected: RoomEvent.ParticipantDisconnected,\n trackSubscribed: RoomEvent.TrackSubscribed,\n trackUnsubscribed: RoomEvent.TrackUnsubscribed,\n connectionQualityChanged: RoomEvent.ConnectionQualityChanged,\n mediaDevicesError: RoomEvent.MediaDevicesError,\n};\n\nexport interface RelyvoCallOptions {\n /** Injectable room, used by tests. */\n createRoom?: RoomFactory;\n}\n\n/**\n * One video or voice call.\n *\n * The participant token is minted by your backend with the Relyvo server SDK; the\n * grant is join-only and already scoped to the room, so this class cannot ask for\n * more than that token allows.\n */\nexport class RelyvoCall {\n readonly room: RoomLike;\n private connected = false;\n\n private constructor(room: RoomLike) {\n this.room = room;\n }\n\n /** Connects to the room the token was minted for. */\n static async connect(\n credentials: CallCredentials,\n options: RelyvoCallOptions = {},\n ): Promise<RelyvoCall> {\n if (!credentials.serverUrl || !credentials.token) {\n throw new RelyvoError(\"invalid_credentials\", \"serverUrl and token are required\");\n }\n\n const call = new RelyvoCall((options.createRoom ?? defaultFactory)());\n\n try {\n await call.room.connect(credentials.serverUrl, credentials.token);\n } catch (cause) {\n throw new RelyvoError(\"connect_failed\", \"the media connection could not be established\", cause);\n }\n\n call.connected = true;\n return call;\n }\n\n /** Publishes or stops the camera. */\n async enableCamera(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setCameraEnabled(enabled);\n }\n\n /** Publishes or stops the microphone. */\n async enableMicrophone(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setMicrophoneEnabled(enabled);\n }\n\n /** Starts or stops screen sharing where the browser supports it. */\n async shareScreen(enabled = true): Promise<void> {\n this.requireConnected();\n if (enabled && typeof navigator !== \"undefined\" && !navigator.mediaDevices?.getDisplayMedia) {\n throw new RelyvoError(\"unsupported\", \"this browser does not support screen sharing\");\n }\n await this.room.localParticipant.setScreenShareEnabled(enabled);\n }\n\n /** Subscribes to a room event. Returns an unsubscribe function. */\n on(event: CallEvent, handler: (...args: any[]) => void): () => void {\n const name = EVENT_NAMES[event];\n this.room.on(name, handler);\n return () => {\n const target = this.room as unknown as {\n off?: (event: string, callback: (...args: any[]) => void) => void;\n };\n target.off?.(name, handler);\n };\n }\n\n /** Leaves the room and releases the local devices. */\n disconnect(): void {\n if (!this.connected) return;\n this.connected = false;\n void this.room.disconnect();\n }\n\n private requireConnected(): void {\n if (!this.connected) {\n throw new RelyvoError(\"not_connected\", \"the call is not connected\");\n }\n }\n}\n"],"mappings":"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","names":["defaultFactory"]}
@@ -0,0 +1,232 @@
1
+ type ChatMessageType = "text" | "system" | "attachment";
2
+ interface ChatMessage {
3
+ id: string;
4
+ channel_id: string;
5
+ channel_seq: number;
6
+ sender_id: string;
7
+ client_message_id?: string;
8
+ type: ChatMessageType;
9
+ content: string;
10
+ metadata?: Record<string, unknown> | null;
11
+ reply_to_message_id?: string | null;
12
+ created_at: string;
13
+ updated_at?: string;
14
+ deleted_at?: string | null;
15
+ }
16
+ interface ChatMessagePage {
17
+ items: ChatMessage[];
18
+ has_more: boolean;
19
+ next_cursor?: string;
20
+ }
21
+ interface ChatChannel {
22
+ id: string;
23
+ type: string;
24
+ name: string;
25
+ last_seq: number;
26
+ external_id?: string;
27
+ }
28
+ /** Credentials your backend mints with the Relyvo server SDK. */
29
+ interface ChatCredentials {
30
+ /** Centrifugo WebSocket URL, e.g. wss://chat.example.com/connection/websocket */
31
+ wsUrl: string;
32
+ /** Centrifugo connection token for this user. */
33
+ token: string;
34
+ /** Conversation channel, e.g. project:<projectID>:channel:<channelID> */
35
+ channel: string;
36
+ /** Subscription token for the conversation channel. */
37
+ subscriptionToken: string;
38
+ /** Optional sibling channel that carries typing events. */
39
+ typingChannel?: string;
40
+ /** Subscription token for the typing channel. */
41
+ typingSubscriptionToken?: string;
42
+ }
43
+ interface CallCredentials {
44
+ /** Browser-facing LiveKit URL, e.g. wss://rtc.example.com */
45
+ serverUrl: string;
46
+ /** Short-lived participant token minted by your backend. */
47
+ token: string;
48
+ }
49
+ interface TypingEvent {
50
+ channel_id: string;
51
+ user_id: string;
52
+ display_name?: string;
53
+ }
54
+ interface MessageEvent {
55
+ message: ChatMessage;
56
+ senderId: string;
57
+ senderName?: string;
58
+ channelId: string;
59
+ }
60
+ type ChatEventHandler = {
61
+ connected: () => void;
62
+ disconnected: (reason?: string) => void;
63
+ message: (event: MessageEvent) => void;
64
+ typing: (event: TypingEvent) => void;
65
+ error: (error: Error) => void;
66
+ };
67
+ type CallEvent = "connected" | "disconnected" | "participantConnected" | "participantDisconnected" | "trackSubscribed" | "trackUnsubscribed" | "connectionQualityChanged" | "mediaDevicesError";
68
+
69
+ /** The subset of the Centrifuge client this SDK depends on. */
70
+ interface CentrifugeSubscription {
71
+ subscribe(): Promise<unknown>;
72
+ unsubscribe(): void;
73
+ on(event: "publication" | "subscribed" | "error", handler: (ctx: any) => void): void;
74
+ }
75
+ /** Centrifuge fetches a replacement token through getToken when one expires. */
76
+ interface ConnectionTokenOptions {
77
+ token?: string;
78
+ getToken?: () => Promise<string>;
79
+ }
80
+ interface SubscriptionTokenOptions {
81
+ token?: string;
82
+ getToken?: () => Promise<string>;
83
+ }
84
+ interface CentrifugeClient {
85
+ connect(): void;
86
+ disconnect(): void;
87
+ on(event: "connected" | "disconnected" | "error", handler: (ctx: any) => void): void;
88
+ newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;
89
+ rpc(method: string, data: unknown): Promise<{
90
+ data: unknown;
91
+ }>;
92
+ }
93
+ type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;
94
+ interface RelyvoChatOptions {
95
+ /** Injectable transport, used by tests. */
96
+ createCentrifuge?: CentrifugeFactory;
97
+ connectTimeoutMs?: number;
98
+ /**
99
+ * Called when the connection token expires. Return a fresh Centrifugo
100
+ * connection token from your backend; the transport reconnects by itself.
101
+ */
102
+ getConnectionToken?: () => Promise<string>;
103
+ /**
104
+ * Called when a subscription token expires. Receive the channel name and
105
+ * return a fresh subscription token for it.
106
+ */
107
+ getSubscriptionToken?: (channel: string) => Promise<string>;
108
+ }
109
+ /**
110
+ * Realtime chat over the Centrifugo connection.
111
+ *
112
+ * Every operation is an RPC frame on the WebSocket the client already holds, so
113
+ * the browser never calls Relyvo REST and needs no CORS allowance. Identity comes
114
+ * from the connection token; this class never sends a project or user id.
115
+ */
116
+ declare class RelyvoChat {
117
+ readonly credentials: ChatCredentials;
118
+ private readonly client;
119
+ private readonly connectTimeoutMs;
120
+ private readonly getSubscriptionToken?;
121
+ private readonly handlers;
122
+ private typingSubscription?;
123
+ private conversationSubscription?;
124
+ constructor(credentials: ChatCredentials, options?: RelyvoChatOptions);
125
+ /** Opens the socket and subscribes to the conversation and typing channels. */
126
+ connect(): Promise<void>;
127
+ on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void;
128
+ /** Sends a message. The same clientMessageId returns the original message. */
129
+ send(input: {
130
+ content: string;
131
+ clientMessageId: string;
132
+ type?: string;
133
+ metadata?: Record<string, unknown>;
134
+ }): Promise<{
135
+ message: ChatMessage;
136
+ created: boolean;
137
+ }>;
138
+ /** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
139
+ history(input?: {
140
+ before?: string;
141
+ limit?: number;
142
+ direction?: "before" | "after";
143
+ }): Promise<ChatMessagePage>;
144
+ /** Marks the channel read up to a message. */
145
+ markRead(messageId: string): Promise<void>;
146
+ /** Lists the channels this user belongs to. */
147
+ channels(input?: {
148
+ cursor?: string;
149
+ limit?: number;
150
+ }): Promise<ChatChannel[]>;
151
+ disconnect(): void;
152
+ /** Channel id extracted from the subscription channel name. */
153
+ private get channelId();
154
+ private subscriptionTokenOptions;
155
+ private rpc;
156
+ private handlePublication;
157
+ private handleTyping;
158
+ private emit;
159
+ }
160
+
161
+ /** One error type for every failure this SDK produces. */
162
+ declare class RelyvoError extends Error {
163
+ /** Stable machine code. RPC failures carry the chat service's code. */
164
+ readonly code: string;
165
+ /** Transport-level cause, when there is one. */
166
+ readonly cause?: unknown;
167
+ constructor(code: string, message: string, cause?: unknown);
168
+ }
169
+ declare function isRelyvoError(error: unknown): error is RelyvoError;
170
+
171
+ /** The subset of the LiveKit Room this SDK depends on. */
172
+ interface RoomLike {
173
+ connect(url: string, token: string): Promise<void>;
174
+ disconnect(): Promise<void> | void;
175
+ on(event: string, handler: (...args: any[]) => void): unknown;
176
+ localParticipant: {
177
+ setCameraEnabled(enabled: boolean): Promise<void>;
178
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
179
+ setScreenShareEnabled(enabled: boolean): Promise<void>;
180
+ };
181
+ }
182
+ type RoomFactory = () => RoomLike;
183
+ interface RelyvoCallOptions {
184
+ /** Injectable room, used by tests. */
185
+ createRoom?: RoomFactory;
186
+ }
187
+ /**
188
+ * One video or voice call.
189
+ *
190
+ * The participant token is minted by your backend with the Relyvo server SDK; the
191
+ * grant is join-only and already scoped to the room, so this class cannot ask for
192
+ * more than that token allows.
193
+ */
194
+ declare class RelyvoCall {
195
+ readonly room: RoomLike;
196
+ private connected;
197
+ private constructor();
198
+ /** Connects to the room the token was minted for. */
199
+ static connect(credentials: CallCredentials, options?: RelyvoCallOptions): Promise<RelyvoCall>;
200
+ /** Publishes or stops the camera. */
201
+ enableCamera(enabled?: boolean): Promise<void>;
202
+ /** Publishes or stops the microphone. */
203
+ enableMicrophone(enabled?: boolean): Promise<void>;
204
+ /** Starts or stops screen sharing where the browser supports it. */
205
+ shareScreen(enabled?: boolean): Promise<void>;
206
+ /** Subscribes to a room event. Returns an unsubscribe function. */
207
+ on(event: CallEvent, handler: (...args: any[]) => void): () => void;
208
+ /** Leaves the room and releases the local devices. */
209
+ disconnect(): void;
210
+ private requireConnected;
211
+ }
212
+
213
+ /**
214
+ * Relyvo browser client SDK.
215
+ *
216
+ * The browser holds only short-lived credentials minted by your backend: a
217
+ * Centrifugo connection token for chat and a LiveKit participant token for
218
+ * media. Chat operations travel as RPC frames on the WebSocket, so this SDK
219
+ * never calls Relyvo REST and never needs a CORS allowance.
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * const chat = new RelyvoChat(credentials);
224
+ * await chat.connect();
225
+ * chat.on("message", ({ message }) => console.log(message.content));
226
+ * await chat.send({ content: "hello", clientMessageId: crypto.randomUUID() });
227
+ * ```
228
+ */
229
+
230
+ declare const VERSION = "0.1.0";
231
+
232
+ export { type CallCredentials, type CallEvent, type CentrifugeClient, type CentrifugeFactory, type CentrifugeSubscription, type ChatChannel, type ChatCredentials, type ChatEventHandler, type ChatMessage, type ChatMessagePage, type ChatMessageType, type MessageEvent, RelyvoCall, type RelyvoCallOptions, RelyvoChat, type RelyvoChatOptions, RelyvoError, type RoomFactory, type RoomLike, type TypingEvent, VERSION, isRelyvoError };
@@ -0,0 +1,232 @@
1
+ type ChatMessageType = "text" | "system" | "attachment";
2
+ interface ChatMessage {
3
+ id: string;
4
+ channel_id: string;
5
+ channel_seq: number;
6
+ sender_id: string;
7
+ client_message_id?: string;
8
+ type: ChatMessageType;
9
+ content: string;
10
+ metadata?: Record<string, unknown> | null;
11
+ reply_to_message_id?: string | null;
12
+ created_at: string;
13
+ updated_at?: string;
14
+ deleted_at?: string | null;
15
+ }
16
+ interface ChatMessagePage {
17
+ items: ChatMessage[];
18
+ has_more: boolean;
19
+ next_cursor?: string;
20
+ }
21
+ interface ChatChannel {
22
+ id: string;
23
+ type: string;
24
+ name: string;
25
+ last_seq: number;
26
+ external_id?: string;
27
+ }
28
+ /** Credentials your backend mints with the Relyvo server SDK. */
29
+ interface ChatCredentials {
30
+ /** Centrifugo WebSocket URL, e.g. wss://chat.example.com/connection/websocket */
31
+ wsUrl: string;
32
+ /** Centrifugo connection token for this user. */
33
+ token: string;
34
+ /** Conversation channel, e.g. project:<projectID>:channel:<channelID> */
35
+ channel: string;
36
+ /** Subscription token for the conversation channel. */
37
+ subscriptionToken: string;
38
+ /** Optional sibling channel that carries typing events. */
39
+ typingChannel?: string;
40
+ /** Subscription token for the typing channel. */
41
+ typingSubscriptionToken?: string;
42
+ }
43
+ interface CallCredentials {
44
+ /** Browser-facing LiveKit URL, e.g. wss://rtc.example.com */
45
+ serverUrl: string;
46
+ /** Short-lived participant token minted by your backend. */
47
+ token: string;
48
+ }
49
+ interface TypingEvent {
50
+ channel_id: string;
51
+ user_id: string;
52
+ display_name?: string;
53
+ }
54
+ interface MessageEvent {
55
+ message: ChatMessage;
56
+ senderId: string;
57
+ senderName?: string;
58
+ channelId: string;
59
+ }
60
+ type ChatEventHandler = {
61
+ connected: () => void;
62
+ disconnected: (reason?: string) => void;
63
+ message: (event: MessageEvent) => void;
64
+ typing: (event: TypingEvent) => void;
65
+ error: (error: Error) => void;
66
+ };
67
+ type CallEvent = "connected" | "disconnected" | "participantConnected" | "participantDisconnected" | "trackSubscribed" | "trackUnsubscribed" | "connectionQualityChanged" | "mediaDevicesError";
68
+
69
+ /** The subset of the Centrifuge client this SDK depends on. */
70
+ interface CentrifugeSubscription {
71
+ subscribe(): Promise<unknown>;
72
+ unsubscribe(): void;
73
+ on(event: "publication" | "subscribed" | "error", handler: (ctx: any) => void): void;
74
+ }
75
+ /** Centrifuge fetches a replacement token through getToken when one expires. */
76
+ interface ConnectionTokenOptions {
77
+ token?: string;
78
+ getToken?: () => Promise<string>;
79
+ }
80
+ interface SubscriptionTokenOptions {
81
+ token?: string;
82
+ getToken?: () => Promise<string>;
83
+ }
84
+ interface CentrifugeClient {
85
+ connect(): void;
86
+ disconnect(): void;
87
+ on(event: "connected" | "disconnected" | "error", handler: (ctx: any) => void): void;
88
+ newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;
89
+ rpc(method: string, data: unknown): Promise<{
90
+ data: unknown;
91
+ }>;
92
+ }
93
+ type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;
94
+ interface RelyvoChatOptions {
95
+ /** Injectable transport, used by tests. */
96
+ createCentrifuge?: CentrifugeFactory;
97
+ connectTimeoutMs?: number;
98
+ /**
99
+ * Called when the connection token expires. Return a fresh Centrifugo
100
+ * connection token from your backend; the transport reconnects by itself.
101
+ */
102
+ getConnectionToken?: () => Promise<string>;
103
+ /**
104
+ * Called when a subscription token expires. Receive the channel name and
105
+ * return a fresh subscription token for it.
106
+ */
107
+ getSubscriptionToken?: (channel: string) => Promise<string>;
108
+ }
109
+ /**
110
+ * Realtime chat over the Centrifugo connection.
111
+ *
112
+ * Every operation is an RPC frame on the WebSocket the client already holds, so
113
+ * the browser never calls Relyvo REST and needs no CORS allowance. Identity comes
114
+ * from the connection token; this class never sends a project or user id.
115
+ */
116
+ declare class RelyvoChat {
117
+ readonly credentials: ChatCredentials;
118
+ private readonly client;
119
+ private readonly connectTimeoutMs;
120
+ private readonly getSubscriptionToken?;
121
+ private readonly handlers;
122
+ private typingSubscription?;
123
+ private conversationSubscription?;
124
+ constructor(credentials: ChatCredentials, options?: RelyvoChatOptions);
125
+ /** Opens the socket and subscribes to the conversation and typing channels. */
126
+ connect(): Promise<void>;
127
+ on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void;
128
+ /** Sends a message. The same clientMessageId returns the original message. */
129
+ send(input: {
130
+ content: string;
131
+ clientMessageId: string;
132
+ type?: string;
133
+ metadata?: Record<string, unknown>;
134
+ }): Promise<{
135
+ message: ChatMessage;
136
+ created: boolean;
137
+ }>;
138
+ /** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
139
+ history(input?: {
140
+ before?: string;
141
+ limit?: number;
142
+ direction?: "before" | "after";
143
+ }): Promise<ChatMessagePage>;
144
+ /** Marks the channel read up to a message. */
145
+ markRead(messageId: string): Promise<void>;
146
+ /** Lists the channels this user belongs to. */
147
+ channels(input?: {
148
+ cursor?: string;
149
+ limit?: number;
150
+ }): Promise<ChatChannel[]>;
151
+ disconnect(): void;
152
+ /** Channel id extracted from the subscription channel name. */
153
+ private get channelId();
154
+ private subscriptionTokenOptions;
155
+ private rpc;
156
+ private handlePublication;
157
+ private handleTyping;
158
+ private emit;
159
+ }
160
+
161
+ /** One error type for every failure this SDK produces. */
162
+ declare class RelyvoError extends Error {
163
+ /** Stable machine code. RPC failures carry the chat service's code. */
164
+ readonly code: string;
165
+ /** Transport-level cause, when there is one. */
166
+ readonly cause?: unknown;
167
+ constructor(code: string, message: string, cause?: unknown);
168
+ }
169
+ declare function isRelyvoError(error: unknown): error is RelyvoError;
170
+
171
+ /** The subset of the LiveKit Room this SDK depends on. */
172
+ interface RoomLike {
173
+ connect(url: string, token: string): Promise<void>;
174
+ disconnect(): Promise<void> | void;
175
+ on(event: string, handler: (...args: any[]) => void): unknown;
176
+ localParticipant: {
177
+ setCameraEnabled(enabled: boolean): Promise<void>;
178
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
179
+ setScreenShareEnabled(enabled: boolean): Promise<void>;
180
+ };
181
+ }
182
+ type RoomFactory = () => RoomLike;
183
+ interface RelyvoCallOptions {
184
+ /** Injectable room, used by tests. */
185
+ createRoom?: RoomFactory;
186
+ }
187
+ /**
188
+ * One video or voice call.
189
+ *
190
+ * The participant token is minted by your backend with the Relyvo server SDK; the
191
+ * grant is join-only and already scoped to the room, so this class cannot ask for
192
+ * more than that token allows.
193
+ */
194
+ declare class RelyvoCall {
195
+ readonly room: RoomLike;
196
+ private connected;
197
+ private constructor();
198
+ /** Connects to the room the token was minted for. */
199
+ static connect(credentials: CallCredentials, options?: RelyvoCallOptions): Promise<RelyvoCall>;
200
+ /** Publishes or stops the camera. */
201
+ enableCamera(enabled?: boolean): Promise<void>;
202
+ /** Publishes or stops the microphone. */
203
+ enableMicrophone(enabled?: boolean): Promise<void>;
204
+ /** Starts or stops screen sharing where the browser supports it. */
205
+ shareScreen(enabled?: boolean): Promise<void>;
206
+ /** Subscribes to a room event. Returns an unsubscribe function. */
207
+ on(event: CallEvent, handler: (...args: any[]) => void): () => void;
208
+ /** Leaves the room and releases the local devices. */
209
+ disconnect(): void;
210
+ private requireConnected;
211
+ }
212
+
213
+ /**
214
+ * Relyvo browser client SDK.
215
+ *
216
+ * The browser holds only short-lived credentials minted by your backend: a
217
+ * Centrifugo connection token for chat and a LiveKit participant token for
218
+ * media. Chat operations travel as RPC frames on the WebSocket, so this SDK
219
+ * never calls Relyvo REST and never needs a CORS allowance.
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * const chat = new RelyvoChat(credentials);
224
+ * await chat.connect();
225
+ * chat.on("message", ({ message }) => console.log(message.content));
226
+ * await chat.send({ content: "hello", clientMessageId: crypto.randomUUID() });
227
+ * ```
228
+ */
229
+
230
+ declare const VERSION = "0.1.0";
231
+
232
+ export { type CallCredentials, type CallEvent, type CentrifugeClient, type CentrifugeFactory, type CentrifugeSubscription, type ChatChannel, type ChatCredentials, type ChatEventHandler, type ChatMessage, type ChatMessagePage, type ChatMessageType, type MessageEvent, RelyvoCall, type RelyvoCallOptions, RelyvoChat, type RelyvoChatOptions, RelyvoError, type RoomFactory, type RoomLike, type TypingEvent, VERSION, isRelyvoError };
package/dist/index.js ADDED
@@ -0,0 +1,274 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/chat.ts
6
+ import { Centrifuge } from "centrifuge";
7
+
8
+ // src/errors.ts
9
+ var RelyvoError = class extends Error {
10
+ constructor(code, message, cause) {
11
+ super(message, cause === void 0 ? void 0 : { cause });
12
+ /** Stable machine code. RPC failures carry the chat service's code. */
13
+ __publicField(this, "code");
14
+ /** Transport-level cause, when there is one. */
15
+ __publicField(this, "cause");
16
+ this.name = "RelyvoError";
17
+ this.code = code;
18
+ this.cause = cause;
19
+ }
20
+ };
21
+ function isRelyvoError(error) {
22
+ return error instanceof RelyvoError;
23
+ }
24
+
25
+ // src/chat.ts
26
+ var defaultFactory = (wsUrl, options) => new Centrifuge(wsUrl, {
27
+ token: options.token,
28
+ ...options.getToken === void 0 ? {} : { getToken: options.getToken }
29
+ });
30
+ var RelyvoChat = class {
31
+ constructor(credentials, options = {}) {
32
+ __publicField(this, "credentials");
33
+ __publicField(this, "client");
34
+ __publicField(this, "connectTimeoutMs");
35
+ __publicField(this, "getSubscriptionToken");
36
+ __publicField(this, "handlers", {
37
+ connected: /* @__PURE__ */ new Set(),
38
+ disconnected: /* @__PURE__ */ new Set(),
39
+ message: /* @__PURE__ */ new Set(),
40
+ typing: /* @__PURE__ */ new Set(),
41
+ error: /* @__PURE__ */ new Set()
42
+ });
43
+ __publicField(this, "typingSubscription");
44
+ __publicField(this, "conversationSubscription");
45
+ this.credentials = credentials;
46
+ this.connectTimeoutMs = options.connectTimeoutMs ?? 1e4;
47
+ this.getSubscriptionToken = options.getSubscriptionToken;
48
+ this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {
49
+ token: credentials.token,
50
+ ...options.getConnectionToken === void 0 ? {} : { getToken: options.getConnectionToken }
51
+ });
52
+ this.client.on("connected", () => this.emit("connected"));
53
+ this.client.on("disconnected", (ctx) => this.emit("disconnected", ctx?.reason));
54
+ this.client.on(
55
+ "error",
56
+ (ctx) => this.emit("error", new RelyvoError("transport_error", String(ctx?.error ?? "connection error")))
57
+ );
58
+ }
59
+ /** Opens the socket and subscribes to the conversation and typing channels. */
60
+ async connect() {
61
+ const connected = new Promise((resolve, reject) => {
62
+ const timer = setTimeout(
63
+ () => reject(new RelyvoError("connect_timeout", "the realtime connection timed out")),
64
+ this.connectTimeoutMs
65
+ );
66
+ this.handlers.connected.add(() => {
67
+ clearTimeout(timer);
68
+ resolve();
69
+ });
70
+ this.handlers.error.add((error) => {
71
+ clearTimeout(timer);
72
+ reject(error);
73
+ });
74
+ });
75
+ this.client.connect();
76
+ await connected;
77
+ this.conversationSubscription = this.client.newSubscription(
78
+ this.credentials.channel,
79
+ this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken)
80
+ );
81
+ this.conversationSubscription.on("publication", (ctx) => this.handlePublication(ctx?.data));
82
+ await this.conversationSubscription.subscribe();
83
+ if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {
84
+ this.typingSubscription = this.client.newSubscription(
85
+ this.credentials.typingChannel,
86
+ this.subscriptionTokenOptions(
87
+ this.credentials.typingChannel,
88
+ this.credentials.typingSubscriptionToken
89
+ )
90
+ );
91
+ this.typingSubscription.on("publication", (ctx) => this.handleTyping(ctx?.data));
92
+ await this.typingSubscription.subscribe();
93
+ }
94
+ }
95
+ on(event, handler) {
96
+ this.handlers[event].add(handler);
97
+ return () => this.handlers[event].delete(handler);
98
+ }
99
+ /** Sends a message. The same clientMessageId returns the original message. */
100
+ async send(input) {
101
+ return this.rpc("chat.send", {
102
+ channel_id: this.channelId,
103
+ client_message_id: input.clientMessageId,
104
+ content: input.content,
105
+ type: input.type ?? "text",
106
+ ...input.metadata === void 0 ? {} : { metadata: input.metadata }
107
+ });
108
+ }
109
+ /** Reads history backwards from a cursor, or forwards with `direction: "after"`. */
110
+ history(input = {}) {
111
+ return this.rpc("chat.history", {
112
+ channel_id: this.channelId,
113
+ ...input.before === void 0 ? {} : { cursor: input.before },
114
+ ...input.limit === void 0 ? {} : { limit: input.limit },
115
+ ...input.direction === void 0 ? {} : { direction: input.direction }
116
+ });
117
+ }
118
+ /** Marks the channel read up to a message. */
119
+ async markRead(messageId) {
120
+ await this.rpc("chat.read", { channel_id: this.channelId, message_id: messageId });
121
+ }
122
+ /** Lists the channels this user belongs to. */
123
+ channels(input = {}) {
124
+ return this.rpc("chat.channels", input).then((page) => page.items ?? []);
125
+ }
126
+ disconnect() {
127
+ this.conversationSubscription?.unsubscribe();
128
+ this.typingSubscription?.unsubscribe();
129
+ this.client.disconnect();
130
+ }
131
+ /** Channel id extracted from the subscription channel name. */
132
+ get channelId() {
133
+ const parts = this.credentials.channel.split(":");
134
+ const id = parts[parts.length - 1];
135
+ if (!id) {
136
+ throw new RelyvoError("invalid_credentials", "the channel name does not contain an id");
137
+ }
138
+ return id;
139
+ }
140
+ subscriptionTokenOptions(channel, token) {
141
+ return {
142
+ token,
143
+ ...this.getSubscriptionToken === void 0 ? {} : { getToken: () => this.getSubscriptionToken(channel) }
144
+ };
145
+ }
146
+ async rpc(method, data) {
147
+ let response;
148
+ try {
149
+ response = await this.client.rpc(method, data);
150
+ } catch (cause) {
151
+ throw new RelyvoError("transport_error", `rpc ${method} failed`, cause);
152
+ }
153
+ const envelope = response?.data;
154
+ if (!envelope || envelope.ok !== true) {
155
+ throw new RelyvoError(
156
+ envelope?.error?.code ?? "rpc_failed",
157
+ envelope?.error?.message ?? `rpc ${method} failed`
158
+ );
159
+ }
160
+ return envelope.data;
161
+ }
162
+ handlePublication(payload) {
163
+ if (!payload || payload.type !== "chat.message.created") return;
164
+ const data = payload.data ?? {};
165
+ if (data.channel_id !== this.channelId || !data.message) return;
166
+ const event = {
167
+ message: data.message,
168
+ channelId: data.channel_id,
169
+ senderId: data.sender?.id ?? data.message.sender_id,
170
+ senderName: data.sender?.display_name
171
+ };
172
+ this.emit("message", event);
173
+ }
174
+ handleTyping(payload) {
175
+ if (!payload || payload.type !== "chat.typing") return;
176
+ const data = payload.data ?? {};
177
+ if (data.channel_id !== this.channelId || !data.user_id) return;
178
+ const event = {
179
+ channel_id: data.channel_id,
180
+ user_id: data.user_id,
181
+ display_name: data.display_name
182
+ };
183
+ this.emit("typing", event);
184
+ }
185
+ emit(event, ...args) {
186
+ for (const handler of this.handlers[event]) {
187
+ handler(...args);
188
+ }
189
+ }
190
+ };
191
+
192
+ // src/video.ts
193
+ import { Room, RoomEvent } from "livekit-client";
194
+ var defaultFactory2 = () => new Room({ adaptiveStream: true, dynacast: true });
195
+ var EVENT_NAMES = {
196
+ connected: RoomEvent.Connected,
197
+ disconnected: RoomEvent.Disconnected,
198
+ participantConnected: RoomEvent.ParticipantConnected,
199
+ participantDisconnected: RoomEvent.ParticipantDisconnected,
200
+ trackSubscribed: RoomEvent.TrackSubscribed,
201
+ trackUnsubscribed: RoomEvent.TrackUnsubscribed,
202
+ connectionQualityChanged: RoomEvent.ConnectionQualityChanged,
203
+ mediaDevicesError: RoomEvent.MediaDevicesError
204
+ };
205
+ var RelyvoCall = class _RelyvoCall {
206
+ constructor(room) {
207
+ __publicField(this, "room");
208
+ __publicField(this, "connected", false);
209
+ this.room = room;
210
+ }
211
+ /** Connects to the room the token was minted for. */
212
+ static async connect(credentials, options = {}) {
213
+ if (!credentials.serverUrl || !credentials.token) {
214
+ throw new RelyvoError("invalid_credentials", "serverUrl and token are required");
215
+ }
216
+ const call = new _RelyvoCall((options.createRoom ?? defaultFactory2)());
217
+ try {
218
+ await call.room.connect(credentials.serverUrl, credentials.token);
219
+ } catch (cause) {
220
+ throw new RelyvoError("connect_failed", "the media connection could not be established", cause);
221
+ }
222
+ call.connected = true;
223
+ return call;
224
+ }
225
+ /** Publishes or stops the camera. */
226
+ async enableCamera(enabled = true) {
227
+ this.requireConnected();
228
+ await this.room.localParticipant.setCameraEnabled(enabled);
229
+ }
230
+ /** Publishes or stops the microphone. */
231
+ async enableMicrophone(enabled = true) {
232
+ this.requireConnected();
233
+ await this.room.localParticipant.setMicrophoneEnabled(enabled);
234
+ }
235
+ /** Starts or stops screen sharing where the browser supports it. */
236
+ async shareScreen(enabled = true) {
237
+ this.requireConnected();
238
+ if (enabled && typeof navigator !== "undefined" && !navigator.mediaDevices?.getDisplayMedia) {
239
+ throw new RelyvoError("unsupported", "this browser does not support screen sharing");
240
+ }
241
+ await this.room.localParticipant.setScreenShareEnabled(enabled);
242
+ }
243
+ /** Subscribes to a room event. Returns an unsubscribe function. */
244
+ on(event, handler) {
245
+ const name = EVENT_NAMES[event];
246
+ this.room.on(name, handler);
247
+ return () => {
248
+ const target = this.room;
249
+ target.off?.(name, handler);
250
+ };
251
+ }
252
+ /** Leaves the room and releases the local devices. */
253
+ disconnect() {
254
+ if (!this.connected) return;
255
+ this.connected = false;
256
+ void this.room.disconnect();
257
+ }
258
+ requireConnected() {
259
+ if (!this.connected) {
260
+ throw new RelyvoError("not_connected", "the call is not connected");
261
+ }
262
+ }
263
+ };
264
+
265
+ // src/index.ts
266
+ var VERSION = "0.1.0";
267
+ export {
268
+ RelyvoCall,
269
+ RelyvoChat,
270
+ RelyvoError,
271
+ VERSION,
272
+ isRelyvoError
273
+ };
274
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat.ts","../src/errors.ts","../src/video.ts","../src/index.ts"],"sourcesContent":["import { Centrifuge } from \"centrifuge\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type {\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\n/** The subset of the Centrifuge client this SDK depends on. */\nexport interface CentrifugeSubscription {\n subscribe(): Promise<unknown>;\n unsubscribe(): void;\n on(event: \"publication\" | \"subscribed\" | \"error\", handler: (ctx: any) => void): void;\n}\n\n/** Centrifuge fetches a replacement token through getToken when one expires. */\nexport interface ConnectionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface SubscriptionTokenOptions {\n token?: string;\n getToken?: () => Promise<string>;\n}\n\nexport interface CentrifugeClient {\n connect(): void;\n disconnect(): void;\n on(event: \"connected\" | \"disconnected\" | \"error\", handler: (ctx: any) => void): void;\n newSubscription(channel: string, options: SubscriptionTokenOptions): CentrifugeSubscription;\n rpc(method: string, data: unknown): Promise<{ data: unknown }>;\n}\n\nexport type CentrifugeFactory = (wsUrl: string, options: ConnectionTokenOptions) => CentrifugeClient;\n\nconst defaultFactory: CentrifugeFactory = (wsUrl, options) =>\n new Centrifuge(wsUrl, {\n token: options.token,\n ...(options.getToken === undefined ? {} : { getToken: options.getToken }),\n }) as unknown as CentrifugeClient;\n\nexport interface RelyvoChatOptions {\n /** Injectable transport, used by tests. */\n createCentrifuge?: CentrifugeFactory;\n connectTimeoutMs?: number;\n /**\n * Called when the connection token expires. Return a fresh Centrifugo\n * connection token from your backend; the transport reconnects by itself.\n */\n getConnectionToken?: () => Promise<string>;\n /**\n * Called when a subscription token expires. Receive the channel name and\n * return a fresh subscription token for it.\n */\n getSubscriptionToken?: (channel: string) => Promise<string>;\n}\n\ntype RpcEnvelope<T> = {\n ok?: boolean;\n data?: T;\n error?: { code?: string; message?: string };\n};\n\n/**\n * Realtime chat over the Centrifugo connection.\n *\n * Every operation is an RPC frame on the WebSocket the client already holds, so\n * the browser never calls Relyvo REST and needs no CORS allowance. Identity comes\n * from the connection token; this class never sends a project or user id.\n */\nexport class RelyvoChat {\n readonly credentials: ChatCredentials;\n\n private readonly client: CentrifugeClient;\n private readonly connectTimeoutMs: number;\n private readonly getSubscriptionToken?: (channel: string) => Promise<string>;\n private readonly handlers: { [K in keyof ChatEventHandler]: Set<ChatEventHandler[K]> } = {\n connected: new Set(),\n disconnected: new Set(),\n message: new Set(),\n typing: new Set(),\n error: new Set(),\n };\n private typingSubscription?: CentrifugeSubscription;\n private conversationSubscription?: CentrifugeSubscription;\n\n constructor(credentials: ChatCredentials, options: RelyvoChatOptions = {}) {\n this.credentials = credentials;\n this.connectTimeoutMs = options.connectTimeoutMs ?? 10_000;\n this.getSubscriptionToken = options.getSubscriptionToken;\n this.client = (options.createCentrifuge ?? defaultFactory)(credentials.wsUrl, {\n token: credentials.token,\n ...(options.getConnectionToken === undefined\n ? {}\n : { getToken: options.getConnectionToken }),\n });\n\n this.client.on(\"connected\", () => this.emit(\"connected\"));\n this.client.on(\"disconnected\", (ctx) => this.emit(\"disconnected\", ctx?.reason));\n this.client.on(\"error\", (ctx) =>\n this.emit(\"error\", new RelyvoError(\"transport_error\", String(ctx?.error ?? \"connection error\"))),\n );\n }\n\n /** Opens the socket and subscribes to the conversation and typing channels. */\n async connect(): Promise<void> {\n const connected = new Promise<void>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new RelyvoError(\"connect_timeout\", \"the realtime connection timed out\")),\n this.connectTimeoutMs,\n );\n\n this.handlers.connected.add(() => {\n clearTimeout(timer);\n resolve();\n });\n this.handlers.error.add((error) => {\n clearTimeout(timer);\n reject(error);\n });\n });\n\n this.client.connect();\n await connected;\n\n this.conversationSubscription = this.client.newSubscription(\n this.credentials.channel,\n this.subscriptionTokenOptions(this.credentials.channel, this.credentials.subscriptionToken),\n );\n this.conversationSubscription.on(\"publication\", (ctx) => this.handlePublication(ctx?.data));\n await this.conversationSubscription.subscribe();\n\n if (this.credentials.typingChannel && this.credentials.typingSubscriptionToken) {\n this.typingSubscription = this.client.newSubscription(\n this.credentials.typingChannel,\n this.subscriptionTokenOptions(\n this.credentials.typingChannel,\n this.credentials.typingSubscriptionToken,\n ),\n );\n this.typingSubscription.on(\"publication\", (ctx) => this.handleTyping(ctx?.data));\n await this.typingSubscription.subscribe();\n }\n }\n\n on<K extends keyof ChatEventHandler>(event: K, handler: ChatEventHandler[K]): () => void {\n this.handlers[event].add(handler);\n return () => this.handlers[event].delete(handler);\n }\n\n /** Sends a message. The same clientMessageId returns the original message. */\n async send(input: {\n content: string;\n clientMessageId: string;\n type?: string;\n metadata?: Record<string, unknown>;\n }): Promise<{ message: ChatMessage; created: boolean }> {\n return this.rpc(\"chat.send\", {\n channel_id: this.channelId,\n client_message_id: input.clientMessageId,\n content: input.content,\n type: input.type ?? \"text\",\n ...(input.metadata === undefined ? {} : { metadata: input.metadata }),\n });\n }\n\n /** Reads history backwards from a cursor, or forwards with `direction: \"after\"`. */\n history(input: { before?: string; limit?: number; direction?: \"before\" | \"after\" } = {}): Promise<ChatMessagePage> {\n return this.rpc(\"chat.history\", {\n channel_id: this.channelId,\n ...(input.before === undefined ? {} : { cursor: input.before }),\n ...(input.limit === undefined ? {} : { limit: input.limit }),\n ...(input.direction === undefined ? {} : { direction: input.direction }),\n });\n }\n\n /** Marks the channel read up to a message. */\n async markRead(messageId: string): Promise<void> {\n await this.rpc(\"chat.read\", { channel_id: this.channelId, message_id: messageId });\n }\n\n /** Lists the channels this user belongs to. */\n channels(input: { cursor?: string; limit?: number } = {}): Promise<ChatChannel[]> {\n return this.rpc<{ items: ChatChannel[] }>(\"chat.channels\", input).then((page) => page.items ?? []);\n }\n\n disconnect(): void {\n this.conversationSubscription?.unsubscribe();\n this.typingSubscription?.unsubscribe();\n this.client.disconnect();\n }\n\n /** Channel id extracted from the subscription channel name. */\n private get channelId(): string {\n const parts = this.credentials.channel.split(\":\");\n const id = parts[parts.length - 1];\n if (!id) {\n throw new RelyvoError(\"invalid_credentials\", \"the channel name does not contain an id\");\n }\n return id;\n }\n\n private subscriptionTokenOptions(\n channel: string,\n token: string,\n ): SubscriptionTokenOptions {\n return {\n token,\n ...(this.getSubscriptionToken === undefined\n ? {}\n : { getToken: () => this.getSubscriptionToken!(channel) }),\n };\n }\n\n private async rpc<T>(method: string, data: unknown): Promise<T> {\n let response: { data: unknown };\n try {\n response = await this.client.rpc(method, data);\n } catch (cause) {\n throw new RelyvoError(\"transport_error\", `rpc ${method} failed`, cause);\n }\n\n const envelope = response?.data as RpcEnvelope<T> | undefined;\n if (!envelope || envelope.ok !== true) {\n throw new RelyvoError(\n envelope?.error?.code ?? \"rpc_failed\",\n envelope?.error?.message ?? `rpc ${method} failed`,\n );\n }\n\n return envelope.data as T;\n }\n\n private handlePublication(payload: any): void {\n if (!payload || payload.type !== \"chat.message.created\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.message) return;\n\n const event: MessageEvent = {\n message: data.message,\n channelId: data.channel_id,\n senderId: data.sender?.id ?? data.message.sender_id,\n senderName: data.sender?.display_name,\n };\n this.emit(\"message\", event);\n }\n\n private handleTyping(payload: any): void {\n if (!payload || payload.type !== \"chat.typing\") return;\n\n const data = payload.data ?? {};\n if (data.channel_id !== this.channelId || !data.user_id) return;\n\n const event: TypingEvent = {\n channel_id: data.channel_id,\n user_id: data.user_id,\n display_name: data.display_name,\n };\n this.emit(\"typing\", event);\n }\n\n private emit<K extends keyof ChatEventHandler>(event: K, ...args: Parameters<ChatEventHandler[K]>): void {\n for (const handler of this.handlers[event]) {\n (handler as (...values: unknown[]) => void)(...args);\n }\n }\n}\n","/** One error type for every failure this SDK produces. */\nexport class RelyvoError extends Error {\n /** Stable machine code. RPC failures carry the chat service's code. */\n readonly code: string;\n /** Transport-level cause, when there is one. */\n readonly cause?: unknown;\n\n constructor(code: string, message: string, cause?: unknown) {\n super(message, cause === undefined ? undefined : { cause });\n this.name = \"RelyvoError\";\n this.code = code;\n this.cause = cause;\n }\n}\n\nexport function isRelyvoError(error: unknown): error is RelyvoError {\n return error instanceof RelyvoError;\n}\n","import { Room, RoomEvent } from \"livekit-client\";\n\nimport { RelyvoError } from \"./errors.js\";\nimport type { CallCredentials, CallEvent } from \"./types.js\";\n\n/** The subset of the LiveKit Room this SDK depends on. */\nexport interface RoomLike {\n connect(url: string, token: string): Promise<void>;\n disconnect(): Promise<void> | void;\n on(event: string, handler: (...args: any[]) => void): unknown;\n localParticipant: {\n setCameraEnabled(enabled: boolean): Promise<void>;\n setMicrophoneEnabled(enabled: boolean): Promise<void>;\n setScreenShareEnabled(enabled: boolean): Promise<void>;\n };\n}\n\nexport type RoomFactory = () => RoomLike;\n\nconst defaultFactory: RoomFactory = () =>\n new Room({ adaptiveStream: true, dynacast: true }) as unknown as RoomLike;\n\n/** LiveKit event names, mapped so callers do not import the media library. */\nconst EVENT_NAMES: Record<CallEvent, RoomEvent> = {\n connected: RoomEvent.Connected,\n disconnected: RoomEvent.Disconnected,\n participantConnected: RoomEvent.ParticipantConnected,\n participantDisconnected: RoomEvent.ParticipantDisconnected,\n trackSubscribed: RoomEvent.TrackSubscribed,\n trackUnsubscribed: RoomEvent.TrackUnsubscribed,\n connectionQualityChanged: RoomEvent.ConnectionQualityChanged,\n mediaDevicesError: RoomEvent.MediaDevicesError,\n};\n\nexport interface RelyvoCallOptions {\n /** Injectable room, used by tests. */\n createRoom?: RoomFactory;\n}\n\n/**\n * One video or voice call.\n *\n * The participant token is minted by your backend with the Relyvo server SDK; the\n * grant is join-only and already scoped to the room, so this class cannot ask for\n * more than that token allows.\n */\nexport class RelyvoCall {\n readonly room: RoomLike;\n private connected = false;\n\n private constructor(room: RoomLike) {\n this.room = room;\n }\n\n /** Connects to the room the token was minted for. */\n static async connect(\n credentials: CallCredentials,\n options: RelyvoCallOptions = {},\n ): Promise<RelyvoCall> {\n if (!credentials.serverUrl || !credentials.token) {\n throw new RelyvoError(\"invalid_credentials\", \"serverUrl and token are required\");\n }\n\n const call = new RelyvoCall((options.createRoom ?? defaultFactory)());\n\n try {\n await call.room.connect(credentials.serverUrl, credentials.token);\n } catch (cause) {\n throw new RelyvoError(\"connect_failed\", \"the media connection could not be established\", cause);\n }\n\n call.connected = true;\n return call;\n }\n\n /** Publishes or stops the camera. */\n async enableCamera(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setCameraEnabled(enabled);\n }\n\n /** Publishes or stops the microphone. */\n async enableMicrophone(enabled = true): Promise<void> {\n this.requireConnected();\n await this.room.localParticipant.setMicrophoneEnabled(enabled);\n }\n\n /** Starts or stops screen sharing where the browser supports it. */\n async shareScreen(enabled = true): Promise<void> {\n this.requireConnected();\n if (enabled && typeof navigator !== \"undefined\" && !navigator.mediaDevices?.getDisplayMedia) {\n throw new RelyvoError(\"unsupported\", \"this browser does not support screen sharing\");\n }\n await this.room.localParticipant.setScreenShareEnabled(enabled);\n }\n\n /** Subscribes to a room event. Returns an unsubscribe function. */\n on(event: CallEvent, handler: (...args: any[]) => void): () => void {\n const name = EVENT_NAMES[event];\n this.room.on(name, handler);\n return () => {\n const target = this.room as unknown as {\n off?: (event: string, callback: (...args: any[]) => void) => void;\n };\n target.off?.(name, handler);\n };\n }\n\n /** Leaves the room and releases the local devices. */\n disconnect(): void {\n if (!this.connected) return;\n this.connected = false;\n void this.room.disconnect();\n }\n\n private requireConnected(): void {\n if (!this.connected) {\n throw new RelyvoError(\"not_connected\", \"the call is not connected\");\n }\n }\n}\n","/**\n * Relyvo browser client SDK.\n *\n * The browser holds only short-lived credentials minted by your backend: a\n * Centrifugo connection token for chat and a LiveKit participant token for\n * media. Chat operations travel as RPC frames on the WebSocket, so this SDK\n * never calls Relyvo REST and never needs a CORS allowance.\n *\n * @example\n * ```ts\n * const chat = new RelyvoChat(credentials);\n * await chat.connect();\n * chat.on(\"message\", ({ message }) => console.log(message.content));\n * await chat.send({ content: \"hello\", clientMessageId: crypto.randomUUID() });\n * ```\n */\n\nexport { RelyvoChat } from \"./chat.js\";\nexport type {\n CentrifugeClient,\n CentrifugeFactory,\n CentrifugeSubscription,\n RelyvoChatOptions,\n} from \"./chat.js\";\nexport { RelyvoError, isRelyvoError } from \"./errors.js\";\nexport { RelyvoCall } from \"./video.js\";\nexport type { RelyvoCallOptions, RoomFactory, RoomLike } from \"./video.js\";\nexport type {\n CallCredentials,\n CallEvent,\n ChatChannel,\n ChatCredentials,\n ChatEventHandler,\n ChatMessage,\n ChatMessagePage,\n ChatMessageType,\n MessageEvent,\n TypingEvent,\n} from \"./types.js\";\n\nexport const VERSION = \"0.1.0\";\n"],"mappings":"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","names":["defaultFactory"]}
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@relyvolabs/web-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Browser client SDK for Relyvo: realtime chat over Centrifugo and video calls over LiveKit, with no REST and no API key in the browser.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "relyvo",
27
+ "rtc",
28
+ "chat",
29
+ "sdk",
30
+ "client",
31
+ "webrtc"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/relyvolabs/relyvo-web-sdk.git"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest run",
41
+ "pack:check": "npm pack --dry-run"
42
+ },
43
+ "peerDependencies": {
44
+ "centrifuge": ">=5",
45
+ "livekit-client": ">=2"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.10.2",
49
+ "centrifuge": "5.8.0",
50
+ "livekit-client": "2.23.0",
51
+ "tsup": "^8.3.5",
52
+ "typescript": "^5.7.2",
53
+ "vitest": "^2.1.8"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "pnpm": {
59
+ "onlyBuiltDependencies": [
60
+ "esbuild"
61
+ ]
62
+ }
63
+ }