@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.
Files changed (99) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +106 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +23 -0
  5. package/dist/src/egress-service-cli.d.ts +2 -0
  6. package/dist/src/egress-service-cli.js +101 -0
  7. package/dist/src/index.d.ts +14 -0
  8. package/dist/src/index.js +53 -0
  9. package/dist/src/livekit-budget.d.ts +57 -0
  10. package/dist/src/livekit-budget.js +131 -0
  11. package/dist/src/livekit-capabilities.d.ts +3 -0
  12. package/dist/src/livekit-capabilities.js +728 -0
  13. package/dist/src/livekit-conformance.d.ts +8 -0
  14. package/dist/src/livekit-conformance.js +13 -0
  15. package/dist/src/livekit-connector.d.ts +130 -0
  16. package/dist/src/livekit-connector.js +382 -0
  17. package/dist/src/livekit-data.d.ts +86 -0
  18. package/dist/src/livekit-data.js +246 -0
  19. package/dist/src/livekit-perform-harness.d.ts +5 -0
  20. package/dist/src/livekit-perform-harness.js +17 -0
  21. package/dist/src/livekit-server.d.ts +23 -0
  22. package/dist/src/livekit-server.js +45 -0
  23. package/dist/src/livekit-service-cli.d.ts +2 -0
  24. package/dist/src/livekit-service-cli.js +88 -0
  25. package/dist/src/livekit-token.d.ts +68 -0
  26. package/dist/src/livekit-token.js +76 -0
  27. package/dist/src/livekit-twin.d.ts +4 -0
  28. package/dist/src/livekit-twin.js +1161 -0
  29. package/dist/src/livekit-types.d.ts +203 -0
  30. package/dist/src/livekit-types.js +1 -0
  31. package/dist/src/livekit-webhook.d.ts +26 -0
  32. package/dist/src/livekit-webhook.js +79 -0
  33. package/dist/src/redis-service-cli.d.ts +2 -0
  34. package/dist/src/redis-service-cli.js +35 -0
  35. package/dist/test-fixtures/livekit-openapi-operations.SOURCE.md +88 -0
  36. package/dist/test-fixtures/livekit-openapi-operations.json +442 -0
  37. package/dist/test-fixtures/protobufs/cloud_replay.proto +82 -0
  38. package/dist/test-fixtures/protobufs/livekit_agent.proto +185 -0
  39. package/dist/test-fixtures/protobufs/livekit_agent_dispatch.proto +103 -0
  40. package/dist/test-fixtures/protobufs/livekit_agent_simulation.proto +422 -0
  41. package/dist/test-fixtures/protobufs/livekit_agent_worker.proto +29 -0
  42. package/dist/test-fixtures/protobufs/livekit_agentdb.proto +226 -0
  43. package/dist/test-fixtures/protobufs/livekit_analytics.proto +312 -0
  44. package/dist/test-fixtures/protobufs/livekit_cloud_agent.proto +376 -0
  45. package/dist/test-fixtures/protobufs/livekit_connector.proto +41 -0
  46. package/dist/test-fixtures/protobufs/livekit_connector_twilio.proto +65 -0
  47. package/dist/test-fixtures/protobufs/livekit_connector_whatsapp.proto +171 -0
  48. package/dist/test-fixtures/protobufs/livekit_egress.proto +641 -0
  49. package/dist/test-fixtures/protobufs/livekit_ingress.proto +222 -0
  50. package/dist/test-fixtures/protobufs/livekit_internal.proto +228 -0
  51. package/dist/test-fixtures/protobufs/livekit_metrics.proto +103 -0
  52. package/dist/test-fixtures/protobufs/livekit_models.proto +992 -0
  53. package/dist/test-fixtures/protobufs/livekit_phone_number.proto +151 -0
  54. package/dist/test-fixtures/protobufs/livekit_room.proto +312 -0
  55. package/dist/test-fixtures/protobufs/livekit_rtc.proto +673 -0
  56. package/dist/test-fixtures/protobufs/livekit_sip.proto +1000 -0
  57. package/dist/test-fixtures/protobufs/livekit_token_source.proto +49 -0
  58. package/dist/test-fixtures/protobufs/livekit_webhook.proto +62 -0
  59. package/package.json +58 -0
  60. package/src/cli.ts +22 -0
  61. package/src/egress-service-cli.ts +111 -0
  62. package/src/index.ts +103 -0
  63. package/src/livekit-budget.ts +158 -0
  64. package/src/livekit-capabilities.ts +789 -0
  65. package/src/livekit-conformance.ts +15 -0
  66. package/src/livekit-connector.ts +406 -0
  67. package/src/livekit-data.ts +278 -0
  68. package/src/livekit-perform-harness.ts +17 -0
  69. package/src/livekit-server.ts +67 -0
  70. package/src/livekit-service-cli.ts +93 -0
  71. package/src/livekit-token.ts +133 -0
  72. package/src/livekit-twin.ts +1162 -0
  73. package/src/livekit-types.ts +211 -0
  74. package/src/livekit-webhook.ts +91 -0
  75. package/src/redis-service-cli.ts +39 -0
  76. package/test-fixtures/livekit-openapi-operations.SOURCE.md +88 -0
  77. package/test-fixtures/livekit-openapi-operations.json +442 -0
  78. package/test-fixtures/protobufs/cloud_replay.proto +82 -0
  79. package/test-fixtures/protobufs/livekit_agent.proto +185 -0
  80. package/test-fixtures/protobufs/livekit_agent_dispatch.proto +103 -0
  81. package/test-fixtures/protobufs/livekit_agent_simulation.proto +422 -0
  82. package/test-fixtures/protobufs/livekit_agent_worker.proto +29 -0
  83. package/test-fixtures/protobufs/livekit_agentdb.proto +226 -0
  84. package/test-fixtures/protobufs/livekit_analytics.proto +312 -0
  85. package/test-fixtures/protobufs/livekit_cloud_agent.proto +376 -0
  86. package/test-fixtures/protobufs/livekit_connector.proto +41 -0
  87. package/test-fixtures/protobufs/livekit_connector_twilio.proto +65 -0
  88. package/test-fixtures/protobufs/livekit_connector_whatsapp.proto +171 -0
  89. package/test-fixtures/protobufs/livekit_egress.proto +641 -0
  90. package/test-fixtures/protobufs/livekit_ingress.proto +222 -0
  91. package/test-fixtures/protobufs/livekit_internal.proto +228 -0
  92. package/test-fixtures/protobufs/livekit_metrics.proto +103 -0
  93. package/test-fixtures/protobufs/livekit_models.proto +992 -0
  94. package/test-fixtures/protobufs/livekit_phone_number.proto +151 -0
  95. package/test-fixtures/protobufs/livekit_room.proto +312 -0
  96. package/test-fixtures/protobufs/livekit_rtc.proto +673 -0
  97. package/test-fixtures/protobufs/livekit_sip.proto +1000 -0
  98. package/test-fixtures/protobufs/livekit_token_source.proto +49 -0
  99. 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;