@protobase/client 0.62.5 → 0.63.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.
@@ -0,0 +1,24 @@
1
+ import { guestCallsPath } from "@protobase/schema/internal";
2
+ //#region src/call-guests.ts
3
+ /**
4
+ * Whether the calls of the organization the signed-in user works in take guests (`/api/calls/guests/organization`):
5
+ * `allowed` for any member, `setAllowed` for its owner or an admin. Turning them off revokes every guest link of its
6
+ * calls and takes their guests out at once.
7
+ */
8
+ const callGuestsClient = (send) => ({
9
+ allowed: async () => (await (await send({
10
+ method: "GET",
11
+ path: guestCallsPath("", "organization"),
12
+ system: true
13
+ })).json()).allowed,
14
+ setAllowed: async (allowed) => {
15
+ await send({
16
+ method: "PUT",
17
+ path: guestCallsPath("", "organization"),
18
+ system: true,
19
+ body: { allowed }
20
+ });
21
+ }
22
+ });
23
+ //#endregion
24
+ export { callGuestsClient };
@@ -0,0 +1,34 @@
1
+ import { CallGuestState, CallTrackSource } from "@protobase/schema/internal";
2
+ //#region src/calls/guest-lobby.d.ts
3
+ /** A guest's wait in the lobby, as the server tells it: once let in, the call and what the guest may send. */
4
+ export type GuestWait = {
5
+ state: CallGuestState;
6
+ call?: string;
7
+ publish?: CallTrackSource[];
8
+ };
9
+ /** Why the lobby did not answer: no live link, too many tries for now, or a name it does not take. */
10
+ export declare class LobbyRefused extends Error {
11
+ readonly status: number;
12
+ constructor(status: number, message: string);
13
+ }
14
+ export type GuestLobbyOptions = {
15
+ /** The API's system path on the page's origin, `/api` by default. */
16
+ systemPath?: string;
17
+ fetch?: typeof fetch;
18
+ };
19
+ /**
20
+ * The lobby a guest of a call waits in, with no account: `enter` with the link's token and the guest's name gives the
21
+ * guest's own token, which `wait` and `leave` send, and which `joinCall` takes as `guest` at `socketUrl` once a member
22
+ * let them in. No cookie is sent or kept.
23
+ */
24
+ export declare const guestLobby: ({ systemPath, fetch: fetchOf }?: GuestLobbyOptions) => {
25
+ /** The guest's call socket, for `joinCall`'s `url`. */
26
+ socketUrl: string;
27
+ enter: (link: string, name: string) => Promise<{
28
+ guest: string;
29
+ token: string;
30
+ }>;
31
+ wait: (token: string) => Promise<GuestWait>;
32
+ leave: (token: string) => Promise<void>;
33
+ };
34
+ //#endregion
@@ -0,0 +1,53 @@
1
+ import { guestCallsPath } from "@protobase/schema/internal";
2
+ //#region src/calls/guest-lobby.ts
3
+ /** Why the lobby did not answer: no live link, too many tries for now, or a name it does not take. */
4
+ var LobbyRefused = class extends Error {
5
+ status;
6
+ constructor(status, message) {
7
+ super(message);
8
+ this.status = status;
9
+ this.name = "LobbyRefused";
10
+ }
11
+ };
12
+ /**
13
+ * The lobby a guest of a call waits in, with no account: `enter` with the link's token and the guest's name gives the
14
+ * guest's own token, which `wait` and `leave` send, and which `joinCall` takes as `guest` at `socketUrl` once a member
15
+ * let them in. No cookie is sent or kept.
16
+ */
17
+ const guestLobby = ({ systemPath = "/api", fetch: fetchOf = globalThis.fetch } = {}) => {
18
+ const lobby = guestCallsPath(systemPath, "lobby");
19
+ const ask = async (init) => {
20
+ const response = await fetchOf(lobby, {
21
+ ...init,
22
+ credentials: "omit",
23
+ cache: "no-store"
24
+ });
25
+ if (!response.ok) throw new LobbyRefused(response.status, (await response.json().catch(() => void 0))?.detail ?? response.statusText);
26
+ return response;
27
+ };
28
+ const bearer = (token) => ({ authorization: `Bearer ${token}` });
29
+ return {
30
+ /** The guest's call socket, for `joinCall`'s `url`. */
31
+ socketUrl: guestCallsPath(systemPath, "socket"),
32
+ enter: async (link, name) => await (await ask({
33
+ method: "POST",
34
+ headers: { "content-type": "application/json" },
35
+ body: JSON.stringify({
36
+ link,
37
+ name
38
+ })
39
+ })).json(),
40
+ wait: async (token) => await (await ask({
41
+ method: "GET",
42
+ headers: bearer(token)
43
+ })).json(),
44
+ leave: async (token) => {
45
+ await ask({
46
+ method: "DELETE",
47
+ headers: bearer(token)
48
+ });
49
+ }
50
+ };
51
+ };
52
+ //#endregion
53
+ export { LobbyRefused, guestLobby };
@@ -0,0 +1,25 @@
1
+ import { CallGuestLink, CallGuestRefusal, CallTrackSource } from "@protobase/schema/internal";
2
+ //#region src/calls/guest-requests.d.ts
3
+ /** A guest request the server did not do, with why: see `CallGuestRefusal`. */
4
+ export declare class GuestRefused extends Error {
5
+ readonly reason: CallGuestRefusal;
6
+ constructor(reason: CallGuestRefusal);
7
+ }
8
+ /** What a member who invites does with the call's guests. Each rejects with `GuestRefused` when the server did not. */
9
+ export type CallGuests = {
10
+ /** A new link to the call that lets a guest send these sources, until `expiresAt` or the call's end; its token once. */
11
+ link: (options: {
12
+ publish: CallTrackSource[];
13
+ expiresAt?: Date;
14
+ }) => Promise<{
15
+ link: CallGuestLink;
16
+ token: string;
17
+ }>;
18
+ /** Revokes a link: its guests are out of the call, and nobody else waits on it. */
19
+ revoke: (link: string) => Promise<void>;
20
+ admit: (guest: string) => Promise<void>;
21
+ deny: (guest: string) => Promise<void>;
22
+ /** Takes a guest out of the call, for good. */
23
+ remove: (guest: string) => Promise<void>;
24
+ };
25
+ //#endregion
@@ -0,0 +1,68 @@
1
+ //#region src/calls/guest-requests.ts
2
+ /** A guest request the server did not do, with why: see `CallGuestRefusal`. */
3
+ var GuestRefused = class extends Error {
4
+ reason;
5
+ constructor(reason) {
6
+ super(`The guest request was refused: ${reason}`);
7
+ this.reason = reason;
8
+ this.name = "GuestRefused";
9
+ }
10
+ };
11
+ /**
12
+ * The member's guest requests over the call's socket: the server answers each once, in order, so the answers are paired
13
+ * with the requests in the order they were sent. A request whose socket closed before its answer rejects.
14
+ */
15
+ const guestRequests = (send) => {
16
+ const pending = [];
17
+ const ask = (message) => new Promise((resolve, reject) => {
18
+ pending.push({
19
+ resolve,
20
+ reject
21
+ });
22
+ send(message);
23
+ });
24
+ return {
25
+ guests: {
26
+ link: ({ publish, expiresAt }) => ask({
27
+ type: "guest-link",
28
+ publish,
29
+ ...expiresAt && { expires_at: expiresAt.toISOString() }
30
+ }),
31
+ revoke: (link) => ask({
32
+ type: "guest-link-revoke",
33
+ link
34
+ }),
35
+ admit: (guest) => ask({
36
+ type: "guest-decide",
37
+ guest,
38
+ admit: true
39
+ }),
40
+ deny: (guest) => ask({
41
+ type: "guest-decide",
42
+ guest,
43
+ admit: false
44
+ }),
45
+ remove: (guest) => ask({
46
+ type: "guest-remove",
47
+ guest
48
+ })
49
+ },
50
+ /** Whether the message answered a request, which it then settles. */
51
+ receive: (message) => {
52
+ if (message.type !== "guest-link" && message.type !== "guest-done" && message.type !== "guest-refused") return false;
53
+ const request = pending.shift();
54
+ if (message.type === "guest-refused") request?.reject(new GuestRefused(message.reason));
55
+ else request?.resolve(message.type === "guest-link" ? {
56
+ link: message.link,
57
+ token: message.token
58
+ } : void 0);
59
+ return true;
60
+ },
61
+ /** The socket closed: the requests on it are not answered. */
62
+ lost: () => {
63
+ for (const request of pending.splice(0)) request.reject(/* @__PURE__ */ new Error("The link to the call was lost before the server answered"));
64
+ }
65
+ };
66
+ };
67
+ //#endregion
68
+ export { GuestRefused, guestRequests };
@@ -1,6 +1,8 @@
1
1
  import { CallConnection } from "./connection-stats.js";
