@pymthouse/builder-sdk 0.1.0 → 0.3.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 (77) hide show
  1. package/README.md +66 -0
  2. package/dist/{client-BroVFyIy.d.ts → client-BHfjDvIe.d.ts} +49 -1
  3. package/dist/{client-BhC1YhB1.d.cts → client-CvhJEhjV.d.cts} +49 -1
  4. package/dist/config.cjs +59 -3
  5. package/dist/config.cjs.map +1 -1
  6. package/dist/config.d.cts +8 -1
  7. package/dist/config.d.ts +8 -1
  8. package/dist/config.js +57 -4
  9. package/dist/config.js.map +1 -1
  10. package/dist/device-initiate.cjs +1 -1
  11. package/dist/device-initiate.cjs.map +1 -1
  12. package/dist/device-initiate.js +1 -1
  13. package/dist/device-initiate.js.map +1 -1
  14. package/dist/device.cjs +1 -1
  15. package/dist/device.cjs.map +1 -1
  16. package/dist/device.d.cts +1 -1
  17. package/dist/device.d.ts +1 -1
  18. package/dist/device.js +1 -1
  19. package/dist/device.js.map +1 -1
  20. package/dist/env.cjs +794 -36
  21. package/dist/env.cjs.map +1 -1
  22. package/dist/env.d.cts +2 -2
  23. package/dist/env.d.ts +2 -2
  24. package/dist/env.js +794 -36
  25. package/dist/env.js.map +1 -1
  26. package/dist/gateway/client/index.cjs +492 -0
  27. package/dist/gateway/client/index.cjs.map +1 -0
  28. package/dist/gateway/client/index.d.cts +63 -0
  29. package/dist/gateway/client/index.d.ts +63 -0
  30. package/dist/gateway/client/index.js +489 -0
  31. package/dist/gateway/client/index.js.map +1 -0
  32. package/dist/gateway/index.cjs +16 -0
  33. package/dist/gateway/index.cjs.map +1 -0
  34. package/dist/gateway/index.d.cts +52 -0
  35. package/dist/gateway/index.d.ts +52 -0
  36. package/dist/gateway/index.js +10 -0
  37. package/dist/gateway/index.js.map +1 -0
  38. package/dist/gateway/server/index.cjs +1248 -0
  39. package/dist/gateway/server/index.cjs.map +1 -0
  40. package/dist/gateway/server/index.d.cts +31 -0
  41. package/dist/gateway/server/index.d.ts +31 -0
  42. package/dist/gateway/server/index.js +1233 -0
  43. package/dist/gateway/server/index.js.map +1 -0
  44. package/dist/index.cjs +1075 -186
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +6 -4
  47. package/dist/index.d.ts +6 -4
  48. package/dist/index.js +1042 -163
  49. package/dist/index.js.map +1 -1
  50. package/dist/ingest-B3Yi8Tb1.d.cts +271 -0
  51. package/dist/ingest-DoKJTWU9.d.ts +271 -0
  52. package/dist/plan-pricing.cjs +108 -0
  53. package/dist/plan-pricing.cjs.map +1 -0
  54. package/dist/plan-pricing.d.cts +15 -0
  55. package/dist/plan-pricing.d.ts +15 -0
  56. package/dist/plan-pricing.js +98 -0
  57. package/dist/plan-pricing.js.map +1 -0
  58. package/dist/signer/server.cjs +1366 -0
  59. package/dist/signer/server.cjs.map +1 -0
  60. package/dist/signer/server.d.cts +73 -0
  61. package/dist/signer/server.d.ts +73 -0
  62. package/dist/signer/server.js +1331 -0
  63. package/dist/signer/server.js.map +1 -0
  64. package/dist/tokens.d.cts +1 -1
  65. package/dist/tokens.d.ts +1 -1
  66. package/dist/types-_R1AwEZp.d.cts +343 -0
  67. package/dist/types-_R1AwEZp.d.ts +343 -0
  68. package/dist/verify.cjs +1 -1
  69. package/dist/verify.cjs.map +1 -1
  70. package/dist/verify.d.cts +1 -1
  71. package/dist/verify.d.ts +1 -1
  72. package/dist/verify.js +1 -1
  73. package/dist/verify.js.map +1 -1
  74. package/gateway/proto/lp_rpc.proto +542 -0
  75. package/package.json +42 -1
  76. package/dist/types-rKzVXvMu.d.cts +0 -196
  77. package/dist/types-rKzVXvMu.d.ts +0 -196
