@volter/twin-livekit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +106 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +23 -0
- package/dist/src/egress-service-cli.d.ts +2 -0
- package/dist/src/egress-service-cli.js +101 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +53 -0
- package/dist/src/livekit-budget.d.ts +57 -0
- package/dist/src/livekit-budget.js +131 -0
- package/dist/src/livekit-capabilities.d.ts +3 -0
- package/dist/src/livekit-capabilities.js +728 -0
- package/dist/src/livekit-conformance.d.ts +8 -0
- package/dist/src/livekit-conformance.js +13 -0
- package/dist/src/livekit-connector.d.ts +130 -0
- package/dist/src/livekit-connector.js +382 -0
- package/dist/src/livekit-data.d.ts +86 -0
- package/dist/src/livekit-data.js +246 -0
- package/dist/src/livekit-perform-harness.d.ts +5 -0
- package/dist/src/livekit-perform-harness.js +17 -0
- package/dist/src/livekit-server.d.ts +23 -0
- package/dist/src/livekit-server.js +45 -0
- package/dist/src/livekit-service-cli.d.ts +2 -0
- package/dist/src/livekit-service-cli.js +88 -0
- package/dist/src/livekit-token.d.ts +68 -0
- package/dist/src/livekit-token.js +76 -0
- package/dist/src/livekit-twin.d.ts +4 -0
- package/dist/src/livekit-twin.js +1161 -0
- package/dist/src/livekit-types.d.ts +203 -0
- package/dist/src/livekit-types.js +1 -0
- package/dist/src/livekit-webhook.d.ts +26 -0
- package/dist/src/livekit-webhook.js +79 -0
- package/dist/src/redis-service-cli.d.ts +2 -0
- package/dist/src/redis-service-cli.js +35 -0
- package/dist/test-fixtures/livekit-openapi-operations.SOURCE.md +88 -0
- package/dist/test-fixtures/livekit-openapi-operations.json +442 -0
- package/dist/test-fixtures/protobufs/cloud_replay.proto +82 -0
- package/dist/test-fixtures/protobufs/livekit_agent.proto +185 -0
- package/dist/test-fixtures/protobufs/livekit_agent_dispatch.proto +103 -0
- package/dist/test-fixtures/protobufs/livekit_agent_simulation.proto +422 -0
- package/dist/test-fixtures/protobufs/livekit_agent_worker.proto +29 -0
- package/dist/test-fixtures/protobufs/livekit_agentdb.proto +226 -0
- package/dist/test-fixtures/protobufs/livekit_analytics.proto +312 -0
- package/dist/test-fixtures/protobufs/livekit_cloud_agent.proto +376 -0
- package/dist/test-fixtures/protobufs/livekit_connector.proto +41 -0
- package/dist/test-fixtures/protobufs/livekit_connector_twilio.proto +65 -0
- package/dist/test-fixtures/protobufs/livekit_connector_whatsapp.proto +171 -0
- package/dist/test-fixtures/protobufs/livekit_egress.proto +641 -0
- package/dist/test-fixtures/protobufs/livekit_ingress.proto +222 -0
- package/dist/test-fixtures/protobufs/livekit_internal.proto +228 -0
- package/dist/test-fixtures/protobufs/livekit_metrics.proto +103 -0
- package/dist/test-fixtures/protobufs/livekit_models.proto +992 -0
- package/dist/test-fixtures/protobufs/livekit_phone_number.proto +151 -0
- package/dist/test-fixtures/protobufs/livekit_room.proto +312 -0
- package/dist/test-fixtures/protobufs/livekit_rtc.proto +673 -0
- package/dist/test-fixtures/protobufs/livekit_sip.proto +1000 -0
- package/dist/test-fixtures/protobufs/livekit_token_source.proto +49 -0
- package/dist/test-fixtures/protobufs/livekit_webhook.proto +62 -0
- package/package.json +58 -0
- package/src/cli.ts +22 -0
- package/src/egress-service-cli.ts +111 -0
- package/src/index.ts +103 -0
- package/src/livekit-budget.ts +158 -0
- package/src/livekit-capabilities.ts +789 -0
- package/src/livekit-conformance.ts +15 -0
- package/src/livekit-connector.ts +406 -0
- package/src/livekit-data.ts +278 -0
- package/src/livekit-perform-harness.ts +17 -0
- package/src/livekit-server.ts +67 -0
- package/src/livekit-service-cli.ts +93 -0
- package/src/livekit-token.ts +133 -0
- package/src/livekit-twin.ts +1162 -0
- package/src/livekit-types.ts +211 -0
- package/src/livekit-webhook.ts +91 -0
- package/src/redis-service-cli.ts +39 -0
- package/test-fixtures/livekit-openapi-operations.SOURCE.md +88 -0
- package/test-fixtures/livekit-openapi-operations.json +442 -0
- package/test-fixtures/protobufs/cloud_replay.proto +82 -0
- package/test-fixtures/protobufs/livekit_agent.proto +185 -0
- package/test-fixtures/protobufs/livekit_agent_dispatch.proto +103 -0
- package/test-fixtures/protobufs/livekit_agent_simulation.proto +422 -0
- package/test-fixtures/protobufs/livekit_agent_worker.proto +29 -0
- package/test-fixtures/protobufs/livekit_agentdb.proto +226 -0
- package/test-fixtures/protobufs/livekit_analytics.proto +312 -0
- package/test-fixtures/protobufs/livekit_cloud_agent.proto +376 -0
- package/test-fixtures/protobufs/livekit_connector.proto +41 -0
- package/test-fixtures/protobufs/livekit_connector_twilio.proto +65 -0
- package/test-fixtures/protobufs/livekit_connector_whatsapp.proto +171 -0
- package/test-fixtures/protobufs/livekit_egress.proto +641 -0
- package/test-fixtures/protobufs/livekit_ingress.proto +222 -0
- package/test-fixtures/protobufs/livekit_internal.proto +228 -0
- package/test-fixtures/protobufs/livekit_metrics.proto +103 -0
- package/test-fixtures/protobufs/livekit_models.proto +992 -0
- package/test-fixtures/protobufs/livekit_phone_number.proto +151 -0
- package/test-fixtures/protobufs/livekit_room.proto +312 -0
- package/test-fixtures/protobufs/livekit_rtc.proto +673 -0
- package/test-fixtures/protobufs/livekit_sip.proto +1000 -0
- package/test-fixtures/protobufs/livekit_token_source.proto +49 -0
- package/test-fixtures/protobufs/livekit_webhook.proto +62 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright 2025 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
syntax = "proto3";
|
|
16
|
+
|
|
17
|
+
package livekit;
|
|
18
|
+
option go_package = "github.com/livekit/protocol/livekit";
|
|
19
|
+
option csharp_namespace = "LiveKit.Proto";
|
|
20
|
+
option ruby_package = "LiveKit::Proto";
|
|
21
|
+
|
|
22
|
+
import "livekit_room.proto";
|
|
23
|
+
|
|
24
|
+
message TokenSourceRequest {
|
|
25
|
+
// The name of the room being requested when generating credentials
|
|
26
|
+
optional string room_name = 1;
|
|
27
|
+
|
|
28
|
+
// The name of the participant being requested for this client when generating credentials
|
|
29
|
+
optional string participant_name = 2;
|
|
30
|
+
|
|
31
|
+
// The identity of the participant being requested for this client when generating credentials
|
|
32
|
+
optional string participant_identity = 3;
|
|
33
|
+
|
|
34
|
+
// Any participant metadata being included along with the credentials generation operation
|
|
35
|
+
optional string participant_metadata = 4;
|
|
36
|
+
|
|
37
|
+
// Any participant attributes being included along with the credentials generation operation
|
|
38
|
+
map<string, string> participant_attributes = 5;
|
|
39
|
+
|
|
40
|
+
// A RoomConfiguration object can be passed to request extra parameters should be included when
|
|
41
|
+
// generating connection credentials - dispatching agents, defining egress settings, etc
|
|
42
|
+
// More info: https://docs.livekit.io/home/get-started/authentication/#room-configuration
|
|
43
|
+
optional RoomConfiguration room_config = 6;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
message TokenSourceResponse {
|
|
47
|
+
string server_url = 1;
|
|
48
|
+
string participant_token = 2;
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright 2023 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
syntax = "proto3";
|
|
16
|
+
|
|
17
|
+
package livekit;
|
|
18
|
+
option go_package = "github.com/livekit/protocol/livekit";
|
|
19
|
+
option csharp_namespace = "LiveKit.Proto";
|
|
20
|
+
option ruby_package = "LiveKit::Proto";
|
|
21
|
+
|
|
22
|
+
import "livekit_models.proto";
|
|
23
|
+
import "livekit_egress.proto";
|
|
24
|
+
import "livekit_ingress.proto";
|
|
25
|
+
import "livekit_agent.proto";
|
|
26
|
+
|
|
27
|
+
message WebhookEvent {
|
|
28
|
+
// one of room_started, room_finished, participant_joined, participant_left, participant_connection_aborted,
|
|
29
|
+
// track_published, track_unpublished, egress_started, egress_updated, egress_ended,
|
|
30
|
+
// ingress_started, ingress_ended, agent_job_started, agent_job_ended
|
|
31
|
+
string event = 1;
|
|
32
|
+
|
|
33
|
+
Room room = 2;
|
|
34
|
+
|
|
35
|
+
// set when event is participant_* or track_*
|
|
36
|
+
ParticipantInfo participant = 3;
|
|
37
|
+
|
|
38
|
+
// set when event is egress_*
|
|
39
|
+
EgressInfo egress_info = 9;
|
|
40
|
+
|
|
41
|
+
// set when event is ingress_*
|
|
42
|
+
IngressInfo ingress_info = 10;
|
|
43
|
+
|
|
44
|
+
// set when event is track_*
|
|
45
|
+
TrackInfo track = 8;
|
|
46
|
+
|
|
47
|
+
// set when event is agent_job_*
|
|
48
|
+
Job job = 12;
|
|
49
|
+
|
|
50
|
+
// unique event uuid
|
|
51
|
+
string id = 6;
|
|
52
|
+
|
|
53
|
+
// timestamp in seconds
|
|
54
|
+
int64 created_at = 7;
|
|
55
|
+
|
|
56
|
+
int32 num_dropped = 11 [deprecated=true];
|
|
57
|
+
|
|
58
|
+
// set when event is room_finished
|
|
59
|
+
RoomEndReason room_end_reason = 13;
|
|
60
|
+
|
|
61
|
+
// NEXT_ID: 14
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volter/twin-livekit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local LiveKit control-plane twin built on @volter/world-core.",
|
|
5
|
+
"author": "Volter (https://github.com/volter-ai)",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"client",
|
|
10
|
+
"test-fixtures",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"!**/*.test.ts",
|
|
14
|
+
"!**/*.test.tsx",
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/volter-ai/twin.git",
|
|
20
|
+
"directory": "packages/twin/livekit"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/livekit#readme",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/src/index.d.ts",
|
|
27
|
+
"default": "./dist/src/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"world-livekit": "dist/src/cli.js",
|
|
32
|
+
"world-livekit-redis": "dist/src/redis-service-cli.js",
|
|
33
|
+
"world-livekit-server": "dist/src/livekit-service-cli.js",
|
|
34
|
+
"world-livekit-egress": "dist/src/egress-service-cli.js"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "bun test src/*.test.ts",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"build": "node ../../../scripts/publish/build.mjs",
|
|
40
|
+
"prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
|
|
41
|
+
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@volter/world-core": "2.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@livekit/protocol": "^1.49.0",
|
|
48
|
+
"@types/bun": "^1.2.20",
|
|
49
|
+
"@types/node": "^24.0.0",
|
|
50
|
+
"@volter/world-core": "2.0.0",
|
|
51
|
+
"@volter/world-tooling": "0.1.0",
|
|
52
|
+
"livekit-server-sdk": "^2.15.0",
|
|
53
|
+
"typescript": "^5.9.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=22.3"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
3
|
+
import { hasFlag, optionValue } from '@volter/world-core/args';
|
|
4
|
+
import { createLiveKitTwinServer } from './livekit-server.ts';
|
|
5
|
+
|
|
6
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
7
|
+
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
8
|
+
const root = optionValue(rest, '--root') || undefined;
|
|
9
|
+
const readOnly = hasFlag(rest, '--read-only');
|
|
10
|
+
|
|
11
|
+
if (cmd === 'serve') {
|
|
12
|
+
const server = await createLiveKitTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
|
|
13
|
+
process.stdout.write(`livekit twin${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${server.port}\n`);
|
|
14
|
+
await keepProcessAlive();
|
|
15
|
+
} else if (cmd === 'conformance') {
|
|
16
|
+
const { checkLiveKitConformance } = await import('./livekit-conformance.ts');
|
|
17
|
+
const report = await checkLiveKitConformance({ ...(root ? { root } : {}) });
|
|
18
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
19
|
+
if (!report.ok) process.exitCode = 1;
|
|
20
|
+
} else {
|
|
21
|
+
process.stdout.write('Usage: world-livekit serve|conformance [--port N] [--root DIR] [--read-only]\n');
|
|
22
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const PORT = process.env.PORT;
|
|
7
|
+
const LIVEKIT_URL = process.env.LIVEKIT_URL;
|
|
8
|
+
const REDIS_HOST = process.env.REDIS_HOST;
|
|
9
|
+
if (!PORT) throw new Error('PORT is required');
|
|
10
|
+
if (!LIVEKIT_URL) throw new Error('LIVEKIT_URL is required');
|
|
11
|
+
if (!REDIS_HOST) throw new Error('REDIS_HOST is required');
|
|
12
|
+
|
|
13
|
+
// Loopback mode (EGRESS_LOOPBACK_PORTS, extra host ports such as an S3 twin's): a forwarding sidecar owns the
|
|
14
|
+
// egress's network namespace and carries 127.0.0.1:<port> to this host's loopback, so the egress reaches LiveKit,
|
|
15
|
+
// Redis and every named port at the same 127.0.0.1 address as everything else on this host. One storage endpoint
|
|
16
|
+
// URL then means the same place to the egress (which uploads to it) and to the host process that reads it back.
|
|
17
|
+
const loopbackPorts = (process.env.EGRESS_LOOPBACK_PORTS ?? '').split(',').map((p) => p.trim()).filter(Boolean);
|
|
18
|
+
const loopback = loopbackPorts.length > 0;
|
|
19
|
+
const portOf = (value) => /:(\d+)/.exec(value ?? '')?.[1];
|
|
20
|
+
const hostLiveKitUrl = loopback ? LIVEKIT_URL : LIVEKIT_URL.replace('127.0.0.1', 'host.docker.internal');
|
|
21
|
+
const hostRedis = loopback ? REDIS_HOST : REDIS_HOST.replace('127.0.0.1', 'host.docker.internal');
|
|
22
|
+
const dockerReachable = (value) => value?.replace('127.0.0.1', 'host.docker.internal').replace('localhost', 'host.docker.internal');
|
|
23
|
+
const proxyUrl = dockerReachable(process.env.HTTPS_PROXY || process.env.HTTP_PROXY || '');
|
|
24
|
+
const caBundle = process.env.AWS_CA_BUNDLE || process.env.SSL_CERT_FILE || '';
|
|
25
|
+
const containerCa = '/tmp/volter-world-ca.pem';
|
|
26
|
+
const configBody = [
|
|
27
|
+
// The keys of the LiveKit this egress serves (a World names its own); the dev pair is LiveKit's --dev default.
|
|
28
|
+
`api_key: ${process.env.LIVEKIT_API_KEY ?? 'devkey'}`,
|
|
29
|
+
`api_secret: ${process.env.LIVEKIT_API_SECRET ?? 'secret'}`,
|
|
30
|
+
`ws_url: ${hostLiveKitUrl}`,
|
|
31
|
+
'insecure: true',
|
|
32
|
+
`health_port: ${PORT}`,
|
|
33
|
+
'redis:',
|
|
34
|
+
` address: ${hostRedis}`,
|
|
35
|
+
'cpu_cost:',
|
|
36
|
+
' room_composite_cpu_cost: 1.0',
|
|
37
|
+
' web_cpu_cost: 1.0',
|
|
38
|
+
' track_composite_cpu_cost: 1.0',
|
|
39
|
+
' track_cpu_cost: 0.5',
|
|
40
|
+
'log_level: info',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
|
|
44
|
+
const name = `volter-livekit-egress-${PORT}`;
|
|
45
|
+
const sidecar = `${name}-loopback`;
|
|
46
|
+
if (loopback) {
|
|
47
|
+
const forward = [...new Set([portOf(LIVEKIT_URL), portOf(REDIS_HOST), ...loopbackPorts].filter(Boolean))];
|
|
48
|
+
const script = forward.map((p) => `socat TCP-LISTEN:${p},fork,reuseaddr,bind=127.0.0.1 TCP:host.docker.internal:${p} &`).join(' ') + ' wait';
|
|
49
|
+
spawnSync('docker', ['rm', '-f', sidecar], { stdio: 'ignore' });
|
|
50
|
+
const started = spawnSync('docker', ['run', '-d', '--rm', '--name', sidecar, '-p', `127.0.0.1:${PORT}:${PORT}`, '--entrypoint', 'sh', 'alpine/socat', '-c', script], { stdio: 'inherit' });
|
|
51
|
+
if (started.status !== 0) throw new Error('the egress loopback sidecar did not start');
|
|
52
|
+
}
|
|
53
|
+
const dockerArgs = [
|
|
54
|
+
'run',
|
|
55
|
+
'--rm',
|
|
56
|
+
'--name',
|
|
57
|
+
name,
|
|
58
|
+
'--cap-add=SYS_ADMIN',
|
|
59
|
+
...(loopback ? ['--network', `container:${sidecar}`] : ['-p', `127.0.0.1:${PORT}:${PORT}`]),
|
|
60
|
+
'-e',
|
|
61
|
+
`EGRESS_CONFIG_BODY=${configBody}`,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
if (proxyUrl) {
|
|
65
|
+
const noProxy = '127.0.0.1,localhost,host.docker.internal';
|
|
66
|
+
dockerArgs.push(
|
|
67
|
+
'-e', `HTTPS_PROXY=${proxyUrl}`,
|
|
68
|
+
'-e', `HTTP_PROXY=${proxyUrl}`,
|
|
69
|
+
'-e', `https_proxy=${proxyUrl}`,
|
|
70
|
+
'-e', `http_proxy=${proxyUrl}`,
|
|
71
|
+
'-e', `NO_PROXY=${noProxy}`,
|
|
72
|
+
'-e', `no_proxy=${noProxy}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (caBundle) {
|
|
76
|
+
if (!existsSync(caBundle)) throw new Error(`world CA bundle does not exist: ${caBundle}`);
|
|
77
|
+
dockerArgs.push(
|
|
78
|
+
'-e', `AWS_CA_BUNDLE=${containerCa}`,
|
|
79
|
+
'-e', `SSL_CERT_FILE=${containerCa}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
dockerArgs.push('livekit/egress:latest');
|
|
84
|
+
|
|
85
|
+
const child = spawn('docker', dockerArgs, { stdio: 'inherit' });
|
|
86
|
+
|
|
87
|
+
function copyCaIntoContainer(attempt = 0) {
|
|
88
|
+
if (!caBundle) return;
|
|
89
|
+
const result = spawnSync('docker', ['cp', caBundle, `${name}:${containerCa}`], { stdio: 'ignore' });
|
|
90
|
+
if (result.status === 0) return;
|
|
91
|
+
if (attempt >= 50) {
|
|
92
|
+
process.stderr.write(`failed to copy world CA into ${name}; S3 egress TLS may fail\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setTimeout(() => copyCaIntoContainer(attempt + 1), 100);
|
|
96
|
+
}
|
|
97
|
+
copyCaIntoContainer();
|
|
98
|
+
|
|
99
|
+
async function stop() {
|
|
100
|
+
try {
|
|
101
|
+
spawn('docker', ['rm', '-f', name], { stdio: 'ignore' });
|
|
102
|
+
if (loopback) spawn('docker', ['rm', '-f', sidecar], { stdio: 'ignore' });
|
|
103
|
+
} catch {
|
|
104
|
+
// best effort
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
process.on('SIGTERM', () => { stop(); process.exit(0); });
|
|
109
|
+
process.on('SIGINT', () => { stop(); process.exit(0); });
|
|
110
|
+
|
|
111
|
+
child.on('exit', (code) => { if (loopback) spawnSync('docker', ['rm', '-f', sidecar], { stdio: 'ignore' }); process.exit(code ?? 0); });
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export { handleLiveKitTwinRequest } from './livekit-twin.ts';
|
|
2
|
+
export { createLiveKitTwinFetch, createLiveKitTwinServer, type LiveKitTwinFetchOptions } from './livekit-server.ts';
|
|
3
|
+
export {
|
|
4
|
+
liveKitHttpOrigin,
|
|
5
|
+
liveKitRequestForAction,
|
|
6
|
+
liveKitTwirpPath,
|
|
7
|
+
liveLiveKitExecute,
|
|
8
|
+
pullLiveKitSnapshot,
|
|
9
|
+
pullLiveKitSIP,
|
|
10
|
+
pullLiveKitAgentDispatch,
|
|
11
|
+
pushLiveKitAction,
|
|
12
|
+
syncLiveKitFromReal,
|
|
13
|
+
} from './livekit-connector.ts';
|
|
14
|
+
export type { LiveKitExecute, LiveKitService } from './livekit-connector.ts';
|
|
15
|
+
// The client-side rate budget — the fail-closed backstop `liveLiveKitExecute` routes every live
|
|
16
|
+
// request through. The mechanism is the kernel's (`@volter/world-core` -> rateBudget.ts); these are this
|
|
17
|
+
// vendor's numbers and the typed bindings around them.
|
|
18
|
+
export {
|
|
19
|
+
LIVEKIT_BUDGET_CEILING,
|
|
20
|
+
LIVEKIT_BUDGET_MAX_RETRY_AFTER_S,
|
|
21
|
+
LIVEKIT_BUDGET_WINDOW_MS,
|
|
22
|
+
LIVEKIT_CALL_WEIGHTS,
|
|
23
|
+
LIVEKIT_RATE_BUDGET,
|
|
24
|
+
LiveKitBudget,
|
|
25
|
+
LiveKitBudgetError,
|
|
26
|
+
liveKitBudgetPath,
|
|
27
|
+
liveKitCallWeight,
|
|
28
|
+
} from './livekit-budget.ts';
|
|
29
|
+
export type {
|
|
30
|
+
LiveKitBudgetErrorKind,
|
|
31
|
+
LiveKitBudgetOptions,
|
|
32
|
+
LiveKitBudgetReservation,
|
|
33
|
+
LiveKitBudgetSnapshot,
|
|
34
|
+
} from './livekit-budget.ts';
|
|
35
|
+
export { mintAccessToken, verifyAccessToken } from './livekit-token.ts';
|
|
36
|
+
export type { LiveKitClaimGrants, LiveKitVideoGrant, LiveKitSIPGrant, MintTokenInput, VerifyResult } from './livekit-token.ts';
|
|
37
|
+
export { buildWebhookBody, signWebhookHeader, verifyWebhook } from './livekit-webhook.ts';
|
|
38
|
+
export type { WebhookEventInput, WebhookVerifyResult } from './livekit-webhook.ts';
|
|
39
|
+
export type {
|
|
40
|
+
EgressRequestCase,
|
|
41
|
+
LiveKitEgress,
|
|
42
|
+
LiveKitIngress,
|
|
43
|
+
LiveKitParticipant,
|
|
44
|
+
LiveKitParticipantPermission,
|
|
45
|
+
LiveKitRequest,
|
|
46
|
+
LiveKitResponse,
|
|
47
|
+
LiveKitRoom,
|
|
48
|
+
LiveKitTrack,
|
|
49
|
+
LiveKitWebhookEvent,
|
|
50
|
+
LiveKitSIPInboundTrunk,
|
|
51
|
+
LiveKitSIPOutboundTrunk,
|
|
52
|
+
LiveKitSIPDispatchRule,
|
|
53
|
+
LiveKitSIPParticipant,
|
|
54
|
+
LiveKitAgentDispatch,
|
|
55
|
+
} from './livekit-types.ts';
|
|
56
|
+
export { EGRESS_STATUS, INGRESS_INPUT, INGRESS_STATUS, PARTICIPANT_STATE, SIP_TRANSPORT, TRACK_SOURCE, TRACK_TYPE } from './livekit-data.ts';
|
|
57
|
+
|
|
58
|
+
import { registerPack, type TwinPack } from '@volter/world-core';
|
|
59
|
+
import { LIVEKIT_RATE_BUDGET as RATE_BUDGET } from './livekit-budget.ts';
|
|
60
|
+
|
|
61
|
+
import { performLiveKitAction, syncLiveKitFromRemote } from './livekit-connector.ts';
|
|
62
|
+
|
|
63
|
+
export const pack: TwinPack = {
|
|
64
|
+
// PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of the real
|
|
65
|
+
// state system: perform one entry against LiveKit's Twirp RPCs, refresh the root from them. Moved
|
|
66
|
+
// 2026-09-08.
|
|
67
|
+
protocol: '2',
|
|
68
|
+
refresh: { every: '1m', webhook: true, onDemand: { atMost: '10s' } }, // rooms and egresses move fast
|
|
69
|
+
stateSystem: { perform: performLiveKitAction, refresh: syncLiveKitFromRemote },
|
|
70
|
+
// the round trip: create a room — LiveKit's CreateRoom answers the existing room when it is already
|
|
71
|
+
// there, so it repeats cleanly on a branch
|
|
72
|
+
roundTrip: { method: 'POST', path: '/twirp/livekit.RoomService/CreateRoom', body: { name: 'round-trip' } },
|
|
73
|
+
parityOrigin: 'http://twin',
|
|
74
|
+
vendor: 'livekit',
|
|
75
|
+
// The SAME object livekit-budget.ts declares at module load — one source of truth, so registering
|
|
76
|
+
// the pack and importing the connector arm identical numbers.
|
|
77
|
+
rateBudget: RATE_BUDGET,
|
|
78
|
+
transport: 'rest',
|
|
79
|
+
archetype: 'crud',
|
|
80
|
+
bin: 'world-livekit',
|
|
81
|
+
resources: ['room', 'participant', 'egress', 'ingress', 'sip_inbound_trunk', 'sip_outbound_trunk', 'sip_dispatch_rule', 'sip_participant', 'agent_dispatch'],
|
|
82
|
+
specSource: 'LiveKit Server APIs: RoomService (incl. Forward/MoveParticipant), Egress, Ingress, SIP (SIPService), and AgentDispatchService Twirp JSON endpoints + AccessToken/WebhookReceiver schemes used by livekit-server-sdk 2.15.x; this pack models the control plane, not the media plane.',
|
|
83
|
+
description: 'LiveKit control-plane twin — local rooms, participants, egress/ingress lifecycle, access-token grants, and webhook signing for server SDK integrations.',
|
|
84
|
+
browserRouting: { apiPathPrefix: '/twirp/', loaderHost: 'https://livekit.cloud' },
|
|
85
|
+
// INTERCEPTION RULING — hostsNone, the pack's own home for it: real local media plane
|
|
86
|
+
// (livekit-server) exposed via app-read env (LIVEKIT_URL is ws:// — WebSocket media is
|
|
87
|
+
// outside the HTTP injector's reach).
|
|
88
|
+
hostsNone:
|
|
89
|
+
"real local media plane (livekit-server) exposed via app-read env (LIVEKIT_URL is ws:// — WebSocket media is outside the HTTP injector's reach)",
|
|
90
|
+
// Adoption, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
|
|
91
|
+
// 2026-08-31).
|
|
92
|
+
adoption: {
|
|
93
|
+
// LiveKit's official Python distributions: the server API client (the counterpart of
|
|
94
|
+
// `livekit-server-sdk`), the realtime SDK (`@livekit/rtc-node`), and the agents framework whose
|
|
95
|
+
// core connects to a LiveKit server.
|
|
96
|
+
pypi: ['livekit-api', 'livekit', 'livekit-agents'],
|
|
97
|
+
sdks: ['livekit-server-sdk', '@livekit/rtc-node'],
|
|
98
|
+
scopes: ['@livekit/'],
|
|
99
|
+
envStems: ['LIVEKIT'],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
// registered at import: the kernel learns the pack's state system (protocol 2)
|
|
103
|
+
registerPack(pack);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// LiveKit's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus the thin typed
|
|
2
|
+
// bindings `liveLiveKitExecute` uses. The MECHANISM — the durable token-keyed ledger, the rolling
|
|
3
|
+
// window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt ledger — lives
|
|
4
|
+
// ONCE in the vendor-agnostic kernel (`@volter/world-core` → `rateBudget.ts`). Read that module's header
|
|
5
|
+
// for the full rationale AND for the honest list of what the guard does not guarantee (an injected
|
|
6
|
+
// clock or ledger path still defeats it — it guards carelessness, not malice).
|
|
7
|
+
//
|
|
8
|
+
// ── WHY THIS EXISTS: THE MISSING CHOKE POINT ─────────────────────────────────────────────────
|
|
9
|
+
// A ~4.5-day Figma token lockout (2026-07-25) came from a burst of raw vendor calls made OUTSIDE any
|
|
10
|
+
// guarded client. The lesson generalized: a pack with no single construction site for its live client
|
|
11
|
+
// is a pack that CANNOT be protected, because there is nowhere for a guard to sit.
|
|
12
|
+
//
|
|
13
|
+
// This connector took an INJECTED `LiveKitExecute` and nothing more. Excellent for testing — every
|
|
14
|
+
// verify runs offline against a fake — but it meant the pack never built a network-calling execute, so
|
|
15
|
+
// a rate budget could only ever be something the CALLER opted into. `liveLiveKitExecute` (in
|
|
16
|
+
// `livekit-connector.ts`) is the choke point that fixes that: the ONE place in this pack that turns a
|
|
17
|
+
// key/secret pair into a function that really talks to a LiveKit server, with this budget charged
|
|
18
|
+
// BEFORE every request goes out. The injected-function contract is untouched.
|
|
19
|
+
//
|
|
20
|
+
// ── WHY THE FAN-OUT SHAPE MATTERS HERE ───────────────────────────────────────────────────────
|
|
21
|
+
// `collectLiveKitSnapshot` lists rooms and then, FOR EACH ROOM, lists that room's participants; the
|
|
22
|
+
// agent-dispatch collector likewise issues one `ListDispatch` per room. So one innocent-looking
|
|
23
|
+
// `syncLiveKitFromReal` over a busy project emits O(rooms) Server-API requests as fast as the event
|
|
24
|
+
// loop allows. That is exactly the shape a budget exists to bound.
|
|
25
|
+
//
|
|
26
|
+
// ── HOW THE CEILING WAS CHOSEN ───────────────────────────────────────────────────────────────
|
|
27
|
+
// LiveKit publishes a scalar for precisely this surface: "All projects have a Server API rate limit of
|
|
28
|
+
// 1,000 requests per minute" — applying to RoomService/EgressService-style requests, NOT to SDK
|
|
29
|
+
// operations like joining a room or sending data packets (docs.livekit.io/deploy/admin/quotas-and-limits/;
|
|
30
|
+
// Scale-plan customers may request an increase). Every call this connector makes is a Server API call.
|
|
31
|
+
//
|
|
32
|
+
// So: 200 weighted units per 60s. With the cheap list/get reads priced at 1 that is 200 requests a
|
|
33
|
+
// minute — 20% of the documented limit — which comfortably covers a real snapshot (a project with a
|
|
34
|
+
// few dozen rooms) while a runaway per-room loop is refused rather than allowed to consume the whole
|
|
35
|
+
// project-wide allowance that the app's OWN server also depends on.
|
|
36
|
+
//
|
|
37
|
+
// ── HOW THE WEIGHTS WERE CHOSEN ──────────────────────────────────────────────────────────────
|
|
38
|
+
// LiveKit counts REQUESTS, so weight 1 is the faithful price for a read, and the flat list/get calls
|
|
39
|
+
// get it. Two get 2 — `RoomService/ListParticipants` and `AgentDispatchService/ListDispatch` — not
|
|
40
|
+
// because LiveKit charges more (it does not) but because they are the two whose call count is
|
|
41
|
+
// UNBOUNDED IN THE ROOM COUNT: they are the inner loop above. Doubling their price halves the room
|
|
42
|
+
// count at which a runaway walk is stopped while barely touching a small snapshot. A judgement call,
|
|
43
|
+
// not a published cost.
|
|
44
|
+
//
|
|
45
|
+
// Everything unclassified costs `defaultWeight` (2) — above a read, because a mutating Twirp method
|
|
46
|
+
// (StartEgress, CreateIngress, SendData) is not a list read and this pack has read no published
|
|
47
|
+
// per-method figure for it. Nothing is ever free.
|
|
48
|
+
import {
|
|
49
|
+
declareRateBudget,
|
|
50
|
+
rateBudgetPath,
|
|
51
|
+
rateBudgetWeight,
|
|
52
|
+
RateBudget,
|
|
53
|
+
type RateBudgetDeclaration,
|
|
54
|
+
type RateBudgetOptions,
|
|
55
|
+
type RateBudgetReservation,
|
|
56
|
+
type RateBudgetSnapshot,
|
|
57
|
+
} from '@volter/world-core';
|
|
58
|
+
|
|
59
|
+
const VENDOR = 'livekit';
|
|
60
|
+
|
|
61
|
+
/** Rolling window, in ms. Matches LiveKit's own per-MINUTE Server API limit. */
|
|
62
|
+
export const LIVEKIT_BUDGET_WINDOW_MS = 60_000;
|
|
63
|
+
|
|
64
|
+
/** Weighted units allowed inside one window. 200 reads/60s = 20% of the documented 1,000/min. */
|
|
65
|
+
export const LIVEKIT_BUDGET_CEILING = 200;
|
|
66
|
+
|
|
67
|
+
/** Seconds. A `Retry-After` above this means the project is throttled hard — fail loudly. */
|
|
68
|
+
export const LIVEKIT_BUDGET_MAX_RETRY_AFTER_S = 300;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Per-call cost, keyed by the Twirp RPC id (`"<Service>/<Method>"`) this connector is about to invoke.
|
|
72
|
+
* A LiveKit call is not a REST path — it is an RPC — so the RPC id is the honest call key.
|
|
73
|
+
*/
|
|
74
|
+
export const LIVEKIT_CALL_WEIGHTS = {
|
|
75
|
+
/** The per-room inner loop: one `ListParticipants` / `ListDispatch` per room. Unbounded fan-out. */
|
|
76
|
+
perRoomFanOut: 2,
|
|
77
|
+
/** A flat `List…` / `Get…` read: one call for a whole collection. */
|
|
78
|
+
read: 1,
|
|
79
|
+
/** Anything else, including every mutating method. Priced above a read, never free. */
|
|
80
|
+
other: 2,
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* THE PACK'S DECLARATION — pure data, the only LiveKit-specific thing in the whole budget. Rules are
|
|
85
|
+
* ordered and first-match-wins, and they are keyed on the Twirp RPC id, so the per-room fan-out rules
|
|
86
|
+
* must come BEFORE the generic list/get rule.
|
|
87
|
+
*
|
|
88
|
+
* Also exported as `pack.rateBudget` (see index.ts), so `registerPack` arms it too.
|
|
89
|
+
*/
|
|
90
|
+
export const LIVEKIT_RATE_BUDGET: RateBudgetDeclaration = {
|
|
91
|
+
windowMs: LIVEKIT_BUDGET_WINDOW_MS,
|
|
92
|
+
ceiling: LIVEKIT_BUDGET_CEILING,
|
|
93
|
+
defaultWeight: LIVEKIT_CALL_WEIGHTS.other,
|
|
94
|
+
maxRetryAfterSeconds: LIVEKIT_BUDGET_MAX_RETRY_AFTER_S,
|
|
95
|
+
rules: [
|
|
96
|
+
{ match: '^RoomService/ListParticipants$', weight: LIVEKIT_CALL_WEIGHTS.perRoomFanOut },
|
|
97
|
+
{ match: '^AgentDispatchService/ListDispatch$', weight: LIVEKIT_CALL_WEIGHTS.perRoomFanOut },
|
|
98
|
+
{ match: '^[A-Za-z]+/(List|Get)[A-Za-z]*$', weight: LIVEKIT_CALL_WEIGHTS.read },
|
|
99
|
+
],
|
|
100
|
+
reason:
|
|
101
|
+
'LiveKit documents that "All projects have a Server API rate limit of 1,000 requests per minute" — ' +
|
|
102
|
+
'covering RoomService/Egress/Ingress/SIP-style requests, not SDK room operations ' +
|
|
103
|
+
'(docs.livekit.io/deploy/admin/quotas-and-limits/). Every call this connector makes is a Server API ' +
|
|
104
|
+
'call. 200 weighted units / 60s prices a flat list/get at 1, so the ceiling is 20% of that ' +
|
|
105
|
+
'project-wide limit — which the app\'s own server shares. ListParticipants and ListDispatch cost 2 ' +
|
|
106
|
+
'because the snapshot issues one PER ROOM, so their call count is unbounded in the room count — ' +
|
|
107
|
+
'a judgement call, not a published cost.',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Declared at module load, so merely importing this module (which `livekit-connector.ts` does) is
|
|
111
|
+
// enough to arm the real ceiling. A budget constructed BEFORE this runs falls back to the kernel's
|
|
112
|
+
// DEFAULT_RATE_BUDGET, which is tighter in call count but prices every call at 2 — cheaper than
|
|
113
|
+
// nothing here, yet it also prices a cheap read at 2, so the two are not ordered. `RateBudget` reads
|
|
114
|
+
// its policy live precisely so this declaration takes effect the moment it lands, and constructing
|
|
115
|
+
// through the subclass below (which imports this module) makes the ordering a non-issue in practice.
|
|
116
|
+
declareRateBudget(VENDOR, LIVEKIT_RATE_BUDGET);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Price one call. Keyed off the Twirp RPC the connector is ABOUT to invoke, so an unclassified method
|
|
120
|
+
* still costs `defaultWeight` — an unknown method must never be free.
|
|
121
|
+
*/
|
|
122
|
+
export function liveKitCallWeight(service: string, method: string): number {
|
|
123
|
+
return rateBudgetWeight(VENDOR, `${service}/${method}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Where LiveKit's ledger lives. API-key-keyed and cwd-independent by default (the limit is per
|
|
127
|
+
* PROJECT and the API key identifies the project, so a cwd-scoped ledger would hand the same project
|
|
128
|
+
* a fresh allowance in every checkout, worktree and CI matrix leg); pass `root` to opt into
|
|
129
|
+
* world-scoped accounting instead. */
|
|
130
|
+
export function liveKitBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
|
|
131
|
+
const o = typeof opts === 'string' ? { root: opts } : opts;
|
|
132
|
+
// VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
|
|
133
|
+
// excess-property check only catches object literals) must not redirect this pack's ledger to
|
|
134
|
+
// another vendor's file.
|
|
135
|
+
return rateBudgetPath({ ...o, vendor: VENDOR });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Construction options for LiveKit's budget. The vendor is fixed; everything else may only TIGHTEN. */
|
|
139
|
+
export type LiveKitBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* LiveKit's budget — the shared kernel guard bound to this vendor's declaration. A real subclass, not
|
|
143
|
+
* an alias, so `budget instanceof LiveKitBudget` in `liveLiveKitExecute` still means "a budget that
|
|
144
|
+
* accounts against LIVEKIT's ledger under LIVEKIT's ceiling": another vendor's `RateBudget` (with its
|
|
145
|
+
* own, possibly larger, ceiling) is NOT assignable there.
|
|
146
|
+
*/
|
|
147
|
+
export class LiveKitBudget extends RateBudget {
|
|
148
|
+
constructor(opts: LiveKitBudgetOptions = {}) {
|
|
149
|
+
super({ ...opts, vendor: VENDOR });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The typed refusal. One error class shared with every other vendor's budget; `err.vendor` says
|
|
154
|
+
* which one refused, and `err.kind` says why. */
|
|
155
|
+
export { RateBudgetError as LiveKitBudgetError } from '@volter/world-core';
|
|
156
|
+
export type { RateBudgetErrorKind as LiveKitBudgetErrorKind } from '@volter/world-core';
|
|
157
|
+
export type LiveKitBudgetReservation = RateBudgetReservation;
|
|
158
|
+
export type LiveKitBudgetSnapshot = RateBudgetSnapshot;
|