2
+ import { CallGuests, GuestRefused } from "./guest-requests.js";
2
3
  import { CallTrack, PublishOptions } from "./media-link.js";
3
4
  import { Call, CallState, JoinCallOptions, joinCall } from "./join-call.js";
4
5
  import { PublishRefused } from "./publish-refused.js";
5
- import { CallLeftReason, CallMode, CallRosterMember, CallTrackSource } from "@protobase/schema/internal";
6
- export { type Call, type CallConnection, type CallLeftReason, type CallMode, type CallRosterMember, type CallState, type CallTrack, type CallTrackSource, type JoinCallOptions, type PublishOptions, PublishRefused, joinCall };
6
+ import { GuestLobbyOptions, GuestWait, LobbyRefused, guestLobby } from "./guest-lobby.js";
7
+ import { CallGuestLink, CallGuestRefusal, CallGuestState, CallLeftReason, CallLobbyGuest, CallMode, CallRosterMember, CallTrackSource } from "@protobase/schema/internal";
8
+ export { type Call, type CallConnection, type CallGuestLink, type CallGuestRefusal, type CallGuestState, type CallGuests, type CallLeftReason, type CallLobbyGuest, type CallMode, type CallRosterMember, type CallState, type CallTrack, type CallTrackSource, type GuestLobbyOptions, GuestRefused, type GuestWait, type JoinCallOptions, LobbyRefused, type PublishOptions, PublishRefused, guestLobby, joinCall };
@@ -1,3 +1,5 @@
1
+ import { GuestRefused } from "./guest-requests.js";
1
2
  import { PublishRefused } from "./publish-refused.js";
