@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,211 @@
1
+ export type LiveKitRequest = {
2
+ method: string;
3
+ path: string;
4
+ body?: string;
5
+ root?: string;
6
+ readOnly?: boolean;
7
+ occurredAt?: string;
8
+ /** Bearer access token (LiveKit JWT). When `apiKeys` is set, auth is enforced. */
9
+ authToken?: string;
10
+ /** apiKey -> apiSecret map. Presence enables JWT grant enforcement (vendor-faithful auth). */
11
+ apiKeys?: Record<string, string>;
12
+ /** Now (unix seconds) for token exp/nbf checks; defaults to occurredAt. */
13
+ nowSeconds?: number;
14
+ /** Max simultaneously-active egress jobs before quota_capacity is returned. */
15
+ maxActiveEgress?: number;
16
+ };
17
+
18
+ export type LiveKitResponse = { status: number; body: unknown };
19
+
20
+ export type LiveKitRoom = {
21
+ id: string;
22
+ sid: string;
23
+ name: string;
24
+ emptyTimeout: number;
25
+ departureTimeout: number;
26
+ maxParticipants: number;
27
+ metadata: string;
28
+ creationTime: string;
29
+ creationTimeMs: string;
30
+ numParticipants: number;
31
+ activeRecording: boolean;
32
+ generation: number;
33
+ deleted?: boolean;
34
+ };
35
+
36
+ export type LiveKitTrack = {
37
+ sid: string;
38
+ name: string;
39
+ type: number;
40
+ muted: boolean;
41
+ width: number;
42
+ height: number;
43
+ source: number;
44
+ mimeType: string;
45
+ };
46
+
47
+ export type LiveKitParticipantPermission = {
48
+ canSubscribe: boolean;
49
+ canPublish: boolean;
50
+ canPublishData: boolean;
51
+ canUpdateMetadata: boolean;
52
+ hidden: boolean;
53
+ canSubscribeMetrics: boolean;
54
+ };
55
+
56
+ export type LiveKitParticipant = {
57
+ id: string;
58
+ sid: string;
59
+ roomName: string;
60
+ identity: string;
61
+ state: number;
62
+ tracks: LiveKitTrack[];
63
+ metadata: string;
64
+ joinedAt: string;
65
+ joinedAtMs: string;
66
+ name: string;
67
+ version: number;
68
+ isPublisher: boolean;
69
+ kind: number;
70
+ attributes: Record<string, string>;
71
+ permission: LiveKitParticipantPermission;
72
+ /** track sids this participant is subscribed to (UpdateSubscriptions) */
73
+ subscribedTracks: string[];
74
+ };
75
+
76
+ export type EgressRequestCase = 'roomComposite' | 'web' | 'participant' | 'trackComposite' | 'track';
77
+
78
+ export type LiveKitEgress = {
79
+ id: string;
80
+ egressId: string;
81
+ roomId: string;
82
+ roomName: string;
83
+ sourceType: number;
84
+ status: number;
85
+ startedAt: string;
86
+ endedAt: string;
87
+ updatedAt: string;
88
+ details: string;
89
+ error: string;
90
+ errorCode: number;
91
+ layout: string;
92
+ requestCase: EgressRequestCase;
93
+ streamUrls: string[];
94
+ fileOutputs: unknown[];
95
+ streamOutputs: unknown[];
96
+ segmentOutputs: unknown[];
97
+ imageOutputs: unknown[];
98
+ request: Record<string, unknown>;
99
+ };
100
+
101
+ export type LiveKitIngress = {
102
+ id: string;
103
+ ingressId: string;
104
+ name: string;
105
+ streamKey: string;
106
+ url: string;
107
+ inputType: number;
108
+ bypassTranscoding: boolean;
109
+ enableTranscoding: boolean;
110
+ roomName: string;
111
+ participantIdentity: string;
112
+ participantName: string;
113
+ participantMetadata: string;
114
+ reusable: boolean;
115
+ enabled: boolean;
116
+ audio?: Record<string, unknown>;
117
+ video?: Record<string, unknown>;
118
+ state: {
119
+ status: number;
120
+ error: string;
121
+ roomId: string;
122
+ startedAt: string;
123
+ endedAt: string;
124
+ updatedAt: string;
125
+ resourceId: string;
126
+ };
127
+ deleted?: boolean;
128
+ };
129
+
130
+ export type LiveKitSIPInboundTrunk = {
131
+ id: string;
132
+ sipTrunkId: string;
133
+ name: string;
134
+ metadata: string;
135
+ numbers: string[];
136
+ allowedAddresses: string[];
137
+ allowedNumbers: string[];
138
+ authUsername: string;
139
+ authPassword: string;
140
+ headers: Record<string, string>;
141
+ headersToAttributes: Record<string, string>;
142
+ krispEnabled: boolean;
143
+ includeHeaders: string;
144
+ mediaEncryption: string;
145
+ deleted?: boolean;
146
+ };
147
+
148
+ export type LiveKitSIPOutboundTrunk = {
149
+ id: string;
150
+ sipTrunkId: string;
151
+ name: string;
152
+ metadata: string;
153
+ address: string;
154
+ transport: string;
155
+ numbers: string[];
156
+ authUsername: string;
157
+ authPassword: string;
158
+ headers: Record<string, string>;
159
+ headersToAttributes: Record<string, string>;
160
+ includeHeaders: string;
161
+ mediaEncryption: string;
162
+ destinationCountry: string;
163
+ deleted?: boolean;
164
+ };
165
+
166
+ /** SIP dispatch rule: the protobuf `rule` oneof is either dispatchRuleDirect or dispatchRuleIndividual. */
167
+ export type LiveKitSIPDispatchRule = {
168
+ id: string;
169
+ sipDispatchRuleId: string;
170
+ name: string;
171
+ metadata: string;
172
+ trunkIds: string[];
173
+ hidePhoneNumber: boolean;
174
+ attributes: Record<string, string>;
175
+ roomPreset: string;
176
+ inboundNumbers: string[];
177
+ /** the active oneof case + its value (direct: { roomName, pin }; individual: { roomPrefix, pin }) */
178
+ rule: { case: 'dispatchRuleDirect' | 'dispatchRuleIndividual'; value: Record<string, unknown> };
179
+ deleted?: boolean;
180
+ };
181
+
182
+ export type LiveKitSIPParticipant = {
183
+ id: string;
184
+ participantId: string;
185
+ participantIdentity: string;
186
+ roomName: string;
187
+ sipCallId: string;
188
+ sipTrunkId: string;
189
+ sipCallTo: string;
190
+ fromNumber: string;
191
+ };
192
+
193
+ export type LiveKitAgentDispatch = {
194
+ id: string;
195
+ dispatchId: string;
196
+ agentName: string;
197
+ room: string;
198
+ metadata: string;
199
+ createdAt: string;
200
+ deleted?: boolean;
201
+ };
202
+
203
+ export type LiveKitWebhookEvent = {
204
+ event: string;
205
+ id: string;
206
+ createdAt: number;
207
+ room?: Record<string, unknown>;
208
+ participant?: Record<string, unknown>;
209
+ egressInfo?: Record<string, unknown>;
210
+ ingressInfo?: Record<string, unknown>;
211
+ };
@@ -0,0 +1,91 @@
1
+ import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
2
+ import type { LiveKitWebhookEvent } from './livekit-types.ts';
3
+
4
+ // LiveKit webhooks POST a JSON event body plus an `Authorization` header that is
5
+ // a signed JWT (same HS256 scheme as access tokens) whose `sha256` claim is the
6
+ // base64 (standard padding) SHA-256 digest of the raw body. The vendor SDK's
7
+ // WebhookReceiver.receive() verifies the JWT and then checks the body hash.
8
+ // We replicate the producer side (build the body + signed header) and a verifier
9
+ // that is byte-compatible with WebhookReceiver — see livekit-twin.test.ts.
10
+
11
+ function base64url(input: Buffer | string): string {
12
+ return Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
13
+ }
14
+
15
+ function base64urlDecode(input: string): Buffer {
16
+ const pad = input.length % 4 === 0 ? '' : '='.repeat(4 - (input.length % 4));
17
+ return Buffer.from(input.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64');
18
+ }
19
+
20
+ function bodyHash(body: string): string {
21
+ return createHash('sha256').update(body).digest('base64');
22
+ }
23
+
24
+ function sign(signingInput: string, secret: string): string {
25
+ return base64url(createHmac('sha256', secret).update(signingInput).digest());
26
+ }
27
+
28
+ export type WebhookEventInput = {
29
+ event: LiveKitWebhookEvent['event'];
30
+ id?: string;
31
+ createdAtSeconds?: number;
32
+ room?: Record<string, unknown>;
33
+ participant?: Record<string, unknown>;
34
+ egressInfo?: Record<string, unknown>;
35
+ ingressInfo?: Record<string, unknown>;
36
+ };
37
+
38
+ /** Serialize a webhook event to its JSON body string. */
39
+ export function buildWebhookBody(input: WebhookEventInput): string {
40
+ const event: Record<string, unknown> = {
41
+ event: input.event,
42
+ id: input.id ?? `EV_${createHash('sha256').update(`${input.event}:${input.id ?? ''}`).digest('hex').slice(0, 16)}`,
43
+ createdAt: input.createdAtSeconds ?? 0,
44
+ };
45
+ if (input.room) event.room = input.room;
46
+ if (input.participant) event.participant = input.participant;
47
+ if (input.egressInfo) event.egressInfo = input.egressInfo;
48
+ if (input.ingressInfo) event.ingressInfo = input.ingressInfo;
49
+ return JSON.stringify(event);
50
+ }
51
+
52
+ /** Build the signed `Authorization` header JWT for a webhook body. */
53
+ export function signWebhookHeader(body: string, apiKey: string, apiSecret: string, nowSeconds = 0, ttlSeconds = 6 * 60 * 60): string {
54
+ const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
55
+ const payload = base64url(JSON.stringify({ iss: apiKey, nbf: nowSeconds, exp: nowSeconds + ttlSeconds, sha256: bodyHash(body) }));
56
+ const signingInput = `${header}.${payload}`;
57
+ return `${signingInput}.${sign(signingInput, apiSecret)}`;
58
+ }
59
+
60
+ export type WebhookVerifyResult =
61
+ | { ok: true; event: LiveKitWebhookEvent }
62
+ | { ok: false; reason: 'missing_header' | 'malformed' | 'bad_signature' | 'expired' | 'hash_mismatch' };
63
+
64
+ /** Verify a webhook delivery (header JWT signature + body sha256), byte-compatible with WebhookReceiver. */
65
+ export function verifyWebhook(body: string, authHeader: string | undefined, apiSecret: string, opts: { nowSeconds?: number; clockToleranceSeconds?: number } = {}): WebhookVerifyResult {
66
+ if (!authHeader) return { ok: false, reason: 'missing_header' };
67
+ const parts = authHeader.split('.');
68
+ if (parts.length !== 3) return { ok: false, reason: 'malformed' };
69
+ const [h, p, sig] = parts as [string, string, string];
70
+ let claims: { exp?: number; nbf?: number; sha256?: string };
71
+ try {
72
+ claims = JSON.parse(base64urlDecode(p).toString('utf8'));
73
+ } catch {
74
+ return { ok: false, reason: 'malformed' };
75
+ }
76
+ const expected = sign(`${h}.${p}`, apiSecret);
77
+ const a = Buffer.from(sig);
78
+ const b = Buffer.from(expected);
79
+ if (a.length !== b.length || !timingSafeEqual(a, b)) return { ok: false, reason: 'bad_signature' };
80
+ const now = opts.nowSeconds ?? 0;
81
+ const tol = opts.clockToleranceSeconds ?? 0;
82
+ if (typeof claims.exp === 'number' && now > claims.exp + tol) return { ok: false, reason: 'expired' };
83
+ if (claims.sha256 !== bodyHash(body)) return { ok: false, reason: 'hash_mismatch' };
84
+ let event: LiveKitWebhookEvent;
85
+ try {
86
+ event = JSON.parse(body);
87
+ } catch {
88
+ return { ok: false, reason: 'malformed' };
89
+ }
90
+ return { ok: true, event };
91
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ // @ts-nocheck
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+
5
+ const PORT = process.env.PORT;
6
+ if (!PORT) throw new Error('PORT is required');
7
+
8
+ const name = `volter-livekit-redis-${PORT}`;
9
+ const args = [
10
+ 'run',
11
+ '--rm',
12
+ '--name',
13
+ name,
14
+ '-p',
15
+ `127.0.0.1:${PORT}:6379`,
16
+ 'redis:7-alpine',
17
+ 'redis-server',
18
+ '--save',
19
+ '',
20
+ '--appendonly',
21
+ 'no',
22
+ ];
23
+
24
+ const child = spawn('docker', args, { stdio: 'inherit' });
25
+
26
+ // Synchronous on purpose: the handler exits the process right after, and a spawned `docker rm`
27
+ // that never ran is how a `volter-livekit-redis-<port>` container outlives every World that made it.
28
+ function stop() {
29
+ try {
30
+ spawnSync('docker', ['rm', '-f', name], { stdio: 'ignore', timeout: 20_000 });
31
+ } catch {
32
+ // best effort
33
+ }
34
+ }
35
+
36
+ process.on('SIGTERM', () => { stop(); process.exit(0); });
37
+ process.on('SIGINT', () => { stop(); process.exit(0); });
38
+
39
+ child.on('exit', (code) => process.exit(code ?? 0));
@@ -0,0 +1,88 @@
1
+ # livekit-openapi-operations.json — provenance
2
+
3
+ The operation-inventory denominator for `../livekit-spec-census.json`: a scope-filtered subset of
4
+ the vendor's published spec, enumerated by `deriveFromOpenAPI` (`packages/world-tooling/src/derive.ts`).
5
+
6
+ - **Upstream:** https://github.com/livekit/protocol — `the 22 top-level .proto files of livekit/protocol@09c9697b`.
7
+ - **How it was built:** `bun scripts/spec-proto-to-openapi.ts <out.json> --style twirp --fetched 2026-09-02 --upstream https://github.com/livekit/protocol test-fixtures/protobufs/*.proto`
8
+ over the vendored copies of the upstream files fetched 2026-09-02, then scope-filtered by
9
+ `buildScopedFixture` (`scripts/spec-fixture-build.ts`, the same code path `spec-census-bootstrap.ts`
10
+ runs); operation metadata only (operationId, summary, tags, `deprecated`, first response, servers),
11
+ schemas dropped. The gate never fetches — refreshing the fixture is re-running that conversion over a
12
+ newer checkout of the protocol repo.
13
+ - **Scope (47 of 94 upstream operations):** every operation whose path starts with one of
14
+ `/twirp/livekit.RoomService/`, `/twirp/livekit.Egress/`, `/twirp/livekit.Ingress/`, `/twirp/livekit.SIP/`, `/twirp/livekit.AgentDispatchService/`.
15
+ - **Mapping at birth:** 0 operations mapped to done capability ids by the rules in `scripts/spec-census-map.ts`
16
+ behind the static served-surface probe; 47 left `unmapped`. The census, not this file, records what
17
+ happened next: hand rulings (a `$ruled` marker, a remap) and the per-operation probe's demotions
18
+ (`$probeComment`). This fixture changes only through a spec refresh.
19
+
20
+ ## Derivation — livekit publishes NO OpenAPI
21
+
22
+ `docs.livekit.io/openapi.json` answers 200 with the docs site's HTML, not a spec. LiveKit's server
23
+ APIs are declared first-party as protobuf/Twirp service definitions in the open-source protocol
24
+ repo, so the paths in this fixture come from the twitchtv/twirp v7 WIRE convention rather than from
25
+ a document: for every `rpc <Method>` inside every `service <Service> { … }` block, one operation
26
+
27
+ POST /twirp/<the file's `package`>.<Service>/<Method>
28
+
29
+ with `operationId` `<Service>.<Method>`, `tags` `[<Service>]`, `summary` from the rpc's leading `//`
30
+ comment run, and `deprecated: true` when the rpc declares `option deprecated = true`. All 22 files
31
+ declare `package livekit;`, so every path is `/twirp/livekit.<Service>/<Method>` — the exact shape
32
+ `route()` in `../src/livekit-twin.ts` parses (`/^\/twirp\/livekit\.([^/]+)\/([^/?]+)$/`). These paths
33
+ are really on the wire; nothing here is invented. 11 services, 94 rpcs at `livekit/protocol@09c9697b`
34
+ (2026-09-02); 47 of them fall inside the scope above.
35
+
36
+ ## The source documents are VENDORED, and a test re-derives this file from them
37
+
38
+ `protobufs/*.proto` ARE the 22 top-level `.proto` files of `livekit/protocol@09c9697b` — the head
39
+ read when this census was born, fetched from
40
+ `https://codeload.github.com/livekit/protocol/tar.gz/09c9697b`. They are committed so the derivation
41
+ can be re-run without a network.
42
+
43
+ `../src/livekit-spec-census.test.ts` re-runs the whole derivation over them — `protoToOpenApi`
44
+ (`scripts/spec-proto-to-openapi.ts`, `--style twirp`) then `buildScopedFixture`
45
+ (`scripts/spec-fixture-build.ts`) — and asserts the result is BYTE-IDENTICAL to this file. It also
46
+ asserts the vendored set really declares the 11 services and 94 rpcs `../livekit-spec-census.json`
47
+ counts, and that `SIP/ListSIPTrunk` still carries the deprecation the proto declares. That test is
48
+ the drift tooth: this fixture cannot silently stop being the derivation of the documents beside it.
49
+ Nothing is fetched at test time.
50
+
51
+ The vendored documents and the test are declared in `../census.json` under `spec.rederivation`,
52
+ where `bun scripts/spec-refresh.ts --self-check` checks they exist — the second of the two machine
53
+ refresh paths a committed census must have.
54
+
55
+ ## RE-DERIVED 2026-09-03 with the committed converter (a ratified artifact revision)
56
+
57
+ This fixture was originally built by an AD-HOC rule, written out in prose in this file and committed
58
+ nowhere: `scripts/spec-proto-to-openapi.ts` did not exist yet, and the note here said so. It exists
59
+ now, and this fixture is its output over the same 22 protos at the same pin. The **route set did not
60
+ move** — the same 47 in-scope operations, so no census row changed — but the operation metadata did:
61
+
62
+ | field | operations | was (ad-hoc rule) | is (committed converter) |
63
+ |---|---|---|---|
64
+ | `operationId` | 47 / 47 | `SIP_ListSIPTrunk` | `SIP.ListSIPTrunk` |
65
+ | first response `description` | 47 / 47 | `ListSIPTrunk response` | `ok` |
66
+ | `deprecated` | 1 / 47 | absent on `SIP/ListSIPTrunk` | `true` |
67
+ | `info.title` / `info.version` | — | `LiveKit server APIs (Twirp, …)` / `09c9697b` | the converter's title / the fetch date |
68
+
69
+ `summary` and `tags` are unchanged on all 47.
70
+
71
+ The `deprecated` row is a **RESTORATION, not a change**: `livekit_sip.proto` has always marked
72
+ `ListSIPTrunk` `option deprecated = true` (in favour of `ListSIPInboundTrunk` /
73
+ `ListSIPOutboundTrunk`), the census's `$unmappedComment` already rules on the operation *because* of
74
+ that deprecation, and the ad-hoc rule simply dropped the fact on the floor. The pin
75
+ (`livekit/protocol@09c9697b`, fetched 2026-09-02) is untouched, so nothing here is a vendor change.
76
+
77
+ Two converter defects were fixed to make this derivation true to the source, both proven by the
78
+ counts this file records:
79
+
80
+ - A wrapped signature (`rpc Foo(Req)\n returns (Res) {}`) was not recognised, which silently
81
+ dropped all six rpcs of `PhoneNumberService` — the converter saw 88 rpcs where the protos declare
82
+ 94. All six are OUTSIDE this fixture's scope, so no in-scope route moved; what they moved is the
83
+ converter's agreement with the census's `fullSpecEstimateOps: 94`, which the test now asserts.
84
+ - A `//` block separated from an rpc by a BLANK line was still taken as that rpc's documentation.
85
+ `livekit_sip.proto` keeps a commented-out `rpc CreateSIPTrunk(…) … DELETED` line one blank line
86
+ above `ListSIPTrunk`, so the live rpc was captioned with a deleted one's signature. `ListSIPTrunk`
87
+ now has the empty summary the proto actually gives it — which is what the committed fixture
88
+ already said.