@protobase/server 0.58.4 → 0.58.6
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/dist/calls/call-member.d.ts +14 -0
- package/dist/calls/calls-access.d.ts +13 -0
- package/dist/calls/calls-access.js +25 -0
- package/dist/calls/calls-options.d.ts +33 -0
- package/dist/calls/calls-options.js +21 -0
- package/dist/calls/client-message.js +106 -0
- package/dist/calls/livekit/join-url.js +33 -0
- package/dist/calls/livekit/livekit-session.js +323 -0
- package/dist/calls/livekit/livekit-token.js +26 -0
- package/dist/calls/livekit/signal-socket.js +60 -0
- package/dist/calls/livekit/track-mapping.js +36 -0
- package/dist/calls/media-backend.js +5 -0
- package/dist/calls/member-relay.js +56 -0
- package/dist/calls/peer/peer-backend.js +131 -0
- package/dist/create-admin.d.ts +4 -1
- package/dist/create-admin.js +2 -1
- package/dist/functions/define-function.d.ts +3 -0
- package/dist/functions/routes.js +7 -3
- package/dist/index.d.ts +4 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/package.js +1 -1
- package/dist/webhooks/attempt.js +2 -1
- package/package.json +5 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CallRemoteTrack, CallTrackSource } from "@protobase/schema/internal";
|
|
2
|
+
//#region src/calls/call-member.d.ts
|
|
3
|
+
/** One member admitted to one call, with what they may send and hear. */
|
|
4
|
+
export type CallMember = {
|
|
5
|
+
/** The call, as the app names it: members with the same call meet. */
|
|
6
|
+
call: string;
|
|
7
|
+
/** Who joins, unique in the call, such as the user's id: the media server knows them by nothing else. */
|
|
8
|
+
member: string;
|
|
9
|
+
/** Whether the member may send media, or only these sources; nothing when `false`. */
|
|
10
|
+
publish: boolean | CallTrackSource[];
|
|
11
|
+
/** Which of the other members' tracks the member hears; every one by default. */
|
|
12
|
+
hears?: (track: CallRemoteTrack) => boolean;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SocketEvents } from "../functions/function-socket.js";
|
|
2
|
+
import { CallMember } from "./call-member.js";
|
|
3
|
+
//#region src/calls/calls-access.d.ts
|
|
4
|
+
/** What a function admits members to calls with. */
|
|
5
|
+
export type CallsAccess = {
|
|
6
|
+
/**
|
|
7
|
+
* The member's media session in the call, as the events of the socket the function accepts for them: return
|
|
8
|
+
* `acceptWebSocket(await calls.join(member))` once the function decided the caller may join. The browser's
|
|
9
|
+
* `joinCall` from `@protobase/client/calls` speaks to it. Rejects when the app has no `calls` configured.
|
|
10
|
+
*/
|
|
11
|
+
join: (member: CallMember) => Promise<SocketEvents>;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/calls/calls-access.ts
|
|
2
|
+
const checkMember = ({ call, member }) => {
|
|
3
|
+
if (typeof call !== "string" || call.length === 0) throw new TypeError("calls.join needs the call, a non-empty string");
|
|
4
|
+
if (typeof member !== "string" || member.length === 0) throw new TypeError("calls.join needs the member, a non-empty string");
|
|
5
|
+
};
|
|
6
|
+
const loadBackend = async (settings) => {
|
|
7
|
+
if (settings.kind === "livekit") return (await import("./livekit/livekit-session.js")).liveKitBackend(settings);
|
|
8
|
+
return (await import("./peer/peer-backend.js")).peerBackend(settings);
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Calls as a function sees them: each member's socket goes through the relay to the configured plugin, made once, at
|
|
12
|
+
* the first join, so an app without calls loads none of their code.
|
|
13
|
+
*/
|
|
14
|
+
const callsAccess = (options) => {
|
|
15
|
+
let backend;
|
|
16
|
+
return { join: async (member) => {
|
|
17
|
+
if (!options) throw new Error("This app has no calls: configure calls: { backend } in protobase.config.ts");
|
|
18
|
+
checkMember(member);
|
|
19
|
+
backend ??= loadBackend(options.backend);
|
|
20
|
+
const [{ memberRelay }, plugin] = await Promise.all([import("./member-relay.js"), backend]);
|
|
21
|
+
return memberRelay(plugin, member);
|
|
22
|
+
} };
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { callsAccess };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/calls/calls-options.d.ts
|
|
2
|
+
/** A LiveKit server, self-hosted or LiveKit Cloud, that carries the calls' media. */
|
|
3
|
+
export type LiveKitBackend = {
|
|
4
|
+
kind: 'livekit';
|
|
5
|
+
/** Its address, such as `wss://livekit.example.com` or `ws://127.0.0.1:7880`. */
|
|
6
|
+
url: string;
|
|
7
|
+
/** The API key and secret Protobase signs each member's LiveKit token with. */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
apiSecret: string;
|
|
10
|
+
};
|
|
11
|
+
/** An ICE server the members' browsers reach each other through, such as a STUN or TURN server. */
|
|
12
|
+
export type CallIceServer = {
|
|
13
|
+
urls: string[];
|
|
14
|
+
username?: string;
|
|
15
|
+
credential?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* No media server: each member's browser sends its media to every other member's, and Protobase passes their offers,
|
|
19
|
+
* answers and candidates between them. For small calls; members meet only on the same process.
|
|
20
|
+
*/
|
|
21
|
+
export type PeerBackend = {
|
|
22
|
+
kind: 'peer';
|
|
23
|
+
iceServers?: CallIceServer[];
|
|
24
|
+
};
|
|
25
|
+
/** The media plugin a call's members send and receive their media through. */
|
|
26
|
+
export type CallBackend = LiveKitBackend | PeerBackend;
|
|
27
|
+
/** Calls: the media plugin behind them. Without `calls`, an app loads none of the calls code. */
|
|
28
|
+
export type CallsOptions = {
|
|
29
|
+
backend: CallBackend;
|
|
30
|
+
};
|
|
31
|
+
/** The options, or an error naming what is wrong with them: the backend's kind and each of its settings. */
|
|
32
|
+
export declare const checkCallsOptions: (options: CallsOptions) => CallsOptions;
|
|
33
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/calls/calls-options.ts
|
|
2
|
+
const nonEmpty = (value) => typeof value === "string" && value.length > 0;
|
|
3
|
+
/** The options, or an error naming what is wrong with them: the backend's kind and each of its settings. */
|
|
4
|
+
const checkCallsOptions = (options) => {
|
|
5
|
+
const backend = options?.backend;
|
|
6
|
+
if (backend?.kind === "peer") {
|
|
7
|
+
const servers = backend.iceServers;
|
|
8
|
+
if (servers !== void 0 && (!Array.isArray(servers) || servers.some((server) => !Array.isArray(server?.urls) || !server.urls.every(nonEmpty)))) throw new Error("calls.backend.iceServers must be a list of { urls: string[], username?, credential? }");
|
|
9
|
+
return options;
|
|
10
|
+
}
|
|
11
|
+
if (backend?.kind !== "livekit") throw new Error(`calls.backend.kind must be 'livekit' or 'peer', not ${JSON.stringify(backend?.kind)}`);
|
|
12
|
+
for (const key of [
|
|
13
|
+
"url",
|
|
14
|
+
"apiKey",
|
|
15
|
+
"apiSecret"
|
|
16
|
+
]) if (!nonEmpty(backend[key])) throw new Error(`calls.backend.${key} must be a non-empty string, such as from an environment variable`);
|
|
17
|
+
if (!/^(wss?|https?):\/\//.test(backend.url)) throw new Error(`calls.backend.url must start with wss://, ws://, https:// or http://, not ${backend.url}`);
|
|
18
|
+
return options;
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { checkCallsOptions };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/calls/client-message.ts
|
|
3
|
+
const id = z.string().min(1).max(256);
|
|
4
|
+
const sdp = z.string().min(1).max(262144);
|
|
5
|
+
const kind = z.enum(["audio", "video"]);
|
|
6
|
+
const size = z.number().int().min(0).max(16384);
|
|
7
|
+
const layer = z.strictObject({
|
|
8
|
+
quality: z.enum([
|
|
9
|
+
"low",
|
|
10
|
+
"medium",
|
|
11
|
+
"high"
|
|
12
|
+
]),
|
|
13
|
+
width: size,
|
|
14
|
+
height: size,
|
|
15
|
+
bitrate: z.number().int().min(0).max(1e8)
|
|
16
|
+
});
|
|
17
|
+
const source = z.enum([
|
|
18
|
+
"camera",
|
|
19
|
+
"microphone",
|
|
20
|
+
"screen",
|
|
21
|
+
"screen-audio"
|
|
22
|
+
]);
|
|
23
|
+
const track = z.strictObject({
|
|
24
|
+
cid: id,
|
|
25
|
+
kind,
|
|
26
|
+
source,
|
|
27
|
+
width: size.optional(),
|
|
28
|
+
height: size.optional(),
|
|
29
|
+
layers: z.array(layer).max(3).optional()
|
|
30
|
+
});
|
|
31
|
+
const candidate = z.strictObject({
|
|
32
|
+
candidate: z.string().max(4096),
|
|
33
|
+
sdpMid: z.string().max(64).nullable().optional(),
|
|
34
|
+
sdpMLineIndex: z.number().int().min(0).max(1024).nullable().optional(),
|
|
35
|
+
usernameFragment: z.string().max(256).nullable().optional()
|
|
36
|
+
});
|
|
37
|
+
const description = z.strictObject({
|
|
38
|
+
type: z.enum(["offer", "answer"]),
|
|
39
|
+
sdp
|
|
40
|
+
});
|
|
41
|
+
const message = z.discriminatedUnion("type", [
|
|
42
|
+
z.strictObject({ type: z.literal("join") }),
|
|
43
|
+
z.strictObject({
|
|
44
|
+
type: z.literal("resume"),
|
|
45
|
+
participant: id,
|
|
46
|
+
offer: sdp.optional(),
|
|
47
|
+
answer: sdp.optional(),
|
|
48
|
+
tracks: z.array(z.strictObject({
|
|
49
|
+
cid: id,
|
|
50
|
+
sid: id,
|
|
51
|
+
kind,
|
|
52
|
+
source
|
|
53
|
+
})).max(16)
|
|
54
|
+
}),
|
|
55
|
+
z.strictObject({
|
|
56
|
+
type: z.literal("publish"),
|
|
57
|
+
tracks: z.array(track).min(1).max(4)
|
|
58
|
+
}),
|
|
59
|
+
z.strictObject({
|
|
60
|
+
type: z.literal("offer"),
|
|
61
|
+
sdp,
|
|
62
|
+
id: z.number().int().min(0)
|
|
63
|
+
}),
|
|
64
|
+
z.strictObject({
|
|
65
|
+
type: z.literal("candidate"),
|
|
66
|
+
candidate
|
|
67
|
+
}),
|
|
68
|
+
z.strictObject({
|
|
69
|
+
type: z.literal("unpublish"),
|
|
70
|
+
sid: id
|
|
71
|
+
}),
|
|
72
|
+
z.strictObject({
|
|
73
|
+
type: z.literal("mute"),
|
|
74
|
+
sid: id,
|
|
75
|
+
muted: z.boolean()
|
|
76
|
+
}),
|
|
77
|
+
z.strictObject({
|
|
78
|
+
type: z.literal("signal"),
|
|
79
|
+
to: id,
|
|
80
|
+
description: description.optional(),
|
|
81
|
+
candidate: candidate.optional()
|
|
82
|
+
}),
|
|
83
|
+
z.strictObject({
|
|
84
|
+
type: z.literal("view"),
|
|
85
|
+
sids: z.array(id).min(1).max(256),
|
|
86
|
+
width: size,
|
|
87
|
+
height: size,
|
|
88
|
+
hidden: z.boolean().optional()
|
|
89
|
+
}),
|
|
90
|
+
z.strictObject({ type: z.literal("leave") })
|
|
91
|
+
]);
|
|
92
|
+
/** A browser's message, or `undefined` when it is no message of the call protocol. */
|
|
93
|
+
const parseClientMessage = (data) => {
|
|
94
|
+
if (typeof data !== "string") return void 0;
|
|
95
|
+
let json;
|
|
96
|
+
try {
|
|
97
|
+
json = JSON.parse(data);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof SyntaxError) return void 0;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
const parsed = message.safeParse(json);
|
|
103
|
+
return parsed.success ? parsed.data : void 0;
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
export { parseClientMessage };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ClientInfo, ClientInfo_SDK, ConnectionSettings, JoinRequest, WrappedJoinRequest, WrappedJoinRequest_Compression } from "@livekit/protocol";
|
|
2
|
+
/** The bridge as LiveKit's client info names it. */
|
|
3
|
+
const clientInfo = () => new ClientInfo({
|
|
4
|
+
sdk: ClientInfo_SDK.UNKNOWN,
|
|
5
|
+
protocol: 17,
|
|
6
|
+
version: "protobase-calls-1"
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* The single peer connection signal endpoint (`/rtc/v1`) with the member's token and join request. The request is
|
|
10
|
+
* standard base64 with `+` and `/` replaced and the `=` padding kept, which LiveKit's decoder needs.
|
|
11
|
+
*/
|
|
12
|
+
const joinUrl = (base, token, { resume }) => {
|
|
13
|
+
const join = new JoinRequest({
|
|
14
|
+
clientInfo: clientInfo(),
|
|
15
|
+
connectionSettings: new ConnectionSettings({
|
|
16
|
+
autoSubscribe: false,
|
|
17
|
+
adaptiveStream: false
|
|
18
|
+
}),
|
|
19
|
+
reconnect: resume !== void 0,
|
|
20
|
+
participantSid: resume?.participant ?? ""
|
|
21
|
+
});
|
|
22
|
+
const wrapped = new WrappedJoinRequest({
|
|
23
|
+
compression: WrappedJoinRequest_Compression.NONE,
|
|
24
|
+
joinRequest: join.toBinary()
|
|
25
|
+
});
|
|
26
|
+
const encoded = Buffer.from(wrapped.toBinary()).toString("base64").replaceAll("+", "-").replaceAll("/", "_");
|
|
27
|
+
const url = new URL("rtc/v1", `${base.replace(/\/+$/, "")}/`);
|
|
28
|
+
url.searchParams.set("access_token", token);
|
|
29
|
+
url.searchParams.set("join_request", encoded);
|
|
30
|
+
return url.href;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { joinUrl };
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { resumeCloseCode } from "../media-backend.js";
|
|
2
|
+
import { joinUrl } from "./join-url.js";
|
|
3
|
+
import { liveKitToken } from "./livekit-token.js";
|
|
4
|
+
import { openSignalSocket } from "./signal-socket.js";
|
|
5
|
+
import { grantSources, liveKitQuality, liveKitSource, remoteTracksOf } from "./track-mapping.js";
|
|
6
|
+
import { AddTrackRequest, DisconnectReason, LeaveRequest, LeaveRequest_Action, MuteTrackRequest, ParticipantInfo_State, RequestResponse_Reason, SessionDescription, SignalTarget, SyncState, TrackInfo, TrackPublishedResponse, TrackType, TrickleRequest, UpdateSubscription, UpdateTrackSettings, VideoLayer } from "@livekit/protocol";
|
|
7
|
+
//#region src/calls/livekit/livekit-session.ts
|
|
8
|
+
/** How long the media server has to accept a track before the publish is refused. */
|
|
9
|
+
const publishTimeoutMs = 1e4;
|
|
10
|
+
/** LiveKit's own ping defaults, for a resumed session, whose answer carries none. */
|
|
11
|
+
const resumePing = {
|
|
12
|
+
interval: 5,
|
|
13
|
+
timeout: 15
|
|
14
|
+
};
|
|
15
|
+
const leftReason = (leave) => {
|
|
16
|
+
if (leave.action === LeaveRequest_Action.RECONNECT) return "rejoin";
|
|
17
|
+
if (leave.reason === DisconnectReason.PARTICIPANT_REMOVED || leave.reason === DisconnectReason.DUPLICATE_IDENTITY) return "removed";
|
|
18
|
+
return "ended";
|
|
19
|
+
};
|
|
20
|
+
const syncState = (resume) => new SyncState({
|
|
21
|
+
offer: new SessionDescription({
|
|
22
|
+
type: "offer",
|
|
23
|
+
sdp: resume.offer
|
|
24
|
+
}),
|
|
25
|
+
answer: new SessionDescription({
|
|
26
|
+
type: "answer",
|
|
27
|
+
sdp: resume.answer
|
|
28
|
+
}),
|
|
29
|
+
publishTracks: resume.tracks.map((track) => new TrackPublishedResponse({
|
|
30
|
+
cid: track.cid,
|
|
31
|
+
track: new TrackInfo({
|
|
32
|
+
sid: track.sid,
|
|
33
|
+
type: track.kind === "audio" ? TrackType.AUDIO : TrackType.VIDEO
|
|
34
|
+
})
|
|
35
|
+
})),
|
|
36
|
+
subscription: new UpdateSubscription({
|
|
37
|
+
subscribe: true,
|
|
38
|
+
trackSids: []
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
const addTrackRequest = (track) => new AddTrackRequest({
|
|
42
|
+
cid: track.cid,
|
|
43
|
+
name: track.source,
|
|
44
|
+
type: track.kind === "audio" ? TrackType.AUDIO : TrackType.VIDEO,
|
|
45
|
+
source: liveKitSource(track.source),
|
|
46
|
+
width: track.width ?? 0,
|
|
47
|
+
height: track.height ?? 0,
|
|
48
|
+
layers: (track.layers ?? []).map((layer) => new VideoLayer({
|
|
49
|
+
quality: liveKitQuality(layer.quality),
|
|
50
|
+
width: layer.width,
|
|
51
|
+
height: layer.height,
|
|
52
|
+
bitrate: layer.bitrate
|
|
53
|
+
}))
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* One member's LiveKit signal session, in this process: the browser's call messages become LiveKit's signal requests,
|
|
57
|
+
* and LiveKit's answers, candidates and room changes become call messages. Protobase decides who hears what: nothing is
|
|
58
|
+
* subscribed until `hears` admits it. The session keeps nothing a new one could not rebuild from the browser's `resume`,
|
|
59
|
+
* so a member moves to another session, or process, without losing media.
|
|
60
|
+
*/
|
|
61
|
+
const liveKitSession = (backend, member, link, deps) => {
|
|
62
|
+
const hears = member.hears ?? (() => true);
|
|
63
|
+
let signal;
|
|
64
|
+
let finished = false;
|
|
65
|
+
const participants = /* @__PURE__ */ new Map();
|
|
66
|
+
const heard = /* @__PURE__ */ new Set();
|
|
67
|
+
const publishing = /* @__PURE__ */ new Map();
|
|
68
|
+
const toBrowser = (message) => link.send(message);
|
|
69
|
+
const finish = (code, reason) => {
|
|
70
|
+
if (finished) return;
|
|
71
|
+
finished = true;
|
|
72
|
+
signal?.close();
|
|
73
|
+
for (const pending of publishing.values()) pending.reject("The call session ended");
|
|
74
|
+
link.close(code, reason);
|
|
75
|
+
};
|
|
76
|
+
const leave = () => {
|
|
77
|
+
signal?.send({
|
|
78
|
+
case: "leave",
|
|
79
|
+
value: new LeaveRequest({
|
|
80
|
+
reason: DisconnectReason.CLIENT_INITIATED,
|
|
81
|
+
action: LeaveRequest_Action.DISCONNECT
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
finish(1e3, "left");
|
|
85
|
+
};
|
|
86
|
+
const updateTracks = () => {
|
|
87
|
+
const tracks = remoteTracksOf([...participants.values()]).filter(hears);
|
|
88
|
+
const added = tracks.filter((track) => !heard.has(track.sid)).map((track) => track.sid);
|
|
89
|
+
const current = new Set(tracks.map((track) => track.sid));
|
|
90
|
+
for (const sid of heard) if (!current.has(sid)) heard.delete(sid);
|
|
91
|
+
for (const sid of added) heard.add(sid);
|
|
92
|
+
if (added.length > 0) signal?.send({
|
|
93
|
+
case: "subscription",
|
|
94
|
+
value: new UpdateSubscription({
|
|
95
|
+
trackSids: added,
|
|
96
|
+
subscribe: true
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
toBrowser({
|
|
100
|
+
type: "tracks",
|
|
101
|
+
tracks
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const updateParticipants = (infos, self) => {
|
|
105
|
+
for (const info of infos) {
|
|
106
|
+
if (info.identity === self) continue;
|
|
107
|
+
if (info.state === ParticipantInfo_State.DISCONNECTED) participants.delete(info.sid);
|
|
108
|
+
else participants.set(info.sid, info);
|
|
109
|
+
}
|
|
110
|
+
updateTracks();
|
|
111
|
+
};
|
|
112
|
+
const onSignal = (message) => {
|
|
113
|
+
switch (message.case) {
|
|
114
|
+
case "join": {
|
|
115
|
+
const join = message.value;
|
|
116
|
+
signal.keepAlive(join.pingInterval || resumePing.interval, join.pingTimeout || resumePing.timeout);
|
|
117
|
+
toBrowser({
|
|
118
|
+
type: "joined",
|
|
119
|
+
mode: "sfu",
|
|
120
|
+
participant: join.participant.sid,
|
|
121
|
+
iceServers: join.iceServers.map((server) => ({
|
|
122
|
+
urls: server.urls,
|
|
123
|
+
...server.username && {
|
|
124
|
+
username: server.username,
|
|
125
|
+
credential: server.credential
|
|
126
|
+
}
|
|
127
|
+
}))
|
|
128
|
+
});
|
|
129
|
+
updateParticipants(join.otherParticipants, member.member);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
case "reconnect":
|
|
133
|
+
signal.keepAlive(resumePing.interval, resumePing.timeout);
|
|
134
|
+
toBrowser({ type: "resumed" });
|
|
135
|
+
return;
|
|
136
|
+
case "answer":
|
|
137
|
+
toBrowser({
|
|
138
|
+
type: "answer",
|
|
139
|
+
sdp: message.value.sdp,
|
|
140
|
+
id: message.value.id,
|
|
141
|
+
mids: message.value.midToTrackId
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
case "trickle":
|
|
145
|
+
toBrowser({
|
|
146
|
+
type: "candidate",
|
|
147
|
+
candidate: JSON.parse(message.value.candidateInit)
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
case "mediaSectionsRequirement":
|
|
151
|
+
toBrowser({
|
|
152
|
+
type: "sections",
|
|
153
|
+
audio: message.value.numAudios,
|
|
154
|
+
video: message.value.numVideos
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
case "update":
|
|
158
|
+
updateParticipants(message.value.participants, member.member);
|
|
159
|
+
return;
|
|
160
|
+
case "trackPublished":
|
|
161
|
+
publishing.get(message.value.cid)?.resolve(message.value.track.sid);
|
|
162
|
+
return;
|
|
163
|
+
case "requestResponse": {
|
|
164
|
+
const { request, reason, message: why } = message.value;
|
|
165
|
+
if (request.case === "addTrack" && reason !== RequestResponse_Reason.OK) publishing.get(request.value.cid)?.reject(why || RequestResponse_Reason[reason]);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
case "trackUnpublished":
|
|
169
|
+
toBrowser({
|
|
170
|
+
type: "unpublished",
|
|
171
|
+
sid: message.value.trackSid
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
case "speakersChanged":
|
|
175
|
+
toBrowser({
|
|
176
|
+
type: "speakers",
|
|
177
|
+
speakers: message.value.speakers.flatMap((speaker) => {
|
|
178
|
+
const info = participants.get(speaker.sid);
|
|
179
|
+
return info ? [{
|
|
180
|
+
member: info.identity,
|
|
181
|
+
level: speaker.active ? speaker.level : 0
|
|
182
|
+
}] : [];
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
case "leave": {
|
|
187
|
+
const leave = message.value;
|
|
188
|
+
if (leave.action === LeaveRequest_Action.RESUME) return finish(resumeCloseCode, "resume");
|
|
189
|
+
toBrowser({
|
|
190
|
+
type: "left",
|
|
191
|
+
reason: leftReason(leave)
|
|
192
|
+
});
|
|
193
|
+
return finish(1e3, "left");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const connect = async (options) => {
|
|
198
|
+
const canPublish = member.publish !== false && !(Array.isArray(member.publish) && member.publish.length === 0);
|
|
199
|
+
const token = await liveKitToken(backend, {
|
|
200
|
+
room: member.call,
|
|
201
|
+
identity: member.member,
|
|
202
|
+
canPublish,
|
|
203
|
+
...Array.isArray(member.publish) && { canPublishSources: member.publish.map((source) => grantSources[source]) }
|
|
204
|
+
});
|
|
205
|
+
signal = deps.connect({
|
|
206
|
+
url: joinUrl(backend.url.replace(/^http/, "ws"), token, options),
|
|
207
|
+
onMessage: onSignal,
|
|
208
|
+
onClose: () => finish(resumeCloseCode, "resume")
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
const publish = async (tracks) => {
|
|
212
|
+
const published = tracks.map((track) => new Promise((resolve, reject) => {
|
|
213
|
+
const timer = setTimeout(() => reject("The media server did not accept the track in time"), publishTimeoutMs);
|
|
214
|
+
const settle = () => {
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
publishing.delete(track.cid);
|
|
217
|
+
};
|
|
218
|
+
publishing.set(track.cid, {
|
|
219
|
+
resolve: (sid) => (settle(), resolve({
|
|
220
|
+
cid: track.cid,
|
|
221
|
+
sid
|
|
222
|
+
})),
|
|
223
|
+
reject: (reason) => (settle(), reject(reason))
|
|
224
|
+
});
|
|
225
|
+
signal.send({
|
|
226
|
+
case: "addTrack",
|
|
227
|
+
value: addTrackRequest(track)
|
|
228
|
+
});
|
|
229
|
+
}));
|
|
230
|
+
const settled = await Promise.allSettled(published);
|
|
231
|
+
const refused = settled.find((result) => result.status === "rejected");
|
|
232
|
+
if (refused) return toBrowser({
|
|
233
|
+
type: "publish-refused",
|
|
234
|
+
reason: String(refused.reason)
|
|
235
|
+
});
|
|
236
|
+
toBrowser({
|
|
237
|
+
type: "published",
|
|
238
|
+
tracks: settled.map((result) => result.value)
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const receive = async (message) => {
|
|
242
|
+
if (message.type === "join") return connect({});
|
|
243
|
+
if (message.type === "resume") {
|
|
244
|
+
if (!message.offer || !message.answer) {
|
|
245
|
+
toBrowser({
|
|
246
|
+
type: "left",
|
|
247
|
+
reason: "rejoin"
|
|
248
|
+
});
|
|
249
|
+
return finish(1e3, "left");
|
|
250
|
+
}
|
|
251
|
+
await connect({ resume: { participant: message.participant } });
|
|
252
|
+
signal.send({
|
|
253
|
+
case: "syncState",
|
|
254
|
+
value: syncState({
|
|
255
|
+
...message,
|
|
256
|
+
offer: message.offer,
|
|
257
|
+
answer: message.answer
|
|
258
|
+
})
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!signal || finished) return;
|
|
263
|
+
switch (message.type) {
|
|
264
|
+
case "publish": return publish(message.tracks);
|
|
265
|
+
case "offer": return signal.send({
|
|
266
|
+
case: "offer",
|
|
267
|
+
value: new SessionDescription({
|
|
268
|
+
type: "offer",
|
|
269
|
+
sdp: message.sdp,
|
|
270
|
+
id: message.id
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
case "candidate": return signal.send({
|
|
274
|
+
case: "trickle",
|
|
275
|
+
value: new TrickleRequest({
|
|
276
|
+
candidateInit: JSON.stringify(message.candidate),
|
|
277
|
+
target: SignalTarget.PUBLISHER
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
case "mute": return signal.send({
|
|
281
|
+
case: "mute",
|
|
282
|
+
value: new MuteTrackRequest({
|
|
283
|
+
sid: message.sid,
|
|
284
|
+
muted: message.muted
|
|
285
|
+
})
|
|
286
|
+
});
|
|
287
|
+
case "view": {
|
|
288
|
+
const sids = message.sids.filter((sid) => heard.has(sid));
|
|
289
|
+
if (sids.length === 0) return;
|
|
290
|
+
return signal.send({
|
|
291
|
+
case: "trackSetting",
|
|
292
|
+
value: new UpdateTrackSettings({
|
|
293
|
+
trackSids: sids,
|
|
294
|
+
disabled: message.hidden ?? false,
|
|
295
|
+
width: message.width,
|
|
296
|
+
height: message.height
|
|
297
|
+
})
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
case "leave": return leave();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
receive,
|
|
305
|
+
end: (how) => {
|
|
306
|
+
if (finished) return;
|
|
307
|
+
finished = true;
|
|
308
|
+
if (how === "left") signal?.send({
|
|
309
|
+
case: "leave",
|
|
310
|
+
value: new LeaveRequest({
|
|
311
|
+
reason: DisconnectReason.PARTICIPANT_REMOVED,
|
|
312
|
+
action: LeaveRequest_Action.DISCONNECT
|
|
313
|
+
})
|
|
314
|
+
});
|
|
315
|
+
signal?.close();
|
|
316
|
+
for (const pending of publishing.values()) pending.reject("The call session ended");
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
/** The LiveKit plugin: each member's signal session to a LiveKit server, self-hosted or LiveKit Cloud. */
|
|
321
|
+
const liveKitBackend = (settings, deps = { connect: openSignalSocket }) => ({ open: (member, link) => liveKitSession(settings, member, link, deps) });
|
|
322
|
+
//#endregion
|
|
323
|
+
export { liveKitBackend };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SignJWT } from "jose";
|
|
2
|
+
//#region src/calls/livekit/livekit-token.ts
|
|
3
|
+
/** How long a member's token admits a join; a resume mints a new one. */
|
|
4
|
+
const tokenSeconds = 600;
|
|
5
|
+
/**
|
|
6
|
+
* A LiveKit access token for one member of one room, signed with the backend's key: no name, metadata or data
|
|
7
|
+
* messages, since names, roles and chat stay in Protobase.
|
|
8
|
+
*/
|
|
9
|
+
const liveKitToken = async ({ apiKey, apiSecret }, grant) => {
|
|
10
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
11
|
+
const video = {
|
|
12
|
+
room: grant.room,
|
|
13
|
+
roomJoin: true,
|
|
14
|
+
canSubscribe: true,
|
|
15
|
+
canPublish: grant.canPublish,
|
|
16
|
+
canPublishData: false,
|
|
17
|
+
canUpdateOwnMetadata: false,
|
|
18
|
+
...grant.canPublishSources && { canPublishSources: grant.canPublishSources }
|
|
19
|
+
};
|
|
20
|
+
return new SignJWT({ video }).setProtectedHeader({
|
|
21
|
+
alg: "HS256",
|
|
22
|
+
typ: "JWT"
|
|
23
|
+
}).setIssuer(apiKey).setSubject(grant.identity).setNotBefore(now).setExpirationTime(now + tokenSeconds).setJti(grant.identity).sign(new TextEncoder().encode(apiSecret));
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { liveKitToken };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Ping, SignalRequest, SignalResponse } from "@livekit/protocol";
|
|
2
|
+
//#region src/calls/livekit/signal-socket.ts
|
|
3
|
+
/** A WebSocket to LiveKit's signal endpoint speaking its protobuf messages, for one member. */
|
|
4
|
+
const openSignalSocket = ({ url, onMessage, onClose }) => {
|
|
5
|
+
const socket = new WebSocket(url);
|
|
6
|
+
socket.binaryType = "arraybuffer";
|
|
7
|
+
let pinger;
|
|
8
|
+
let lastPong = Date.now();
|
|
9
|
+
let closed = false;
|
|
10
|
+
const queued = [];
|
|
11
|
+
const stop = () => {
|
|
12
|
+
if (closed) return false;
|
|
13
|
+
closed = true;
|
|
14
|
+
clearInterval(pinger);
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
17
|
+
const close = () => {
|
|
18
|
+
if (stop()) socket.close();
|
|
19
|
+
};
|
|
20
|
+
socket.onmessage = (event) => {
|
|
21
|
+
const { message } = SignalResponse.fromBinary(new Uint8Array(event.data));
|
|
22
|
+
if (message.case === "pongResp") lastPong = Date.now();
|
|
23
|
+
onMessage(message);
|
|
24
|
+
};
|
|
25
|
+
socket.onopen = () => {
|
|
26
|
+
for (const data of queued.splice(0)) socket.send(data);
|
|
27
|
+
};
|
|
28
|
+
socket.onclose = () => {
|
|
29
|
+
if (stop()) onClose();
|
|
30
|
+
};
|
|
31
|
+
socket.onerror = () => {};
|
|
32
|
+
const send = (message) => {
|
|
33
|
+
const data = new SignalRequest({ message }).toBinary();
|
|
34
|
+
if (socket.readyState === WebSocket.CONNECTING) queued.push(data);
|
|
35
|
+
else if (socket.readyState === WebSocket.OPEN) socket.send(data);
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
send,
|
|
39
|
+
keepAlive: (intervalSeconds, timeoutSeconds) => {
|
|
40
|
+
clearInterval(pinger);
|
|
41
|
+
lastPong = Date.now();
|
|
42
|
+
pinger = setInterval(() => {
|
|
43
|
+
if (Date.now() - lastPong > timeoutSeconds * 1e3) {
|
|
44
|
+
if (stop()) {
|
|
45
|
+
socket.close();
|
|
46
|
+
onClose();
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
send({
|
|
51
|
+
case: "pingReq",
|
|
52
|
+
value: new Ping({ timestamp: BigInt(Date.now()) })
|
|
53
|
+
});
|
|
54
|
+
}, intervalSeconds * 1e3);
|
|
55
|
+
},
|
|
56
|
+
close
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { openSignalSocket };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ParticipantInfo_State, TrackSource, TrackType, VideoQuality } from "@livekit/protocol";
|
|
2
|
+
//#region src/calls/livekit/track-mapping.ts
|
|
3
|
+
const sources = {
|
|
4
|
+
camera: TrackSource.CAMERA,
|
|
5
|
+
microphone: TrackSource.MICROPHONE,
|
|
6
|
+
screen: TrackSource.SCREEN_SHARE,
|
|
7
|
+
"screen-audio": TrackSource.SCREEN_SHARE_AUDIO
|
|
8
|
+
};
|
|
9
|
+
/** The names LiveKit's token grants give each source in `canPublishSources`. */
|
|
10
|
+
const grantSources = {
|
|
11
|
+
camera: "camera",
|
|
12
|
+
microphone: "microphone",
|
|
13
|
+
screen: "screen_share",
|
|
14
|
+
"screen-audio": "screen_share_audio"
|
|
15
|
+
};
|
|
16
|
+
const liveKitSource = (source) => sources[source];
|
|
17
|
+
/** A LiveKit source as the call protocol names it; an unknown one counts as the camera or microphone of its kind. */
|
|
18
|
+
const callSource = (source, type) => {
|
|
19
|
+
return Object.keys(sources).find((name) => sources[name] === source) ?? (type === TrackType.AUDIO ? "microphone" : "camera");
|
|
20
|
+
};
|
|
21
|
+
const liveKitQuality = (quality) => ({
|
|
22
|
+
low: VideoQuality.LOW,
|
|
23
|
+
medium: VideoQuality.MEDIUM,
|
|
24
|
+
high: VideoQuality.HIGH
|
|
25
|
+
})[quality];
|
|
26
|
+
const remoteTrack = (member, track) => ({
|
|
27
|
+
sid: track.sid,
|
|
28
|
+
member,
|
|
29
|
+
kind: track.type === TrackType.AUDIO ? "audio" : "video",
|
|
30
|
+
source: callSource(track.source, track.type),
|
|
31
|
+
muted: track.muted
|
|
32
|
+
});
|
|
33
|
+
/** The audio and video tracks of the participants that are still in the room, each under its participant's identity. */
|
|
34
|
+
const remoteTracksOf = (participants) => participants.filter((participant) => participant.state !== ParticipantInfo_State.DISCONNECTED).flatMap((participant) => participant.tracks.filter((track) => track.type !== TrackType.DATA).map((track) => remoteTrack(participant.identity, track)));
|
|
35
|
+
//#endregion
|
|
36
|
+
export { callSource, grantSources, liveKitQuality, liveKitSource, remoteTracksOf };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseClientMessage } from "./client-message.js";
|
|
2
|
+
//#region src/calls/member-relay.ts
|
|
3
|
+
/** The close code of a socket whose caller Protobase refused at a recheck: the member leaves the call too. */
|
|
4
|
+
const refusedCloseCode = 1008;
|
|
5
|
+
const mayPublish = (member, tracks) => member.publish === true || Array.isArray(member.publish) && tracks.every((track) => member.publish.includes(track.source));
|
|
6
|
+
/**
|
|
7
|
+
* One member's socket, relayed to the call's media plugin: the relay reads and checks the browser's messages, refuses a
|
|
8
|
+
* publish the member may not make, opens the member's session at their `join` or `resume` and tells it how the socket
|
|
9
|
+
* ended. The plugin only ever sees well-formed messages of a member Protobase admitted.
|
|
10
|
+
*/
|
|
11
|
+
const memberRelay = (backend, member) => {
|
|
12
|
+
let browser;
|
|
13
|
+
let session;
|
|
14
|
+
let finished = false;
|
|
15
|
+
const send = (message) => void browser.send(JSON.stringify(message));
|
|
16
|
+
const close = (code, reason) => {
|
|
17
|
+
if (finished) return;
|
|
18
|
+
finished = true;
|
|
19
|
+
session?.end(code === 1e3 || code === refusedCloseCode ? "left" : "lost");
|
|
20
|
+
browser.close(code, reason);
|
|
21
|
+
};
|
|
22
|
+
const relay = async (message) => {
|
|
23
|
+
if (message.type === "join" || message.type === "resume") {
|
|
24
|
+
if (session) return close(refusedCloseCode, "The call session already started");
|
|
25
|
+
session = backend.open(member, {
|
|
26
|
+
send,
|
|
27
|
+
close
|
|
28
|
+
});
|
|
29
|
+
return session.receive(message);
|
|
30
|
+
}
|
|
31
|
+
if (!session) return close(refusedCloseCode, "Join or resume the call first");
|
|
32
|
+
if (message.type === "publish" && !mayPublish(member, message.tracks)) return send({
|
|
33
|
+
type: "publish-refused",
|
|
34
|
+
reason: "The member may not publish these tracks"
|
|
35
|
+
});
|
|
36
|
+
await session.receive(message);
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
onOpen: (socket) => {
|
|
40
|
+
browser = socket;
|
|
41
|
+
},
|
|
42
|
+
onMessage: async (data) => {
|
|
43
|
+
if (finished) return;
|
|
44
|
+
const message = parseClientMessage(data);
|
|
45
|
+
if (!message) return close(refusedCloseCode, "Not a call message");
|
|
46
|
+
await relay(message);
|
|
47
|
+
},
|
|
48
|
+
onClose: (code) => {
|
|
49
|
+
if (finished) return;
|
|
50
|
+
finished = true;
|
|
51
|
+
session?.end(code === refusedCloseCode ? "left" : "lost");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { memberRelay };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//#region src/calls/peer/peer-backend.ts
|
|
2
|
+
/** How long a member whose socket was lost stays in the call for the others, waiting for their browser to resume. */
|
|
3
|
+
const awayMs = 3e4;
|
|
4
|
+
/**
|
|
5
|
+
* The peer plugin: no media server. Protobase passes each member's descriptions and candidates to the member they name,
|
|
6
|
+
* and tells every member who else is in the call and which of their tracks they hear; the browsers send their media to
|
|
7
|
+
* each other directly. Members meet only within this process, which keeps who is in each call.
|
|
8
|
+
*/
|
|
9
|
+
const peerBackend = (settings) => {
|
|
10
|
+
const calls = /* @__PURE__ */ new Map();
|
|
11
|
+
const membersOf = (call) => [...calls.get(call)?.values() ?? []];
|
|
12
|
+
const announce = (call) => {
|
|
13
|
+
const present = membersOf(call);
|
|
14
|
+
for (const viewer of present) {
|
|
15
|
+
if (viewer.away) continue;
|
|
16
|
+
const others = present.filter((other) => other !== viewer);
|
|
17
|
+
const hears = viewer.member.hears ?? (() => true);
|
|
18
|
+
viewer.link.send({
|
|
19
|
+
type: "peers",
|
|
20
|
+
members: others.map((other) => other.member.member)
|
|
21
|
+
});
|
|
22
|
+
viewer.link.send({
|
|
23
|
+
type: "tracks",
|
|
24
|
+
tracks: others.flatMap((other) => [...other.tracks.values()]).filter(hears)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const remove = (self) => {
|
|
29
|
+
const call = calls.get(self.member.call);
|
|
30
|
+
if (call?.get(self.member.member) !== self) return;
|
|
31
|
+
clearTimeout(self.away);
|
|
32
|
+
call.delete(self.member.member);
|
|
33
|
+
if (call.size === 0) calls.delete(self.member.call);
|
|
34
|
+
announce(self.member.call);
|
|
35
|
+
};
|
|
36
|
+
const open = (member, link) => {
|
|
37
|
+
const self = {
|
|
38
|
+
member,
|
|
39
|
+
link,
|
|
40
|
+
tracks: /* @__PURE__ */ new Map()
|
|
41
|
+
};
|
|
42
|
+
const enter = () => {
|
|
43
|
+
const call = calls.get(member.call) ?? /* @__PURE__ */ new Map();
|
|
44
|
+
calls.set(member.call, call);
|
|
45
|
+
const before = call.get(member.member);
|
|
46
|
+
call.set(member.member, self);
|
|
47
|
+
if (before) {
|
|
48
|
+
clearTimeout(before.away);
|
|
49
|
+
if (!before.away) {
|
|
50
|
+
before.link.send({
|
|
51
|
+
type: "left",
|
|
52
|
+
reason: "removed"
|
|
53
|
+
});
|
|
54
|
+
before.link.close(1e3, "left");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const receive = (message) => {
|
|
59
|
+
switch (message.type) {
|
|
60
|
+
case "join":
|
|
61
|
+
case "resume":
|
|
62
|
+
if (message.type === "resume") for (const track of message.tracks) self.tracks.set(track.sid, {
|
|
63
|
+
sid: track.sid,
|
|
64
|
+
member: member.member,
|
|
65
|
+
kind: track.kind,
|
|
66
|
+
source: track.source,
|
|
67
|
+
muted: false
|
|
68
|
+
});
|
|
69
|
+
enter();
|
|
70
|
+
link.send(message.type === "join" ? {
|
|
71
|
+
type: "joined",
|
|
72
|
+
mode: "peer",
|
|
73
|
+
participant: member.member,
|
|
74
|
+
iceServers: settings.iceServers ?? []
|
|
75
|
+
} : { type: "resumed" });
|
|
76
|
+
return announce(member.call);
|
|
77
|
+
case "publish":
|
|
78
|
+
for (const track of message.tracks) self.tracks.set(track.cid, {
|
|
79
|
+
sid: track.cid,
|
|
80
|
+
member: member.member,
|
|
81
|
+
kind: track.kind,
|
|
82
|
+
source: track.source,
|
|
83
|
+
muted: false
|
|
84
|
+
});
|
|
85
|
+
link.send({
|
|
86
|
+
type: "published",
|
|
87
|
+
tracks: message.tracks.map((track) => ({
|
|
88
|
+
cid: track.cid,
|
|
89
|
+
sid: track.cid
|
|
90
|
+
}))
|
|
91
|
+
});
|
|
92
|
+
return announce(member.call);
|
|
93
|
+
case "unpublish":
|
|
94
|
+
self.tracks.delete(message.sid);
|
|
95
|
+
return announce(member.call);
|
|
96
|
+
case "mute": {
|
|
97
|
+
const track = self.tracks.get(message.sid);
|
|
98
|
+
if (!track) return;
|
|
99
|
+
track.muted = message.muted;
|
|
100
|
+
return announce(member.call);
|
|
101
|
+
}
|
|
102
|
+
case "signal": {
|
|
103
|
+
const to = calls.get(member.call)?.get(message.to);
|
|
104
|
+
if (!to || to.away || to === self) return;
|
|
105
|
+
to.link.send({
|
|
106
|
+
type: "signal",
|
|
107
|
+
from: member.member,
|
|
108
|
+
...message.description && { description: message.description },
|
|
109
|
+
...message.candidate && { candidate: message.candidate }
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case "leave": return link.close(1e3, "left");
|
|
114
|
+
case "offer":
|
|
115
|
+
case "candidate":
|
|
116
|
+
case "view": return;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
receive,
|
|
121
|
+
end: (how) => {
|
|
122
|
+
if (how === "left") return remove(self);
|
|
123
|
+
if (calls.get(member.call)?.get(member.member) !== self) return;
|
|
124
|
+
self.away = setTimeout(() => remove(self), awayMs);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
return { open };
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
export { peerBackend };
|
package/dist/create-admin.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { CommentUsers } from "./comments/comment-users.js";
|
|
|
9
9
|
import { HistoryOptions } from "./history/options.js";
|
|
10
10
|
import { WebhooksOptions } from "./webhooks/table/options.js";
|
|
11
11
|
import { NotificationsOptions } from "./notifications/options.js";
|
|
12
|
+
import { CallsOptions } from "./calls/calls-options.js";
|
|
12
13
|
import { SubscriptionDefinition } from "./webhooks/subscription.js";
|
|
13
14
|
import { FilesOptions } from "./files/options.js";
|
|
14
15
|
import { Difficulty } from "./forms/challenge.js";
|
|
@@ -54,6 +55,8 @@ export type CreateAdminInput = {
|
|
|
54
55
|
realtime?: RealtimeOptions;
|
|
55
56
|
/** Each user's devices and the endpoints that register them, from `notifications: {}` (see `NotificationsOptions`). */
|
|
56
57
|
notifications?: NotificationsOptions;
|
|
58
|
+
/** The media server behind the calls functions admit members to with `calls.join` (see `CallsOptions`). */
|
|
59
|
+
calls?: CallsOptions;
|
|
57
60
|
/**
|
|
58
61
|
* The app's iOS and Android build (see `MobileConfig`): the API answers its Capacitor origins through CORS, and sign-in
|
|
59
62
|
* at `/api/auth` trusts the Capacitor origin a request names, without CORS, as the app calls it through native HTTP.
|
|
@@ -93,5 +96,5 @@ export type CreateAdminInput = {
|
|
|
93
96
|
upgradeWebSocket?: UpgradeWebSocket;
|
|
94
97
|
};
|
|
95
98
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
96
|
-
export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, secret, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, realtime, notifications, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
99
|
+
export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, secret, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, realtime, notifications, calls, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
97
100
|
//#endregion
|
package/dist/create-admin.js
CHANGED
|
@@ -108,7 +108,7 @@ const declaredRoles = (auth, roles) => {
|
|
|
108
108
|
return names.length > 0 ? [...new Set(names)] : void 0;
|
|
109
109
|
};
|
|
110
110
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
111
|
-
const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, secret, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, realtime, notifications, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
111
|
+
const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, secret, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, realtime, notifications, calls, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
112
112
|
const basePath = options.basePath ?? "/api/v1";
|
|
113
113
|
const appModels = appResources.map((resource) => resource.toModel());
|
|
114
114
|
const withComments = appModels.some((model) => model.comments);
|
|
@@ -388,6 +388,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
388
388
|
wake: () => wakeDispatch(deps),
|
|
389
389
|
upgradeWebSocket,
|
|
390
390
|
upgradeAuthenticate,
|
|
391
|
+
...calls && { calls },
|
|
391
392
|
report
|
|
392
393
|
}));
|
|
393
394
|
const verifyGoTrue = auth ? jwtVerifier(goTrueTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens"));
|
|
@@ -2,6 +2,7 @@ import { DecryptField } from "../encryption/decrypt-field-types.js";
|
|
|
2
2
|
import { Session } from "../types.js";
|
|
3
3
|
import { Caller } from "../api-keys/resolve-caller.js";
|
|
4
4
|
import { SocketEvents } from "./function-socket.js";
|
|
5
|
+
import { CallsAccess } from "../calls/calls-access.js";
|
|
5
6
|
import { FunctionRecords } from "./records.js";
|
|
6
7
|
import { Capability } from "@protobase/schema";
|
|
7
8
|
import { Db } from "@protobase/query";
|
|
@@ -22,6 +23,8 @@ export type PublicFunctionContext = {
|
|
|
22
23
|
* not repeated unless it passes them as `recheck`. `maxMessageBytes` lowers the largest message the socket takes.
|
|
23
24
|
*/
|
|
24
25
|
acceptWebSocket: (events: SocketEvents) => Response;
|
|
26
|
+
/** Admits a member to a call's media, over the socket the function accepts: see `CallsAccess`. */
|
|
27
|
+
calls: CallsAccess;
|
|
25
28
|
};
|
|
26
29
|
/** Who called a function, and the app's resources as them. */
|
|
27
30
|
export type FunctionCaller = {
|
package/dist/functions/routes.js
CHANGED
|
@@ -3,6 +3,7 @@ import { namingTimeouts } from "../statement-timeout.js";
|
|
|
3
3
|
import { auditOrigin } from "../audit/origin.js";
|
|
4
4
|
import { invokeFunction } from "./define-function.js";
|
|
5
5
|
import { runInBackground } from "../background-work.js";
|
|
6
|
+
import { callsAccess } from "../calls/calls-access.js";
|
|
6
7
|
import { socketAcceptor } from "./accept-socket.js";
|
|
7
8
|
import { functionGate, requireGate } from "./function-gate.js";
|
|
8
9
|
import { sameCaller } from "./same-caller.js";
|
|
@@ -23,7 +24,8 @@ const isUpgrade = (request) => request.headers.get("upgrade")?.toLowerCase() ===
|
|
|
23
24
|
* A function may answer an upgrade with a socket, only once it returned. While it is open, its caller is checked again
|
|
24
25
|
* every minute: a protected function's always, a public one's once `caller()` resolved.
|
|
25
26
|
*/
|
|
26
|
-
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, decryptField, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, report }) => {
|
|
27
|
+
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, decryptField, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, calls: callsOptions, report }) => {
|
|
28
|
+
const calls = callsAccess(callsOptions);
|
|
27
29
|
const app = new Hono().basePath(basePath);
|
|
28
30
|
app.onError(onError);
|
|
29
31
|
const sessionOf = (request) => isUpgrade(request) ? upgradeAuthenticate(request) : authenticate(request);
|
|
@@ -67,7 +69,8 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
67
69
|
name,
|
|
68
70
|
db,
|
|
69
71
|
caller: resolveCaller,
|
|
70
|
-
acceptWebSocket: acceptor.accept
|
|
72
|
+
acceptWebSocket: acceptor.accept,
|
|
73
|
+
calls
|
|
71
74
|
};
|
|
72
75
|
return acceptor.answer(name, await invokeFunction(name, fn, request, context));
|
|
73
76
|
}
|
|
@@ -95,7 +98,8 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
95
98
|
...caller,
|
|
96
99
|
caller: async () => caller,
|
|
97
100
|
decryptField: decryptField(reader),
|
|
98
|
-
acceptWebSocket: acceptor.accept
|
|
101
|
+
acceptWebSocket: acceptor.accept,
|
|
102
|
+
calls
|
|
99
103
|
};
|
|
100
104
|
return acceptor.answer(name, await invokeFunction(name, fn, request, context));
|
|
101
105
|
}));
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { DeviceStore } from "./notifications/device-store.js";
|
|
|
21
21
|
import { HistoryStore, maxHistoryPage, maxUnreadCount } from "./notifications/history-store.js";
|
|
22
22
|
import { PushAnswer, PushSender } from "./notifications/sender.js";
|
|
23
23
|
import { DeviceColumn, HistoryColumn, NotificationsOptions } from "./notifications/options.js";
|
|
24
|
+
import { CallBackend, CallsOptions, LiveKitBackend } from "./calls/calls-options.js";
|
|
24
25
|
import { EventData, RunContext, SubscriptionDefinition, SubscriptionEvent, SubscriptionOptions, subscription } from "./webhooks/subscription.js";
|
|
25
26
|
import { Sharp, renderVariant, thumbHash } from "./files/images.js";
|
|
26
27
|
import { FileProvider, FileStore, StoredObject } from "./files/store.js";
|
|
@@ -28,6 +29,8 @@ import { FilesOptions } from "./files/options.js";
|
|
|
28
29
|
import { ApiKeys, ApiKeysOptions, apiKeySettings, apiKeyVariables } from "./api-keys/api-key-settings.js";
|
|
29
30
|
import { Caller, CallerOptions, apiKeyCaller } from "./api-keys/resolve-caller.js";
|
|
30
31
|
import { FunctionSocket, SocketEvents } from "./functions/function-socket.js";
|
|
32
|
+
import { CallMember } from "./calls/call-member.js";
|
|
33
|
+
import { CallsAccess } from "./calls/calls-access.js";
|
|
31
34
|
import { ResourceSource, configExports } from "./resource-source.js";
|
|
32
35
|
import { ErrorReportingOptions, ErrorReportingSettings, errorReportingSettings, errorReportingVariable } from "./error-reporting/error-reporting-settings.js";
|
|
33
36
|
import { ChatMessage, ChatTextPart, Completion, CompletionOptions, ModelError, ModelSettings, ReasoningDetail, ReasoningEffort, ToolCall, ToolSpec, streamChatCompletion } from "./assistant/chat-completions.js";
|
|
@@ -171,4 +174,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
|
|
|
171
174
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
172
175
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
173
176
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
|
|
174
|
-
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type CascadingKey, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type SendNotification, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
177
|
+
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LiveKitBackend, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type SendNotification, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkNotificationsOptions } from "./notifications/options.js";
|
|
2
|
+
import { checkCallsOptions } from "./calls/calls-options.js";
|
|
2
3
|
import { isSubscription } from "./webhooks/subscription.js";
|
|
3
4
|
import { appPool, longQueryPool } from "./app-pool.js";
|
|
4
5
|
import { AppAuthInput, appAuthOptions, buildAppAuth } from "./better-auth/auth-settings.js";
|
|
@@ -10,4 +11,4 @@ import { fileProviderOf } from "./files/provider-settings.js";
|
|
|
10
11
|
import { declaredJob } from "./jobs/declared-job.js";
|
|
11
12
|
import { AuthPlugins, buildAuth, createAuth, loadAuthPlugins } from "./better-auth/create-auth.js";
|
|
12
13
|
import { webhooksModule } from "./webhooks/table/webhooks-module.js";
|
|
13
|
-
export { type AppAuthInput, type AuthPlugins, appAuthOptions, appPool, betterAuthAuthenticator, buildAppAuth, buildAuth, buildAuthenticator, checkNotificationsOptions, createAuth, declaredJob, fileProviderOf, isSubscription, loadAuthPlugins, localFiles, longQueryPool, s3Files, webhooksModule };
|
|
14
|
+
export { type AppAuthInput, type AuthPlugins, appAuthOptions, appPool, betterAuthAuthenticator, buildAppAuth, buildAuth, buildAuthenticator, checkCallsOptions, checkNotificationsOptions, createAuth, declaredJob, fileProviderOf, isSubscription, loadAuthPlugins, localFiles, longQueryPool, s3Files, webhooksModule };
|
package/dist/internal.js
CHANGED
|
@@ -10,4 +10,5 @@ import { declaredJob } from "./jobs/declared-job.js";
|
|
|
10
10
|
import { appAuthOptions, buildAppAuth } from "./better-auth/auth-settings.js";
|
|
11
11
|
import { buildAuthenticator } from "./better-auth/authenticate-settings.js";
|
|
12
12
|
import { webhooksModule } from "./webhooks/table/webhooks-module.js";
|
|
13
|
-
|
|
13
|
+
import { checkCallsOptions } from "./calls/calls-options.js";
|
|
14
|
+
export { appAuthOptions, appPool, betterAuthAuthenticator, buildAppAuth, buildAuth, buildAuthenticator, checkCallsOptions, checkNotificationsOptions, createAuth, declaredJob, fileProviderOf, isSubscription, loadAuthPlugins, localFiles, longQueryPool, s3Files, webhooksModule };
|
package/dist/package.js
CHANGED
package/dist/webhooks/attempt.js
CHANGED
|
@@ -54,7 +54,8 @@ const callFunction = async (name, subscription, event, options, context, signal)
|
|
|
54
54
|
name,
|
|
55
55
|
db: context.db,
|
|
56
56
|
caller: async () => caller,
|
|
57
|
-
acceptWebSocket
|
|
57
|
+
acceptWebSocket,
|
|
58
|
+
calls: { join: async () => acceptWebSocket() }
|
|
58
59
|
};
|
|
59
60
|
return invokeFunction(name, fn, await request("http://protobase.internal/", event, options, signal), fn.options.public ? functionContext : {
|
|
60
61
|
...functionContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.6",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"@better-auth/oauth-provider": "^1.7.12",
|
|
41
41
|
"@better-auth/passkey": "^1.7.12",
|
|
42
42
|
"@better-auth/sso": "^1.7.12",
|
|
43
|
+
"@livekit/protocol": "1.53.1",
|
|
43
44
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
44
|
-
"@protobase/layout": "0.58.
|
|
45
|
-
"@protobase/query": "0.58.
|
|
46
|
-
"@protobase/schema": "0.58.
|
|
45
|
+
"@protobase/layout": "0.58.6",
|
|
46
|
+
"@protobase/query": "0.58.6",
|
|
47
|
+
"@protobase/schema": "0.58.6",
|
|
47
48
|
"@react-email/render": "^2.1.0",
|
|
48
49
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
49
50
|
"@sentry/core": "^11.7.0",
|