2
3
  import { joinCall } from "./join-call.js";
3
- export { PublishRefused, joinCall };
4
+ import { LobbyRefused, guestLobby } from "./guest-lobby.js";
5
+ export { GuestRefused, LobbyRefused, PublishRefused, guestLobby, joinCall };
@@ -1,6 +1,7 @@
1
1
  import { CallConnection } from "./connection-stats.js";
2
+ import { CallGuests } from "./guest-requests.js";
2
3
  import { CallTrack, PublishOptions } from "./media-link.js";
3
- import { CallLeftReason, CallRosterMember, CallTrackSource } from "@protobase/schema/internal";
4
+ import { CallGuestLink, CallLeftReason, CallLobbyGuest, CallRosterMember, CallTrackSource } from "@protobase/schema/internal";
4
5
  //#region src/calls/join-call.d.ts
5
6
  /** Where a call stands: `resuming` while the link to Protobase is restored, with media still flowing. */
6
7
  export type CallState = {
@@ -20,6 +21,13 @@ export type JoinCallOptions = {
20
21
  * page's origin, or an absolute `http(s)` or `ws(s)` URL. The browser sends its session cookie with it.
21
22
  */
22
23
  url: string;
24
+ /** A guest's token from `guestLobby`, with the lobby's `socketUrl` as `url`: the guest joins as themselves, whom no cookie names. */
25
+ guest?: string;
26
+ /** With a member who may invite: who waits in the call's lobby, and its guest links, whenever that changes. */
27
+ onLobby?: (lobby: {
28
+ guests: CallLobbyGuest[];
29
+ links: CallGuestLink[];
30
+ }) => void;
23
31
  /** Every track of other members the member hears, whenever one comes, goes or its media arrives. */
24
32
  onTracks?: (tracks: CallTrack[]) => void;
25
33
  /** Everyone in the call, the member too, in the order they came, whenever that changes. */
@@ -62,6 +70,8 @@ export type Call = {
62
70
  tracks: () => CallTrack[];
63
71
  /** How each of the member's connections fares now: to each other member, or to the media server. */
64
72
  connections: () => Promise<CallConnection[]>;
73
+ /** With a member who may invite: guest links, and letting the lobby's guests in. */
74
+ guests: CallGuests;
65
75
  };
66
76
  /**
67
77
  * Joins a call through the app's function socket and resolves once the member is in it. Media flows over plain
@@ -1,3 +1,4 @@
1
+ import { guestRequests } from "./guest-requests.js";
1
2
  import { peerLink } from "./peer-link.js";
2
3
  import { sfuLink } from "./sfu-link.js";
3
4
  //#region src/calls/join-call.ts
@@ -45,6 +46,8 @@ const joinCall = (options) => {
45
46
  const send = (message) => {
46
47
  if (socket.readyState === WebSocketOf.OPEN) socket.send(JSON.stringify(message));
47
48
  };
49
+ const requests = guestRequests(send);
50
+ const guest = options.guest !== void 0 ? { guest: options.guest } : {};
48
51
  const setState = (state) => options.onState?.(state);
49
52
  const end = (reason, error) => {
50
53
  if (ended) return;
@@ -103,7 +106,15 @@ const joinCall = (options) => {
103
106
  if (message.reason === "rejoin") rejoin = true;
104
107
  else end(message.reason);
105
108
  return;
106
- default: await link?.receive(message);
109
+ case "lobby":
110
+ options.onLobby?.({
111
+ guests: message.guests,
112
+ links: message.links
113
+ });
114
+ return;
115
+ default:
116
+ if (requests.receive(message)) return;
117
+ await link?.receive(message);
107
118
  }
108
119
  };
109
120
  const connect = (mode) => {
@@ -117,8 +128,12 @@ const joinCall = (options) => {
117
128
  send(resume ? {
118
129
  type: "resume",
119
130
  call: callId,
120
- ...resume
121
- } : { type: "join" });
131
+ ...resume,
132
+ ...guest
133
+ } : {
134
+ type: "join",
135
+ ...guest
136
+ });
122
137
  });
123
138
  };
124
139
  socket.onmessage = (event) => {
@@ -126,6 +141,7 @@ const joinCall = (options) => {
126
141
  chain = chain.then(() => handle(message)).catch((error) => end("ended", error));
127
142
  };
128
143
  socket.onclose = (event) => {
144
+ requests.lost();
129
145
  chain = chain.then(() => closed(mode, event.code));
130
146
  };
131
147
  };
@@ -182,7 +198,8 @@ const joinCall = (options) => {
182
198
  end("ended");
183
199
  },
184
200
  tracks: () => remote,
185
- connections: async () => link ? link.connections() : []
201
+ connections: async () => link ? link.connections() : [],
202
+ guests: requests.guests
186
203
  };
187
204
  setState({ state: "connecting" });
188
205
  connect("join");
package/dist/client.d.ts CHANGED
@@ -125,6 +125,10 @@ export declare const createClient: (options?: ClientOptions) => {
125
125
  list: () => Promise<import("@protobase/schema/internal").CallRing[]>;
126
126
  decline: (call: string) => Promise<void>;
127
127
  };
128
+ callGuests: {
129
+ allowed: () => Promise<boolean>;
130
+ setAllowed: (allowed: boolean) => Promise<void>;
131
+ };
128
132
  };
129
133
  export type Client = ReturnType<typeof createClient>;
130
134
  //#endregion
package/dist/client.js CHANGED
@@ -10,6 +10,7 @@ import { statusClient } from "./status.js";
10
10
  import { notificationHistoryClient } from "./notification-history.js";
11
11
  import { recordCallsClient } from "./record-calls.js";
12
12
  import { ringsClient } from "./rings.js";
13
+ import { callGuestsClient } from "./call-guests.js";
13
14
  import { encodeKey } from "@protobase/schema/internal";
14
15
  //#region src/client.ts
15
16
  const nameOf = (resource) => typeof resource === "string" ? resource : resource.toModel().name;
@@ -475,7 +476,8 @@ const createClient = (options = {}) => {
475
476
  status: statusClient(send),
476
477
  calls,
477
478
  watchCalls,
478
- rings: ringsClient(send)
479
+ rings: ringsClient(send),
480
+ callGuests: callGuestsClient(send)
479
481
  };
480
482
  };
481
483
  //#endregion
@@ -45,6 +45,7 @@ const routedClient = (own, sourceOf) => {
45
45
  notifications: own.notifications,
46
46
  status: own.status,
47
47
  rings: own.rings,
48
+ callGuests: own.callGuests,
48
49
  calls: (resource, key) => to(resource).calls(resource, key),
49
50
  watchCalls: (resource, key, handlers) => to(resource).watchCalls(resource, key, handlers)
50
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/client",
3
- "version": "0.62.5",
3
+ "version": "0.63.0",
4
4
  "description": "Typed fetch client for the Protobase REST API.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@better-auth/passkey": "^1.7.12",
34
- "@protobase/layout": "0.62.5",
35
- "@protobase/schema": "0.62.5",
34
+ "@protobase/layout": "0.63.0",
35
+ "@protobase/schema": "0.63.0",
36
36
  "@simplewebauthn/browser": "^13.3.0",
37
37
  "better-auth": "^1.7.12"
38
38
  }