@@ -0,0 +1,1233 @@
1
+ import http from 'http';
2
+ import https from 'https';
3
+ import { createRequire } from 'module';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import tls from 'tls';
7
+ import { createHash, randomUUID } from 'crypto';
8
+
9
+ // src/string-utils.ts
10
+ function stripTrailingSlashes(value) {
11
+ let end = value.length;
12
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
13
+ end--;
14
+ }
15
+ return value.slice(0, end);
16
+ }
17
+ function endsWithIgnoreCase(value, suffix) {
18
+ if (suffix.length > value.length) {
19
+ return false;
20
+ }
21
+ const start = value.length - suffix.length;
22
+ for (let i = 0; i < suffix.length; i++) {
23
+ const a = value.codePointAt(start + i) ?? 0;
24
+ const b = suffix.codePointAt(i) ?? 0;
25
+ if (a !== b && (a | 32) !== (b | 32)) {
26
+ return false;
27
+ }
28
+ }
29
+ return true;
30
+ }
31
+ function stripSuffixIgnoreCase(value, suffix) {
32
+ return endsWithIgnoreCase(value, suffix) ? value.slice(0, value.length - suffix.length) : value;
33
+ }
34
+ function stripIssuerOriginFromOidcUrl(issuerUrl) {
35
+ let base = stripTrailingSlashes(issuerUrl.trim());
36
+ base = stripSuffixIgnoreCase(base, "/api/v1/oidc");
37
+ base = stripSuffixIgnoreCase(base, "/oidc");
38
+ return stripTrailingSlashes(base);
39
+ }
40
+
41
+ // src/gateway/server/config.ts
42
+ function readGatewayConfigFromEnv(env = process.env) {
43
+ const enabled = env.GATEWAY_ENABLED === "1" || env.NEXT_PUBLIC_GATEWAY_ENABLED === "1";
44
+ if (!enabled) {
45
+ return null;
46
+ }
47
+ const issuerUrl = env.PYMTHOUSE_ISSUER_URL?.trim();
48
+ const signerUrl = env.PYMTHOUSE_SIGNER_URL?.trim() || env.SIGNER_PUBLIC_URL?.trim() || (issuerUrl ? `${stripIssuerOriginFromOidcUrl(issuerUrl)}/api/signer` : "");
49
+ if (!signerUrl) {
50
+ return null;
51
+ }
52
+ const discoveryUrl = env.LIVEPEER_DISCOVERY_SERVICE_URL?.trim() || env.GATEWAY_DISCOVERY_URL?.trim() || void 0;
53
+ const discoveryTimeoutMs = Number(env.GATEWAY_DISCOVERY_TIMEOUT_MS ?? "60000");
54
+ const paymentIntervalMs = Number(env.GATEWAY_PAYMENT_INTERVAL_MS ?? "2000");
55
+ return {
56
+ enabled: true,
57
+ signerUrl,
58
+ discoveryUrl,
59
+ discoveryTimeoutMs: Number.isFinite(discoveryTimeoutMs) ? discoveryTimeoutMs : 6e4,
60
+ useTofu: env.GATEWAY_USE_TOFU !== "0",
61
+ paymentIntervalMs: Number.isFinite(paymentIntervalMs) ? paymentIntervalMs : 2e3
62
+ };
63
+ }
64
+
65
+ // src/gateway/server/auth.ts
66
+ function isAsciiWhitespace(code) {
67
+ return code <= 32;
68
+ }
69
+ function bearerTokenStart(header) {
70
+ const prefix = "bearer";
71
+ if (header.length < prefix.length) {
72
+ return -1;
73
+ }
74
+ for (let i = 0; i < prefix.length; i++) {
75
+ if (((header.codePointAt(i) ?? 0) | 32) !== prefix.codePointAt(i)) {
76
+ return -1;
77
+ }
78
+ }
79
+ let start = prefix.length;
80
+ while (start < header.length && isAsciiWhitespace(header.codePointAt(start) ?? 0)) {
81
+ start++;
82
+ }
83
+ return start < header.length ? start : -1;
84
+ }
85
+ function extractBearerToken(request) {
86
+ const header = request.headers.get("Authorization")?.trim();
87
+ if (!header) {
88
+ return null;
89
+ }
90
+ const start = bearerTokenStart(header);
91
+ if (start < 0) {
92
+ return null;
93
+ }
94
+ let end = header.length;
95
+ while (end > start && isAsciiWhitespace(header.codePointAt(end - 1) ?? 0)) {
96
+ end--;
97
+ }
98
+ return header.slice(start, end);
99
+ }
100
+ function unauthorizedResponse(message = "unauthorized") {
101
+ return Response.json({ error: message }, { status: 401 });
102
+ }
103
+ function forbiddenResponse(message = "forbidden") {
104
+ return Response.json({ error: message }, { status: 403 });
105
+ }
106
+ function disabledResponse() {
107
+ return Response.json(
108
+ {
109
+ error: "gateway_disabled",
110
+ error_description: "Set GATEWAY_ENABLED=1 to enable the browser gateway relay"
111
+ },
112
+ { status: 503 }
113
+ );
114
+ }
115
+
116
+ // src/gateway/types.ts
117
+ var LIVE_VIDEO_TO_VIDEO_CAPABILITY_ID = 35;
118
+ var DEFAULT_TRICKLE_MIME_TYPE = "video/mp2t";
119
+ var DEFAULT_DISCOVERY_TIMEOUT_MS = 6e4;
120
+ var TRICKLE_SEQ_LATEST = -1;
121
+ var TRICKLE_SEQ_CURRENT = -2;
122
+
123
+ // src/gateway/server/capabilities.ts
124
+ function modelCapabilityQuery(modelId) {
125
+ return `live-video-to-video/${modelId.trim()}`;
126
+ }
127
+ function buildLv2vCapabilitiesMessage(modelId) {
128
+ const capId = LIVE_VIDEO_TO_VIDEO_CAPABILITY_ID;
129
+ return {
130
+ capacities: { [capId]: 1 },
131
+ constraints: {
132
+ PerCapability: {
133
+ [capId]: {
134
+ models: {
135
+ [modelId.trim()]: {}
136
+ }
137
+ }
138
+ }
139
+ }
140
+ };
141
+ }
142
+ function capabilityForDiscoveryUrl(url, modelId) {
143
+ if (!url.includes("/v1/discovery/raw")) {
144
+ return modelCapabilityQuery(modelId);
145
+ }
146
+ if (!modelId.includes("/")) {
147
+ return modelId;
148
+ }
149
+ const segment = modelId.split("/").pop();
150
+ return segment ?? modelId;
151
+ }
152
+ function appendCapabilityQuery(url, modelId) {
153
+ const parsed = new URL(url);
154
+ parsed.searchParams.append("caps", capabilityForDiscoveryUrl(url, modelId));
155
+ return parsed.toString();
156
+ }
157
+ var insecureHttpsAgent = new https.Agent({
158
+ rejectUnauthorized: false
159
+ });
160
+ function encodeRequestBody(raw) {
161
+ if (raw === void 0) {
162
+ return void 0;
163
+ }
164
+ if (raw instanceof Buffer) {
165
+ return raw;
166
+ }
167
+ return Buffer.from(raw);
168
+ }
169
+ async function insecureFetch(url, init = {}) {
170
+ const parsed = new URL(url.includes("://") ? url : `https://${url}`);
171
+ const isHttps = parsed.protocol === "https:";
172
+ const lib = isHttps ? https : http;
173
+ const timeoutMs = init.timeoutMs ?? 6e4;
174
+ const body = encodeRequestBody(init.body);
175
+ const defaultMethod = body === void 0 ? "GET" : "POST";
176
+ return new Promise((resolve, reject) => {
177
+ const headers = { ...init.headers };
178
+ if (body !== void 0 && !headers["Content-Length"]) {
179
+ headers["Content-Length"] = String(body.length);
180
+ }
181
+ const req = lib.request(
182
+ parsed,
183
+ {
184
+ method: init.method ?? defaultMethod,
185
+ headers,
186
+ agent: isHttps ? insecureHttpsAgent : void 0,
187
+ rejectUnauthorized: false
188
+ },
189
+ (res) => {
190
+ const chunks = [];
191
+ res.on("data", (chunk) => chunks.push(chunk));
192
+ res.on("end", () => {
193
+ const merged = Buffer.concat(chunks);
194
+ const responseHeaders = new Headers();
195
+ for (const [key, value] of Object.entries(res.headers)) {
196
+ if (value === void 0) {
197
+ continue;
198
+ }
199
+ if (Array.isArray(value)) {
200
+ for (const item of value) {
201
+ responseHeaders.append(key, item);
202
+ }
203
+ } else {
204
+ responseHeaders.set(key, value);
205
+ }
206
+ }
207
+ resolve(
208
+ new Response(merged, {
209
+ status: res.statusCode ?? 0,
210
+ headers: responseHeaders
211
+ })
212
+ );
213
+ });
214
+ }
215
+ );
216
+ const timer = setTimeout(() => {
217
+ req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
218
+ }, timeoutMs);
219
+ if (init.signal) {
220
+ init.signal.addEventListener("abort", () => {
221
+ req.destroy(new Error("aborted"));
222
+ });
223
+ }
224
+ req.on("error", (err) => {
225
+ clearTimeout(timer);
226
+ reject(err);
227
+ });
228
+ req.on("close", () => clearTimeout(timer));
229
+ if (body !== void 0) {
230
+ req.write(body);
231
+ }
232
+ req.end();
233
+ });
234
+ }
235
+ async function readJsonResponse(response) {
236
+ const text = await response.text();
237
+ if (!text.trim()) {
238
+ return {};
239
+ }
240
+ return JSON.parse(text);
241
+ }
242
+ function httpOrigin(url) {
243
+ const parsed = new URL(url.includes("://") ? url : `https://${url}`);
244
+ return `${parsed.protocol}//${parsed.host}`;
245
+ }
246
+
247
+ // src/gateway/server/discovery.ts
248
+ var DISCOVERY_SERVICE_RAW_PATH = "/v1/discovery/raw";
249
+ function isDiscoveryServiceEndpoint(url) {
250
+ return url.includes(DISCOVERY_SERVICE_RAW_PATH);
251
+ }
252
+ function normalizeDiscoveryServiceUrl(url) {
253
+ const parsed = new URL(url.trim());
254
+ let path2 = parsed.pathname.replace(/\/$/, "");
255
+ if (!path2.endsWith(DISCOVERY_SERVICE_RAW_PATH)) {
256
+ if (path2.endsWith("/v1/discovery") || !path2) {
257
+ path2 = DISCOVERY_SERVICE_RAW_PATH;
258
+ }
259
+ }
260
+ parsed.pathname = path2;
261
+ if (!parsed.searchParams.has("serviceType")) {
262
+ parsed.searchParams.set("serviceType", "legacy");
263
+ }
264
+ return parsed.toString();
265
+ }
266
+ function resolveDiscoveryEndpoint(input) {
267
+ if (input.orchestratorUrl?.trim()) {
268
+ const list = input.orchestratorUrl.split(",").map((s) => s.trim()).filter(Boolean);
269
+ if (list.length > 0) {
270
+ return { url: "", headers: input.signerHeaders, discoveryService: false };
271
+ }
272
+ }
273
+ if (input.discoveryUrl?.trim()) {
274
+ let url = input.discoveryUrl.trim();
275
+ const discoveryService = isDiscoveryServiceEndpoint(url);
276
+ if (discoveryService) {
277
+ url = normalizeDiscoveryServiceUrl(url);
278
+ }
279
+ url = appendCapabilityQuery(url, input.modelId);
280
+ return {
281
+ url,
282
+ headers: input.signerHeaders,
283
+ discoveryService
284
+ };
285
+ }
286
+ if (input.signerUrl?.trim()) {
287
+ const url = appendCapabilityQuery(
288
+ `${httpOrigin(input.signerUrl)}/discover-orchestrators`,
289
+ input.modelId
290
+ );
291
+ return { url, headers: input.signerHeaders, discoveryService: false };
292
+ }
293
+ throw new Error("discovery requires orchestratorUrl, discoveryUrl, or signerUrl");
294
+ }
295
+ function pickDiscoveryAddress(record) {
296
+ if (typeof record.address === "string") {
297
+ return record.address;
298
+ }
299
+ if (typeof record.url === "string") {
300
+ return record.url;
301
+ }
302
+ return void 0;
303
+ }
304
+ function parseDiscoveryList(data) {
305
+ if (!Array.isArray(data)) {
306
+ throw new TypeError(`Discovery response must be a JSON list, got ${typeof data}`);
307
+ }
308
+ const urls = [];
309
+ for (const item of data) {
310
+ if (!item || typeof item !== "object") {
311
+ continue;
312
+ }
313
+ const record = item;
314
+ const trimmed = pickDiscoveryAddress(record)?.trim();
315
+ if (trimmed) {
316
+ urls.push(trimmed);
317
+ }
318
+ }
319
+ return urls;
320
+ }
321
+ async function discoverOrchestrators(input) {
322
+ if (input.orchestratorUrl?.trim()) {
323
+ return input.orchestratorUrl.split(",").map((s) => s.trim()).filter(Boolean);
324
+ }
325
+ const { url, headers } = resolveDiscoveryEndpoint(input);
326
+ const response = await insecureFetch(url, {
327
+ method: "GET",
328
+ headers: {
329
+ Accept: "application/json",
330
+ ...headers
331
+ },
332
+ timeoutMs: input.discoveryTimeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS
333
+ });
334
+ if (!response.ok) {
335
+ const body = await response.text();
336
+ throw new Error(`Discovery failed HTTP ${response.status}: ${body.slice(0, 500)}`);
337
+ }
338
+ const data = await readJsonResponse(response);
339
+ return parseDiscoveryList(data);
340
+ }
341
+ var cachedRoot = null;
342
+ function protoPath() {
343
+ const here = path.dirname(fileURLToPath(import.meta.url));
344
+ return path.resolve(here, "../../../gateway/proto/lp_rpc.proto");
345
+ }
346
+ function requireGrpcPeer(label, load) {
347
+ try {
348
+ return load();
349
+ } catch {
350
+ throw new Error(
351
+ `${label} is required for @pymthouse/builder-sdk/gateway/server. Install peer dependencies: @grpc/grpc-js @grpc/proto-loader`
352
+ );
353
+ }
354
+ }
355
+ function loadProtoRoot() {
356
+ const grpc = requireGrpcPeer(
357
+ "@grpc/grpc-js",
358
+ () => createRequire(import.meta.url)("@grpc/grpc-js")
359
+ );
360
+ if (cachedRoot) {
361
+ return { grpc, root: cachedRoot };
362
+ }
363
+ const protoLoader = requireGrpcPeer(
364
+ "@grpc/proto-loader",
365
+ () => createRequire(import.meta.url)("@grpc/proto-loader")
366
+ );
367
+ const packageDefinition = protoLoader.loadSync(protoPath(), {
368
+ keepCase: true,
369
+ longs: String,
370
+ enums: String,
371
+ defaults: true,
372
+ oneofs: true
373
+ });
374
+ cachedRoot = grpc.loadPackageDefinition(packageDefinition);
375
+ return { grpc, root: cachedRoot };
376
+ }
377
+ function loadOrchestratorGrpc() {
378
+ const { grpc, root } = loadProtoRoot();
379
+ return { grpc, Orchestrator: root.net.Orchestrator };
380
+ }
381
+ function encodeCapabilitiesBase64(modelId) {
382
+ const { root } = loadProtoRoot();
383
+ const bytes = root.net.Capabilities.serialize(buildLv2vCapabilitiesMessage(modelId));
384
+ return Buffer.from(bytes).toString("base64");
385
+ }
386
+
387
+ // src/gateway/server/signer-material.ts
388
+ var signerMaterialCache = /* @__PURE__ */ new Map();
389
+ function cacheKey(signerUrl, headers) {
390
+ const headerPart = headers ? JSON.stringify(Object.entries(headers).sort(([a], [b]) => a.localeCompare(b))) : "";
391
+ return `${httpOrigin(signerUrl)}|${headerPart}`;
392
+ }
393
+ async function getSignerMaterial(signerUrl, signerHeaders) {
394
+ if (!signerUrl.trim()) {
395
+ return { address: "", sig: "" };
396
+ }
397
+ const key = cacheKey(signerUrl, signerHeaders);
398
+ const cached = signerMaterialCache.get(key);
399
+ if (cached) {
400
+ return cached;
401
+ }
402
+ const url = `${httpOrigin(signerUrl)}/sign-orchestrator-info`;
403
+ const response = await insecureFetch(url, {
404
+ method: "POST",
405
+ headers: {
406
+ Accept: "application/json",
407
+ "Content-Type": "application/json",
408
+ ...signerHeaders
409
+ },
410
+ body: Buffer.from("{}"),
411
+ timeoutMs: 5e3
412
+ });
413
+ if (!response.ok) {
414
+ const body = await response.text();
415
+ throw new Error(`Signer error HTTP ${response.status}: ${body.slice(0, 500)}`);
416
+ }
417
+ const data = await readJsonResponse(response);
418
+ const address = data.address?.trim() ?? "";
419
+ const sig = data.signature?.trim() ?? "";
420
+ if (!address || !sig) {
421
+ throw new Error("Signer response missing address or signature");
422
+ }
423
+ const material = { address, sig };
424
+ signerMaterialCache.set(key, material);
425
+ return material;
426
+ }
427
+ function hexToBytes(hex) {
428
+ let value = hex.trim();
429
+ if (value.startsWith("0x") || value.startsWith("0X")) {
430
+ value = value.slice(2);
431
+ }
432
+ if (value.length % 2 === 1) {
433
+ value = `0${value}`;
434
+ }
435
+ return Buffer.from(value, "hex");
436
+ }
437
+ function signerMaterialToGrpcFields(material) {
438
+ return {
439
+ address: hexToBytes(material.address),
440
+ sig: hexToBytes(material.sig)
441
+ };
442
+ }
443
+ var tofuCache = /* @__PURE__ */ new Map();
444
+ function splitHostPort(target) {
445
+ const trimmed = target.trim();
446
+ if (trimmed.startsWith("[")) {
447
+ const host2 = trimmed.slice(1, trimmed.indexOf("]"));
448
+ const port = Number(trimmed.slice(trimmed.indexOf("]") + 2));
449
+ return { host: host2, port };
450
+ }
451
+ const [host, portRaw] = trimmed.split(":");
452
+ return { host, port: Number(portRaw) };
453
+ }
454
+ function isIpAddress(host) {
455
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
456
+ }
457
+ function authorityFromSan(san) {
458
+ for (const entry of san) {
459
+ if (entry.typ === "DNS" && entry.val) {
460
+ return entry.val;
461
+ }
462
+ }
463
+ for (const entry of san) {
464
+ if (entry.typ === "IP" && entry.val) {
465
+ return entry.val;
466
+ }
467
+ }
468
+ return void 0;
469
+ }
470
+ function authorityFromSubject(cn) {
471
+ if (typeof cn === "string") {
472
+ return cn;
473
+ }
474
+ if (Array.isArray(cn) && cn.length > 0) {
475
+ return cn[0] ?? "";
476
+ }
477
+ return "";
478
+ }
479
+ function pickCertAuthority(cert) {
480
+ const san = cert.subjectaltname?.split(", ").map((entry) => {
481
+ const [typ, val] = entry.split(":");
482
+ return { typ, val };
483
+ });
484
+ if (san) {
485
+ const fromSan = authorityFromSan(san);
486
+ if (fromSan) {
487
+ return fromSan;
488
+ }
489
+ }
490
+ return authorityFromSubject(cert.subject?.CN);
491
+ }
492
+ async function fetchTofuRootCert(target) {
493
+ const { host, port } = splitHostPort(target);
494
+ return new Promise((resolve, reject) => {
495
+ const servername = isIpAddress(host) ? void 0 : host;
496
+ const socket = tls.connect(
497
+ {
498
+ host,
499
+ port,
500
+ servername,
501
+ rejectUnauthorized: false,
502
+ ALPNProtocols: ["h2"]
503
+ },
504
+ () => {
505
+ const peer = socket.getPeerCertificate();
506
+ socket.end();
507
+ if (!peer?.raw) {
508
+ reject(new Error("No peer certificate"));
509
+ return;
510
+ }
511
+ const pem = `-----BEGIN CERTIFICATE-----
512
+ ${peer.raw.toString("base64").match(/.{1,64}/g)?.join("\n")}
513
+ -----END CERTIFICATE-----
514
+ `;
515
+ const authority = pickCertAuthority(peer) || host;
516
+ resolve({ rootPem: Buffer.from(pem), authority });
517
+ }
518
+ );
519
+ socket.on("error", reject);
520
+ socket.setTimeout(5e3, () => {
521
+ socket.destroy(new Error("TLS probe timeout"));
522
+ });
523
+ });
524
+ }
525
+ function parseGrpcTarget(orchUrl) {
526
+ const url = orchUrl.includes("://") ? orchUrl : `https://${orchUrl}`;
527
+ const parsed = new URL(url);
528
+ if (parsed.protocol !== "https:") {
529
+ throw new Error(`Only https orchestrator URLs are supported (got ${parsed.protocol})`);
530
+ }
531
+ return parsed.host;
532
+ }
533
+ async function trustOnFirstUse(target) {
534
+ const cached = tofuCache.get(target);
535
+ if (cached) {
536
+ return cached;
537
+ }
538
+ const material = await fetchTofuRootCert(target);
539
+ tofuCache.set(target, material);
540
+ return material;
541
+ }
542
+ function evictTofuCache(target) {
543
+ tofuCache.delete(target);
544
+ }
545
+ function isCertVerifyError(message) {
546
+ return message.includes("CERTIFICATE_VERIFY_FAILED");
547
+ }
548
+
549
+ // src/gateway/server/orch-grpc.ts
550
+ async function getOrchestratorInfo(input) {
551
+ const useTofu = input.useTofu !== false;
552
+ const target = parseGrpcTarget(input.orchUrl);
553
+ const signer = await getSignerMaterial(input.signerUrl, input.signerHeaders);
554
+ const { address, sig } = signerMaterialToGrpcFields(signer);
555
+ const request = {
556
+ address,
557
+ sig,
558
+ capabilities: buildLv2vCapabilitiesMessage(input.modelId),
559
+ ignoreCapacityCheck: true
560
+ };
561
+ const maxAttempts = useTofu ? 2 : 1;
562
+ let lastError = null;
563
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
564
+ try {
565
+ return await callGetOrchestrator(target, request, useTofu);
566
+ } catch (err) {
567
+ lastError = err instanceof Error ? err : new Error(String(err));
568
+ if (useTofu && attempt === 0 && isCertVerifyError(lastError.message)) {
569
+ evictTofuCache(target);
570
+ continue;
571
+ }
572
+ throw lastError;
573
+ }
574
+ }
575
+ throw lastError ?? new Error("GetOrchestrator failed");
576
+ }
577
+ function callGetOrchestrator(target, request, useTofu) {
578
+ return new Promise((resolve, reject) => {
579
+ void (async () => {
580
+ try {
581
+ const { grpc, Orchestrator } = loadOrchestratorGrpc();
582
+ let credentials;
583
+ let options = {};
584
+ if (useTofu) {
585
+ const { rootPem, authority } = await trustOnFirstUse(target);
586
+ credentials = grpc.credentials.createSsl(rootPem);
587
+ options = {
588
+ "grpc.ssl_target_name_override": authority,
589
+ "grpc.default_authority": authority
590
+ };
591
+ } else {
592
+ credentials = grpc.credentials.createSsl();
593
+ }
594
+ const client = new Orchestrator(target, credentials, options);
595
+ client.GetOrchestrator(request, (err, response) => {
596
+ if (err) {
597
+ reject(err);
598
+ return;
599
+ }
600
+ resolve(response);
601
+ });
602
+ } catch (e) {
603
+ reject(e);
604
+ }
605
+ })();
606
+ });
607
+ }
608
+ function serializeOrchestratorInfo(info) {
609
+ if (typeof info.SerializeToString === "function") {
610
+ return Buffer.from(info.SerializeToString());
611
+ }
612
+ const { root } = loadProtoRoot();
613
+ return Buffer.from(root.net.OrchestratorInfo.serialize({ ...info }));
614
+ }
615
+
616
+ // src/gateway/server/payment-session.ts
617
+ var PaymentSession = class {
618
+ constructor(signerUrl, orchestratorInfo, signerHeaders, modelId, useTofu) {
619
+ this.signerUrl = signerUrl;
620
+ this.signerHeaders = signerHeaders;
621
+ this.modelId = modelId;
622
+ this.useTofu = useTofu;
623
+ this.orchestratorInfo = orchestratorInfo;
624
+ }
625
+ signerUrl;
626
+ signerHeaders;
627
+ modelId;
628
+ useTofu;
629
+ manifestId = null;
630
+ state = null;
631
+ orchestratorInfo;
632
+ setManifestId(manifestId) {
633
+ this.manifestId = manifestId.trim();
634
+ }
635
+ get transcoderUrl() {
636
+ const url = this.orchestratorInfo.transcoder?.trim();
637
+ if (!url) {
638
+ throw new Error("OrchestratorInfo missing transcoder URL");
639
+ }
640
+ return url;
641
+ }
642
+ async getPaymentHeaders() {
643
+ if (!this.signerUrl.trim()) {
644
+ return { payment: "", segCreds: "" };
645
+ }
646
+ let attempts = 0;
647
+ while (true) {
648
+ try {
649
+ return await this.requestPayment();
650
+ } catch (err) {
651
+ if (attempts >= 3 || !(err instanceof SignerRefreshRequired)) {
652
+ throw err;
653
+ }
654
+ this.orchestratorInfo = await getOrchestratorInfo({
655
+ orchUrl: this.transcoderUrl,
656
+ signerUrl: this.signerUrl,
657
+ signerHeaders: this.signerHeaders,
658
+ modelId: this.modelId,
659
+ useTofu: this.useTofu
660
+ });
661
+ attempts += 1;
662
+ }
663
+ }
664
+ }
665
+ async sendPayment(orchestratorUrl) {
666
+ const headers = await this.getPaymentHeaders();
667
+ const target = orchestratorUrl?.trim() || this.transcoderUrl;
668
+ const url = `${httpOrigin(target)}/payment`;
669
+ const response = await insecureFetch(url, {
670
+ method: "POST",
671
+ headers: {
672
+ "Livepeer-Payment": headers.payment,
673
+ "Livepeer-Segment": headers.segCreds
674
+ },
675
+ body: Buffer.alloc(0),
676
+ timeoutMs: 5e3
677
+ });
678
+ if (!response.ok) {
679
+ const body = await response.text();
680
+ throw new Error(`Payment POST failed HTTP ${response.status}: ${body.slice(0, 300)}`);
681
+ }
682
+ }
683
+ async requestPayment() {
684
+ const url = `${httpOrigin(this.signerUrl)}/generate-live-payment`;
685
+ const orchB64 = serializeOrchestratorInfo(this.orchestratorInfo).toString("base64");
686
+ const capsB64 = encodeCapabilitiesBase64(this.modelId);
687
+ const payload = {
688
+ orchestrator: orchB64,
689
+ type: "lv2v",
690
+ capabilities: capsB64
691
+ };
692
+ if (this.manifestId) {
693
+ payload.ManifestID = this.manifestId;
694
+ }
695
+ if (this.state) {
696
+ payload.state = this.state;
697
+ }
698
+ const response = await insecureFetch(url, {
699
+ method: "POST",
700
+ headers: {
701
+ Accept: "application/json",
702
+ "Content-Type": "application/json",
703
+ ...this.signerHeaders
704
+ },
705
+ body: Buffer.from(JSON.stringify(payload)),
706
+ timeoutMs: 1e4
707
+ });
708
+ if (response.status === 480) {
709
+ const orchHeader = response.headers.get("Livepeer-Orchestrator-URL")?.trim();
710
+ throw new SignerRefreshRequired(orchHeader ?? "");
711
+ }
712
+ if (!response.ok) {
713
+ const body = await response.text();
714
+ throw new Error(`generate-live-payment HTTP ${response.status}: ${body.slice(0, 500)}`);
715
+ }
716
+ const data = await readJsonResponse(response);
717
+ const payment = data.payment ?? "";
718
+ const segCreds = data.segCreds ?? "";
719
+ if (!payment) {
720
+ throw new Error("generate-live-payment missing payment field");
721
+ }
722
+ if (!data.state || typeof data.state !== "object") {
723
+ throw new Error("generate-live-payment missing state object");
724
+ }
725
+ this.state = data.state;
726
+ return { payment, segCreds };
727
+ }
728
+ };
729
+ var SignerRefreshRequired = class extends Error {
730
+ constructor(orchestratorUrl) {
731
+ super("Signer refresh required (HTTP 480)");
732
+ this.orchestratorUrl = orchestratorUrl;
733
+ this.name = "SignerRefreshRequired";
734
+ }
735
+ orchestratorUrl;
736
+ };
737
+
738
+ // src/gateway/server/lv2v.ts
739
+ async function startLv2vSession(input) {
740
+ const orchList = await discoverOrchestrators({
741
+ orchestratorUrl: input.request.orchestratorUrl,
742
+ discoveryUrl: input.request.discoveryUrl ?? input.discoveryUrl,
743
+ signerUrl: input.signerUrl,
744
+ signerHeaders: input.signerHeaders,
745
+ modelId: input.request.modelId,
746
+ discoveryTimeoutMs: input.discoveryTimeoutMs
747
+ });
748
+ if (orchList.length === 0) {
749
+ throw new Error("No orchestrators discovered");
750
+ }
751
+ const rejections = [];
752
+ for (const orchUrl of orchList) {
753
+ try {
754
+ return await startLv2vOnOrchestrator(orchUrl, input);
755
+ } catch (err) {
756
+ rejections.push({
757
+ url: orchUrl,
758
+ reason: err instanceof Error ? err.message : String(err)
759
+ });
760
+ }
761
+ }
762
+ throw new Error(
763
+ `All orchestrators failed (${rejections.length} tried): ${rejections.map((r) => `${r.url}: ${r.reason}`).join("; ")}`
764
+ );
765
+ }
766
+ async function startLv2vOnOrchestrator(orchUrl, input) {
767
+ const info = await getOrchestratorInfo({
768
+ orchUrl,
769
+ signerUrl: input.signerUrl,
770
+ signerHeaders: input.signerHeaders,
771
+ modelId: input.request.modelId,
772
+ useTofu: input.useTofu
773
+ });
774
+ const paymentSession = new PaymentSession(
775
+ input.signerUrl,
776
+ info,
777
+ input.signerHeaders,
778
+ input.request.modelId,
779
+ input.useTofu !== false
780
+ );
781
+ const paymentHeaders = await paymentSession.getPaymentHeaders();
782
+ const transcoder = paymentSession.transcoderUrl;
783
+ const url = `${httpOrigin(transcoder)}/live-video-to-video`;
784
+ const body = {
785
+ model_id: input.request.modelId
786
+ };
787
+ if (input.request.params) {
788
+ body.params = input.request.params;
789
+ }
790
+ if (input.request.streamId) {
791
+ body.stream_id = input.request.streamId;
792
+ }
793
+ if (input.request.requestId) {
794
+ body.gateway_request_id = input.request.requestId;
795
+ }
796
+ const response = await insecureFetch(url, {
797
+ method: "POST",
798
+ headers: {
799
+ Accept: "application/json",
800
+ "Content-Type": "application/json",
801
+ "Livepeer-Payment": paymentHeaders.payment,
802
+ "Livepeer-Segment": paymentHeaders.segCreds
803
+ },
804
+ body: Buffer.from(JSON.stringify(body)),
805
+ timeoutMs: 1e4
806
+ });
807
+ if (!response.ok) {
808
+ const text = await response.text();
809
+ throw new Error(`live-video-to-video HTTP ${response.status}: ${text.slice(0, 500)}`);
810
+ }
811
+ const data = await readJsonResponse(response);
812
+ const manifestId = data.manifest_id?.trim();
813
+ const publishUrl = data.publish_url?.trim();
814
+ const subscribeUrl = data.subscribe_url?.trim();
815
+ if (!manifestId) {
816
+ throw new Error("live-video-to-video response missing manifest_id");
817
+ }
818
+ if (!publishUrl) {
819
+ throw new Error("live-video-to-video response missing publish_url");
820
+ }
821
+ if (!subscribeUrl) {
822
+ throw new Error("live-video-to-video response missing subscribe_url");
823
+ }
824
+ paymentSession.setManifestId(manifestId);
825
+ return {
826
+ manifestId,
827
+ publishUrl,
828
+ subscribeUrl,
829
+ controlUrl: data.control_url?.trim(),
830
+ eventsUrl: data.events_url?.trim(),
831
+ mimeType: DEFAULT_TRICKLE_MIME_TYPE,
832
+ paymentSession,
833
+ orchestratorUrl: orchUrl
834
+ };
835
+ }
836
+ var SESSIONS_GLOBAL_KEY = /* @__PURE__ */ Symbol.for("@pymthouse/builder-sdk/gateway-sessions");
837
+ function sessionsMap() {
838
+ const globalStore = globalThis;
839
+ globalStore[SESSIONS_GLOBAL_KEY] ??= /* @__PURE__ */ new Map();
840
+ return globalStore[SESSIONS_GLOBAL_KEY];
841
+ }
842
+ function hashBearerToken(token) {
843
+ return createHash("sha256").update(token.trim()).digest("hex");
844
+ }
845
+ function createSessionId() {
846
+ return randomUUID();
847
+ }
848
+ function putSession(record) {
849
+ sessionsMap().set(record.id, record);
850
+ }
851
+ function getSession(sessionId) {
852
+ return sessionsMap().get(sessionId);
853
+ }
854
+ function deleteSession(sessionId) {
855
+ return sessionsMap().delete(sessionId);
856
+ }
857
+ function assertSessionOwner(record, ownerTokenHash) {
858
+ if (record.ownerTokenHash !== ownerTokenHash) {
859
+ throw new Error("Forbidden: session does not belong to this bearer token");
860
+ }
861
+ }
862
+ function closeSessionRecord(record) {
863
+ if (record.closed) {
864
+ return;
865
+ }
866
+ record.closed = true;
867
+ if (record.paymentInterval) {
868
+ clearInterval(record.paymentInterval);
869
+ record.paymentInterval = void 0;
870
+ }
871
+ }
872
+
873
+ // src/gateway/server/trickle-relay.ts
874
+ function parseTrickleIntHeader(headers, name) {
875
+ const raw = headers.get(name);
876
+ if (raw === null) {
877
+ return null;
878
+ }
879
+ const parsed = Number.parseInt(raw, 10);
880
+ return Number.isFinite(parsed) ? parsed : null;
881
+ }
882
+ function publishBaseUrl(record) {
883
+ return record.publishUrl.replace(/\/$/, "");
884
+ }
885
+ function subscribeBaseUrl(record) {
886
+ return record.subscribeUrl.replace(/\/$/, "");
887
+ }
888
+ async function ensureTrickleChannel(record) {
889
+ if (record.trickleCreated) {
890
+ return;
891
+ }
892
+ const response = await insecureFetch(record.publishUrl, {
893
+ method: "POST",
894
+ headers: {
895
+ "Expect-Content": record.mimeType
896
+ },
897
+ body: Buffer.alloc(0),
898
+ timeoutMs: 1e4
899
+ });
900
+ if (!response.ok) {
901
+ const body = await response.text();
902
+ throw new Error(`Trickle create failed HTTP ${response.status}: ${body.slice(0, 300)}`);
903
+ }
904
+ record.trickleCreated = true;
905
+ }
906
+ async function resolveTricklePublishSeq(record) {
907
+ await ensureTrickleChannel(record);
908
+ const url = `${publishBaseUrl(record)}/next`;
909
+ const response = await insecureFetch(url, {
910
+ method: "GET",
911
+ timeoutMs: 1e4
912
+ });
913
+ if (!response.ok) {
914
+ return 0;
915
+ }
916
+ const latest = response.headers.get("Lp-Trickle-Latest");
917
+ if (latest !== null) {
918
+ const parsed = Number.parseInt(latest, 10);
919
+ if (Number.isFinite(parsed)) {
920
+ return parsed;
921
+ }
922
+ }
923
+ return 0;
924
+ }
925
+ async function prepareTricklePublish(record) {
926
+ await ensureTrickleChannel(record);
927
+ if (record.publishSeq < 0) {
928
+ record.publishSeq = await resolveTricklePublishSeq(record);
929
+ }
930
+ }
931
+ async function publishTrickleSegment(record, seq, bytes, contentType) {
932
+ await prepareTricklePublish(record);
933
+ const url = `${publishBaseUrl(record)}/${seq}`;
934
+ const headers = {
935
+ "Content-Type": contentType
936
+ };
937
+ if (!record.trickleResetSent) {
938
+ headers["Lp-Trickle-Reset"] = "1";
939
+ record.trickleResetSent = true;
940
+ }
941
+ const response = await insecureFetch(url, {
942
+ method: "POST",
943
+ headers,
944
+ body: bytes,
945
+ timeoutMs: 12e4
946
+ });
947
+ if (!response.ok) {
948
+ const body = await response.text();
949
+ throw new Error(`Trickle publish failed HTTP ${response.status}: ${body.slice(0, 300)}`);
950
+ }
951
+ record.publishSeq = Math.max(record.publishSeq, seq + 1);
952
+ }
953
+ async function resolveTrickleSubscribeSeq(record) {
954
+ const url = `${subscribeBaseUrl(record)}/next`;
955
+ const response = await insecureFetch(url, {
956
+ method: "GET",
957
+ timeoutMs: 1e4
958
+ });
959
+ if (!response.ok) {
960
+ return TRICKLE_SEQ_LATEST;
961
+ }
962
+ const fromHeader = parseTrickleIntHeader(response.headers, "Lp-Trickle-Latest");
963
+ if (fromHeader !== null) {
964
+ return fromHeader;
965
+ }
966
+ const body = (await response.text()).trim();
967
+ const fromBody = Number.parseInt(body, 10);
968
+ return Number.isFinite(fromBody) ? fromBody : TRICKLE_SEQ_LATEST;
969
+ }
970
+ async function subscribeTrickleSegment(record, seq) {
971
+ const url = `${subscribeBaseUrl(record)}/${seq}`;
972
+ const response = await insecureFetch(url, {
973
+ method: "GET",
974
+ headers: record.trickleCreated ? { Connection: "close" } : void 0,
975
+ timeoutMs: 3e4
976
+ });
977
+ if (response.status === 404) {
978
+ const latest = await resolveTrickleSubscribeSeq(record);
979
+ return { wait: true, latestSeq: latest };
980
+ }
981
+ if (response.status === 470) {
982
+ const latestHeader = response.headers.get("Lp-Trickle-Latest");
983
+ const parsedLatest = latestHeader ? Number.parseInt(latestHeader, 10) : Number.NaN;
984
+ const latest = Number.isFinite(parsedLatest) ? parsedLatest : await resolveTrickleSubscribeSeq(record);
985
+ if (latest < seq) {
986
+ return { wait: true, latestSeq: latest };
987
+ }
988
+ if (latest === seq) {
989
+ return { wait: true, latestSeq: latest };
990
+ }
991
+ return subscribeTrickleSegment(record, latest);
992
+ }
993
+ if (!response.ok) {
994
+ const body = await response.text();
995
+ throw new Error(`Trickle subscribe failed HTTP ${response.status}: ${body.slice(0, 300)}`);
996
+ }
997
+ const data = response.body;
998
+ if (!data) {
999
+ throw new Error("Trickle subscribe response missing body stream");
1000
+ }
1001
+ const contentType = response.headers.get("Content-Type") ?? record.mimeType;
1002
+ const segmentSeq = parseTrickleIntHeader(response.headers, "Lp-Trickle-Seq") ?? Math.max(seq, 0);
1003
+ const latestSeq = parseTrickleIntHeader(response.headers, "Lp-Trickle-Latest") ?? segmentSeq;
1004
+ return { data, contentType, segmentSeq, nextSeq: segmentSeq + 1, latestSeq };
1005
+ }
1006
+ async function closeTricklePublish(record) {
1007
+ if (!record.trickleCreated) {
1008
+ return;
1009
+ }
1010
+ await insecureFetch(record.publishUrl, {
1011
+ method: "DELETE",
1012
+ timeoutMs: 5e3
1013
+ }).catch(() => void 0);
1014
+ record.trickleCreated = false;
1015
+ }
1016
+
1017
+ // src/gateway/server/handlers.ts
1018
+ function signerHeadersFromBearer(token) {
1019
+ return { Authorization: `Bearer ${token}` };
1020
+ }
1021
+ function startPaymentLoop(config, record) {
1022
+ if (!record || record.paymentInterval) {
1023
+ return;
1024
+ }
1025
+ record.paymentInterval = setInterval(() => {
1026
+ void record.paymentSession.sendPayment(record.orchestratorUrl).catch(() => void 0);
1027
+ }, config.paymentIntervalMs);
1028
+ }
1029
+ async function parseStartGatewaySessionRequest(request, config) {
1030
+ try {
1031
+ const parsed = await request.json();
1032
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1033
+ return Response.json({ error: "invalid_request" }, { status: 400 });
1034
+ }
1035
+ const record = parsed;
1036
+ const modelId = typeof record.modelId === "string" ? record.modelId.trim() : "";
1037
+ if (!modelId) {
1038
+ return Response.json({ error: "modelId is required" }, { status: 400 });
1039
+ }
1040
+ return {
1041
+ modelId,
1042
+ orchestratorUrl: typeof record.orchestratorUrl === "string" ? record.orchestratorUrl : void 0,
1043
+ discoveryUrl: typeof record.discoveryUrl === "string" ? record.discoveryUrl : config.discoveryUrl,
1044
+ params: record.params && typeof record.params === "object" && !Array.isArray(record.params) ? record.params : void 0,
1045
+ streamId: typeof record.streamId === "string" ? record.streamId : void 0,
1046
+ requestId: typeof record.requestId === "string" ? record.requestId : void 0
1047
+ };
1048
+ } catch {
1049
+ return Response.json({ error: "invalid_json" }, { status: 400 });
1050
+ }
1051
+ }
1052
+ async function startGatewaySessionRecord(config, token, body) {
1053
+ try {
1054
+ const job = await startLv2vSession({
1055
+ request: body,
1056
+ signerUrl: config.signerUrl,
1057
+ signerHeaders: signerHeadersFromBearer(token),
1058
+ discoveryUrl: config.discoveryUrl,
1059
+ discoveryTimeoutMs: config.discoveryTimeoutMs,
1060
+ useTofu: config.useTofu
1061
+ });
1062
+ const sessionId = createSessionId();
1063
+ const ownerTokenHash = hashBearerToken(token);
1064
+ const sessionRecord = {
1065
+ id: sessionId,
1066
+ ownerTokenHash,
1067
+ manifestId: job.manifestId,
1068
+ publishUrl: job.publishUrl,
1069
+ subscribeUrl: job.subscribeUrl,
1070
+ controlUrl: job.controlUrl,
1071
+ mimeType: job.mimeType,
1072
+ paymentSession: job.paymentSession,
1073
+ orchestratorUrl: job.orchestratorUrl,
1074
+ publishSeq: -1,
1075
+ subscribeSeq: TRICKLE_SEQ_CURRENT,
1076
+ trickleCreated: false,
1077
+ trickleResetSent: false,
1078
+ closed: false
1079
+ };
1080
+ putSession(sessionRecord);
1081
+ startPaymentLoop(config, sessionRecord);
1082
+ let publishSeq = 0;
1083
+ try {
1084
+ await prepareTricklePublish(sessionRecord);
1085
+ publishSeq = Math.max(0, sessionRecord.publishSeq);
1086
+ } catch (prepareErr) {
1087
+ const message = prepareErr instanceof Error ? prepareErr.message : String(prepareErr);
1088
+ return Response.json({ error: "trickle_prepare_failed", message }, { status: 502 });
1089
+ }
1090
+ return Response.json({
1091
+ sessionId,
1092
+ manifestId: job.manifestId,
1093
+ mimeType: job.mimeType,
1094
+ publishSeq,
1095
+ subscribeSeq: sessionRecord.subscribeSeq
1096
+ });
1097
+ } catch (err) {
1098
+ const message = err instanceof Error ? err.message : String(err);
1099
+ return Response.json({ error: "start_session_failed", message }, { status: 502 });
1100
+ }
1101
+ }
1102
+ function createGatewayStartSessionHandler(config) {
1103
+ return async (request) => {
1104
+ if (!config) {
1105
+ return disabledResponse();
1106
+ }
1107
+ const token = extractBearerToken(request);
1108
+ if (!token) {
1109
+ return unauthorizedResponse();
1110
+ }
1111
+ const body = await parseStartGatewaySessionRequest(request, config);
1112
+ if (body instanceof Response) {
1113
+ return body;
1114
+ }
1115
+ return startGatewaySessionRecord(config, token, body);
1116
+ };
1117
+ }
1118
+ function createGatewayPublishSegmentHandler(config) {
1119
+ return async (request, context) => {
1120
+ if (!config) {
1121
+ return disabledResponse();
1122
+ }
1123
+ const token = extractBearerToken(request);
1124
+ if (!token) {
1125
+ return unauthorizedResponse();
1126
+ }
1127
+ const { id, seq: seqRaw } = await context.params;
1128
+ const seq = Number.parseInt(seqRaw, 10);
1129
+ if (!Number.isFinite(seq)) {
1130
+ return Response.json({ error: "invalid_seq" }, { status: 400 });
1131
+ }
1132
+ const record = getSession(id);
1133
+ if (!record || record.closed) {
1134
+ return Response.json({ error: "session_not_found" }, { status: 404 });
1135
+ }
1136
+ try {
1137
+ assertSessionOwner(record, hashBearerToken(token));
1138
+ } catch {
1139
+ return forbiddenResponse();
1140
+ }
1141
+ const bytes = Buffer.from(await request.arrayBuffer());
1142
+ const contentType = request.headers.get("Content-Type")?.trim() || record.mimeType;
1143
+ try {
1144
+ await publishTrickleSegment(record, seq, bytes, contentType);
1145
+ record.publishSeq = Math.max(record.publishSeq, seq);
1146
+ return Response.json({ seq, ok: true });
1147
+ } catch (err) {
1148
+ const message = err instanceof Error ? err.message : String(err);
1149
+ return Response.json({ error: "publish_failed", message }, { status: 502 });
1150
+ }
1151
+ };
1152
+ }
1153
+ function createGatewaySubscribeSegmentHandler(config) {
1154
+ return async (request, context) => {
1155
+ if (!config) {
1156
+ return disabledResponse();
1157
+ }
1158
+ const token = extractBearerToken(request);
1159
+ if (!token) {
1160
+ return unauthorizedResponse();
1161
+ }
1162
+ const { id } = await context.params;
1163
+ const record = getSession(id);
1164
+ if (!record || record.closed) {
1165
+ return Response.json({ error: "session_not_found" }, { status: 404 });
1166
+ }
1167
+ try {
1168
+ assertSessionOwner(record, hashBearerToken(token));
1169
+ } catch {
1170
+ return forbiddenResponse();
1171
+ }
1172
+ const url = new URL(request.url);
1173
+ const seqParam = url.searchParams.get("seq");
1174
+ const seq = seqParam === null ? record.subscribeSeq : Number.parseInt(seqParam, 10);
1175
+ try {
1176
+ const segment = await subscribeTrickleSegment(record, seq);
1177
+ if (!segment) {
1178
+ return new Response(null, { status: 204 });
1179
+ }
1180
+ if ("wait" in segment) {
1181
+ return new Response(null, {
1182
+ status: 204,
1183
+ headers: {
1184
+ "X-Gateway-Latest-Seq": String(segment.latestSeq),
1185
+ "X-Gateway-Wait": "1"
1186
+ }
1187
+ });
1188
+ }
1189
+ record.subscribeSeq = segment.nextSeq;
1190
+ return new Response(segment.data, {
1191
+ status: 200,
1192
+ headers: {
1193
+ "Content-Type": segment.contentType,
1194
+ "X-Gateway-Segment-Seq": String(segment.segmentSeq),
1195
+ "X-Gateway-Next-Seq": String(segment.nextSeq),
1196
+ "X-Gateway-Latest-Seq": String(segment.latestSeq)
1197
+ }
1198
+ });
1199
+ } catch (err) {
1200
+ const message = err instanceof Error ? err.message : String(err);
1201
+ return Response.json({ error: "subscribe_failed", message }, { status: 502 });
1202
+ }
1203
+ };
1204
+ }
1205
+ function createGatewayStopSessionHandler(config) {
1206
+ return async (request, context) => {
1207
+ if (!config) {
1208
+ return disabledResponse();
1209
+ }
1210
+ const token = extractBearerToken(request);
1211
+ if (!token) {
1212
+ return unauthorizedResponse();
1213
+ }
1214
+ const { id } = await context.params;
1215
+ const record = getSession(id);
1216
+ if (!record) {
1217
+ return new Response(null, { status: 204 });
1218
+ }
1219
+ try {
1220
+ assertSessionOwner(record, hashBearerToken(token));
1221
+ } catch {
1222
+ return forbiddenResponse();
1223
+ }
1224
+ closeSessionRecord(record);
1225
+ await closeTricklePublish(record).catch(() => void 0);
1226
+ deleteSession(id);
1227
+ return new Response(null, { status: 204 });
1228
+ };
1229
+ }
1230
+
1231
+ export { createGatewayPublishSegmentHandler, createGatewayStartSessionHandler, createGatewayStopSessionHandler, createGatewaySubscribeSegmentHandler, hashBearerToken, readGatewayConfigFromEnv };
1232
+ //# sourceMappingURL=index.js.map
1233
+ //# sourceMappingURL=index.js.map