@protobase/server 0.58.5 → 0.58.7
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/better-auth/auth-settings.d.ts +4 -0
- package/dist/better-auth/auth-tables.d.ts +5 -1
- package/dist/better-auth/auth-tables.js +10 -2
- package/dist/better-auth/create-auth.d.ts +4 -0
- package/dist/better-auth/store/drift.js +19 -4
- package/dist/better-auth/store/migrations/001-core.js +6 -4
- package/dist/better-auth/store/migrations/002-two-factor.js +2 -2
- package/dist/better-auth/store/migrations/006-organizations.js +2 -2
- package/dist/better-auth/store/migrations/migration.js +2 -1
- package/dist/better-auth/user-table.d.ts +9 -0
- package/dist/better-auth/user-table.js +3 -2
- 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
|
@@ -8819,6 +8819,10 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
|
|
|
8819
8819
|
column: (name: UserColumn) => string;
|
|
8820
8820
|
fieldNames: Record<string, string>;
|
|
8821
8821
|
metadata: string | undefined;
|
|
8822
|
+
keyTable: {
|
|
8823
|
+
schema: string;
|
|
8824
|
+
table: string;
|
|
8825
|
+
} | undefined;
|
|
8822
8826
|
};
|
|
8823
8827
|
socialProviders: string[];
|
|
8824
8828
|
appName: string;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { AdminAuth } from "./create-auth.js";
|
|
2
2
|
//#region src/better-auth/auth-tables.d.ts
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The admin store's tables, which the assistant's queries and the app's resources must never read: the user table and
|
|
5
|
+
* `storeTableNames`, and with `users.keyTable` the table under the user view too, such as Supabase's `auth.users`,
|
|
6
|
+
* whose password hashes the view leaves out but the table holds.
|
|
7
|
+
*/
|
|
4
8
|
export declare const authStoreTables: (auth: AdminAuth) => {
|
|
5
9
|
schema: string | undefined;
|
|
6
10
|
name: string;
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { storeNames } from "./store/store-names.js";
|
|
2
2
|
import { storeTableNames } from "./store/table-names.js";
|
|
3
3
|
//#region src/better-auth/auth-tables.ts
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The admin store's tables, which the assistant's queries and the app's resources must never read: the user table and
|
|
6
|
+
* `storeTableNames`, and with `users.keyTable` the table under the user view too, such as Supabase's `auth.users`,
|
|
7
|
+
* whose password hashes the view leaves out but the table holds.
|
|
8
|
+
*/
|
|
5
9
|
const authStoreTables = (auth) => {
|
|
6
10
|
const { schema, users } = storeNames(auth);
|
|
7
|
-
|
|
11
|
+
const own = [users.modelName, ...storeTableNames].map((name) => ({
|
|
8
12
|
schema,
|
|
9
13
|
name
|
|
10
14
|
}));
|
|
15
|
+
return users.keyTable ? [...own, {
|
|
16
|
+
schema: users.keyTable.schema,
|
|
17
|
+
name: users.keyTable.table
|
|
18
|
+
}] : own;
|
|
11
19
|
};
|
|
12
20
|
//#endregion
|
|
13
21
|
export { authStoreTables };
|
|
@@ -8958,6 +8958,10 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
8958
8958
|
column: (name: UserColumn) => string;
|
|
8959
8959
|
fieldNames: Record<string, string>;
|
|
8960
8960
|
metadata: string | undefined;
|
|
8961
|
+
keyTable: {
|
|
8962
|
+
schema: string;
|
|
8963
|
+
table: string;
|
|
8964
|
+
} | undefined;
|
|
8961
8965
|
};
|
|
8962
8966
|
socialProviders: string[];
|
|
8963
8967
|
/** The app's name it signs in and mails as: `PROTOBASE_APP_NAME`, else `appName`, else `Protobase`. */
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { storeNames } from "./store-names.js";
|
|
2
|
+
import { storeDb } from "./store-db.js";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
import { getSchema } from "better-auth/db";
|
|
1
5
|
import { getMigrations } from "better-auth/db/migration";
|
|
2
6
|
//#region src/better-auth/store/drift.ts
|
|
7
|
+
/** Each column of Better Auth's user model the view lacks, or the view itself when there is none. */
|
|
8
|
+
const userViewDrift = async (auth) => {
|
|
9
|
+
const { schema, users } = storeNames(auth);
|
|
10
|
+
const { rows } = await sql`select column_name as "column" from information_schema.columns
|
|
11
|
+
where table_schema = ${schema ?? sql`current_schema()`} and table_name = ${users.modelName}`.execute(storeDb(auth));
|
|
12
|
+
if (rows.length === 0) return [`lacks the view ${users.modelName}`];
|
|
13
|
+
const found = new Set(rows.map(({ column }) => column));
|
|
14
|
+
return ["id", ...Object.keys(getSchema(auth.options)[users.modelName]?.fields ?? {})].filter((column) => !found.has(column)).map((column) => `lacks the column ${users.modelName}.${column}`);
|
|
15
|
+
};
|
|
3
16
|
/**
|
|
4
17
|
* What Better Auth, with the features the app turns on, finds the store lacks or has otherwise: tables, `table.column`s,
|
|
5
18
|
* indexes and its other findings; none when the store matches. It compares the database's tables, so only
|
|
@@ -7,12 +20,14 @@ import { getMigrations } from "better-auth/db/migration";
|
|
|
7
20
|
*/
|
|
8
21
|
const authSchemaDrift = async (auth) => {
|
|
9
22
|
const { toBeCreated, toBeAdded, toBeAddedIndexes, schemaProblems } = await getMigrations(auth.options, { throwOnUnsafe: false });
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
...
|
|
13
|
-
...
|
|
23
|
+
const view = auth.userTable.keyTable ? auth.userTable.modelName : void 0;
|
|
24
|
+
const tables = [
|
|
25
|
+
...toBeCreated.filter(({ table }) => table !== view).map(({ table }) => `lacks the table ${table}`),
|
|
26
|
+
...toBeAdded.filter(({ table }) => table !== view).flatMap(({ table, fields }) => Object.keys(fields).map((field) => `lacks the column ${table}.${field}`)),
|
|
27
|
+
...toBeAddedIndexes.filter(({ table }) => table !== view).map(({ name }) => `lacks the index ${name}`),
|
|
14
28
|
...schemaProblems
|
|
15
29
|
];
|
|
30
|
+
return view ? [...tables, ...await userViewDrift(auth)] : tables;
|
|
16
31
|
};
|
|
17
32
|
//#endregion
|
|
18
33
|
export { authSchemaDrift };
|
|
@@ -11,9 +11,10 @@ const core = {
|
|
|
11
11
|
"verification",
|
|
12
12
|
"jwks"
|
|
13
13
|
],
|
|
14
|
-
statements: ({ table, user, userColumn: column, metadata, id, idRef }) => [
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
statements: ({ table, user, userDdl, userColumn: column, metadata, id, idRef }) => [
|
|
15
|
+
...userDdl([
|
|
16
|
+
ddl`create table if not exists ${user} (id ${id} primary key)`,
|
|
17
|
+
ddl`alter table ${user}
|
|
17
18
|
add column if not exists ${column("name")} text not null,
|
|
18
19
|
add column if not exists ${column("email")} text not null unique,
|
|
19
20
|
add column if not exists ${column("email_verified")} boolean not null,
|
|
@@ -26,7 +27,8 @@ const core = {
|
|
|
26
27
|
add column if not exists ${column("ban_expires")} timestamptz,
|
|
27
28
|
add column if not exists ${column("last_role")} text,
|
|
28
29
|
add column if not exists ${column("email_confirmed_at")} timestamptz`,
|
|
29
|
-
|
|
30
|
+
...metadata ? [ddl`alter table ${user} add column if not exists ${metadata} jsonb`] : []
|
|
31
|
+
]),
|
|
30
32
|
ddl`create table ${table("session")} (
|
|
31
33
|
id ${id} primary key,
|
|
32
34
|
expires_at timestamptz not null,
|
|
@@ -6,8 +6,8 @@ const twoFactor = {
|
|
|
6
6
|
name: "two_factor",
|
|
7
7
|
title: "The authenticator secrets and backup codes of two-step sign-in",
|
|
8
8
|
tables: ["two_factor"],
|
|
9
|
-
statements: ({ table, user, userColumn: column, id, idRef }) => [
|
|
10
|
-
ddl`alter table ${user} add column if not exists ${column("two_factor_enabled")} boolean
|
|
9
|
+
statements: ({ table, user, userDdl, userColumn: column, id, idRef }) => [
|
|
10
|
+
...userDdl([ddl`alter table ${user} add column if not exists ${column("two_factor_enabled")} boolean`]),
|
|
11
11
|
ddl`create table ${table("two_factor")} (
|
|
12
12
|
id ${id} primary key,
|
|
13
13
|
secret text not null,
|
|
@@ -10,8 +10,8 @@ const organizations = {
|
|
|
10
10
|
"member",
|
|
11
11
|
"invitation"
|
|
12
12
|
],
|
|
13
|
-
statements: ({ table, user, userColumn: column, id, idRef }) => [
|
|
14
|
-
ddl`alter table ${user} add column if not exists ${column("last_organization_id")} text
|
|
13
|
+
statements: ({ table, user, userDdl, userColumn: column, id, idRef }) => [
|
|
14
|
+
...userDdl([ddl`alter table ${user} add column if not exists ${column("last_organization_id")} text`]),
|
|
15
15
|
ddl`alter table ${table("session")} add column active_organization_id text`,
|
|
16
16
|
ddl`create table ${table("organization")} (
|
|
17
17
|
id ${id} primary key,
|
|
@@ -4,7 +4,8 @@ import { sql } from "kysely";
|
|
|
4
4
|
/** The `ddl` parts of the app's names. */
|
|
5
5
|
const migrationNames = (names) => ({
|
|
6
6
|
table: (name) => storeTable(names, name),
|
|
7
|
-
user: storeTable(names, names.users.modelName),
|
|
7
|
+
user: names.users.keyTable ? sql.id(names.users.keyTable.schema, names.users.keyTable.table) : storeTable(names, names.users.modelName),
|
|
8
|
+
userDdl: (statements) => names.users.keyTable ? [] : statements,
|
|
8
9
|
userColumn: names.users.column,
|
|
9
10
|
metadata: names.users.metadata,
|
|
10
11
|
id: sql.raw(names.uuidIds ? "uuid default pg_catalog.gen_random_uuid()" : "text"),
|
|
@@ -28,5 +28,14 @@ export type UserTableOptions = {
|
|
|
28
28
|
columns?: Partial<Record<UserColumn, string>>;
|
|
29
29
|
/** A JSON column that holds each user's metadata, such as `raw_user_meta_data`; none by default. */
|
|
30
30
|
metadata?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The table that holds the user ids, when `table` is a view over another system's user table, such as
|
|
33
|
+
* `{ schema: 'auth', table: 'users' }` under a view over Supabase's `auth.users`: the store's migrations then make
|
|
34
|
+
* nothing of the user table, which the app's own migration makes, and every foreign key points at this table.
|
|
35
|
+
*/
|
|
36
|
+
keyTable?: {
|
|
37
|
+
schema: string;
|
|
38
|
+
table: string;
|
|
39
|
+
};
|
|
31
40
|
};
|
|
32
41
|
//#endregion
|
|
@@ -17,7 +17,7 @@ const userFields = {
|
|
|
17
17
|
last_organization_id: "lastOrganizationId"
|
|
18
18
|
};
|
|
19
19
|
/** The user table's names, from `auth.users` option; unknown columns are an error. */
|
|
20
|
-
const userTable = ({ table = "user", columns = {}, metadata } = {}) => {
|
|
20
|
+
const userTable = ({ table = "user", columns = {}, metadata, keyTable } = {}) => {
|
|
21
21
|
const unknown = Object.keys(columns).find((name) => !(name in userFields));
|
|
22
22
|
if (unknown !== void 0) throw new Error(`users.columns names "${unknown}", which is not a column of the user table: ${Object.keys(userFields).join(", ")}`);
|
|
23
23
|
const column = (name) => columns[name] ?? name;
|
|
@@ -25,7 +25,8 @@ const userTable = ({ table = "user", columns = {}, metadata } = {}) => {
|
|
|
25
25
|
modelName: table,
|
|
26
26
|
column,
|
|
27
27
|
fieldNames: Object.fromEntries(Object.entries(userFields).map(([name, field]) => [field, column(name)])),
|
|
28
|
-
metadata
|
|
28
|
+
metadata,
|
|
29
|
+
keyTable
|
|
29
30
|
};
|
|
30
31
|
};
|
|
31
32
|
/** Better Auth's `user` option: the table and its core columns. */
|
|
@@ -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 };
|