@pymthouse/builder-sdk 0.0.8 → 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 (85) hide show
  1. package/README.md +106 -2
  2. package/dist/client-BHfjDvIe.d.ts +129 -0
  3. package/dist/client-CvhJEhjV.d.cts +129 -0
  4. package/dist/config.cjs +122 -0
  5. package/dist/config.cjs.map +1 -0
  6. package/dist/config.d.cts +29 -0
  7. package/dist/config.d.ts +29 -0
  8. package/dist/config.js +111 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/device-initiate.cjs +88 -0
  11. package/dist/device-initiate.cjs.map +1 -0
  12. package/dist/device-initiate.d.cts +32 -0
  13. package/dist/device-initiate.d.ts +32 -0
  14. package/dist/device-initiate.js +84 -0
  15. package/dist/device-initiate.js.map +1 -0
  16. package/dist/device.cjs +1 -1
  17. package/dist/device.cjs.map +1 -1
  18. package/dist/device.d.cts +1 -1
  19. package/dist/device.d.ts +1 -1
  20. package/dist/device.js +1 -1
  21. package/dist/device.js.map +1 -1
  22. package/dist/env.cjs +1071 -28
  23. package/dist/env.cjs.map +1 -1
  24. package/dist/env.d.cts +15 -2
  25. package/dist/env.d.ts +15 -2
  26. package/dist/env.js +1071 -28
  27. package/dist/env.js.map +1 -1
  28. package/dist/gateway/client/index.cjs +492 -0
  29. package/dist/gateway/client/index.cjs.map +1 -0
  30. package/dist/gateway/client/index.d.cts +63 -0
  31. package/dist/gateway/client/index.d.ts +63 -0
  32. package/dist/gateway/client/index.js +489 -0
  33. package/dist/gateway/client/index.js.map +1 -0
  34. package/dist/gateway/index.cjs +16 -0
  35. package/dist/gateway/index.cjs.map +1 -0
  36. package/dist/gateway/index.d.cts +52 -0
  37. package/dist/gateway/index.d.ts +52 -0
  38. package/dist/gateway/index.js +10 -0
  39. package/dist/gateway/index.js.map +1 -0
  40. package/dist/gateway/server/index.cjs +1248 -0
  41. package/dist/gateway/server/index.cjs.map +1 -0
  42. package/dist/gateway/server/index.d.cts +31 -0
  43. package/dist/gateway/server/index.d.ts +31 -0
  44. package/dist/gateway/server/index.js +1233 -0
  45. package/dist/gateway/server/index.js.map +1 -0
  46. package/dist/index.cjs +1401 -137
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +41 -5
  49. package/dist/index.d.ts +41 -5
  50. package/dist/index.js +1334 -105
  51. package/dist/index.js.map +1 -1
  52. package/dist/ingest-B3Yi8Tb1.d.cts +271 -0
  53. package/dist/ingest-DoKJTWU9.d.ts +271 -0
  54. package/dist/plan-pricing.cjs +108 -0
  55. package/dist/plan-pricing.cjs.map +1 -0
  56. package/dist/plan-pricing.d.cts +15 -0
  57. package/dist/plan-pricing.d.ts +15 -0
  58. package/dist/plan-pricing.js +98 -0
  59. package/dist/plan-pricing.js.map +1 -0
  60. package/dist/signer/server.cjs +1366 -0
  61. package/dist/signer/server.cjs.map +1 -0
  62. package/dist/signer/server.d.cts +73 -0
  63. package/dist/signer/server.d.ts +73 -0
  64. package/dist/signer/server.js +1331 -0
  65. package/dist/signer/server.js.map +1 -0
  66. package/dist/tokens.cjs +75 -0
  67. package/dist/tokens.cjs.map +1 -0
  68. package/dist/tokens.d.cts +48 -0
  69. package/dist/tokens.d.ts +48 -0
  70. package/dist/tokens.js +64 -0
  71. package/dist/tokens.js.map +1 -0
  72. package/dist/types-_R1AwEZp.d.cts +343 -0
  73. package/dist/types-_R1AwEZp.d.ts +343 -0
  74. package/dist/verify.cjs +1 -1
  75. package/dist/verify.cjs.map +1 -1
  76. package/dist/verify.d.cts +1 -1
  77. package/dist/verify.d.ts +1 -1
  78. package/dist/verify.js +1 -1
  79. package/dist/verify.js.map +1 -1
  80. package/gateway/proto/lp_rpc.proto +542 -0
  81. package/package.json +57 -1
  82. package/dist/env-4YmzarGJ.d.ts +0 -68
  83. package/dist/env-CZczUMzR.d.cts +0 -68
  84. package/dist/types-W9PJAspR.d.cts +0 -136
  85. package/dist/types-W9PJAspR.d.ts +0 -136
@@ -0,0 +1,492 @@
1
+ 'use strict';
2
+
3
+ // src/gateway/types.ts
4
+ var TRICKLE_SEQ_LATEST = -1;
5
+ var TRICKLE_SEQ_CURRENT = -2;
6
+
7
+ // src/string-utils.ts
8
+ function stripTrailingSlashes(value) {
9
+ let end = value.length;
10
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
11
+ end--;
12
+ }
13
+ return value.slice(0, end);
14
+ }
15
+
16
+ // src/errors.ts
17
+ var PmtHouseError = class extends Error {
18
+ status;
19
+ code;
20
+ details;
21
+ constructor(message, {
22
+ status = 500,
23
+ code = "pymthouse_error",
24
+ details
25
+ } = {}) {
26
+ super(message);
27
+ this.name = "PmtHouseError";
28
+ this.status = status;
29
+ this.code = code;
30
+ this.details = details;
31
+ }
32
+ };
33
+
34
+ // src/signer/fetch-json.ts
35
+ function oauthFailureDescription(parsed, failureLabel, status) {
36
+ if (typeof parsed.error_description === "string") {
37
+ return parsed.error_description;
38
+ }
39
+ if (typeof parsed.error === "string") {
40
+ return parsed.error;
41
+ }
42
+ return `${failureLabel} (${status})`;
43
+ }
44
+ async function readJsonObjectFromResponse(response, options) {
45
+ const text = await response.text();
46
+ let parsed;
47
+ try {
48
+ parsed = text ? JSON.parse(text) : {};
49
+ } catch {
50
+ throw new PmtHouseError(options.invalidJsonMessage, {
51
+ status: 502,
52
+ code: options.invalidJsonCode,
53
+ details: { status: response.status }
54
+ });
55
+ }
56
+ if (!response.ok) {
57
+ const description = oauthFailureDescription(parsed, options.failureLabel, response.status);
58
+ throw new PmtHouseError(description, {
59
+ status: response.status,
60
+ code: typeof parsed.error === "string" ? parsed.error : options.defaultErrorCode,
61
+ details: parsed
62
+ });
63
+ }
64
+ return parsed;
65
+ }
66
+
67
+ // src/signer/device-exchange.ts
68
+ function extractSignerAccessTokenFromExchangeBody(body) {
69
+ const tokenObj = body.token;
70
+ if (tokenObj !== null && typeof tokenObj === "object" && !Array.isArray(tokenObj)) {
71
+ const nested = tokenObj;
72
+ for (const key of ["accessToken", "access_token"]) {
73
+ const value = nested[key];
74
+ if (typeof value === "string" && value.trim()) {
75
+ return value.trim();
76
+ }
77
+ }
78
+ }
79
+ for (const key of ["accessToken", "access_token"]) {
80
+ const value = body[key];
81
+ if (typeof value === "string" && value.trim()) {
82
+ return value.trim();
83
+ }
84
+ }
85
+ throw new PmtHouseError("Device exchange response missing signer access token", {
86
+ status: 502,
87
+ code: "invalid_exchange_response"
88
+ });
89
+ }
90
+ function normalizeDeviceExchangeResponse(minted, options) {
91
+ const scope = minted.scope.trim() || "sign:job";
92
+ const body = {
93
+ access_token: minted.access_token,
94
+ token_type: "Bearer",
95
+ expires_in: minted.expires_in,
96
+ scope,
97
+ balanceUsdMicros: minted.balanceUsdMicros,
98
+ lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,
99
+ token: {
100
+ accessToken: minted.access_token,
101
+ access_token: minted.access_token,
102
+ expiresIn: minted.expires_in,
103
+ expires_in: minted.expires_in,
104
+ scope,
105
+ balanceUsdMicros: minted.balanceUsdMicros,
106
+ lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros
107
+ }
108
+ };
109
+ const signerUrl = options?.signerUrl?.trim();
110
+ if (signerUrl) {
111
+ body.signerUrl = signerUrl;
112
+ }
113
+ return body;
114
+ }
115
+
116
+ // src/signer/api-key-exchange.ts
117
+ var EXCHANGE_RESPONSE_ERROR = "invalid_exchange_response";
118
+ async function exchangeApiKeyForSigner(options) {
119
+ const fetchImpl = options.fetch ?? fetch;
120
+ const url = `${stripTrailingSlashes(options.facadeUrl)}/api/pymthouse/keys/exchange`;
121
+ const body = { apiKey: options.apiKey };
122
+ if (options.scope?.trim()) {
123
+ body.scope = options.scope.trim();
124
+ }
125
+ if (options.clientId?.trim()) {
126
+ body.clientId = options.clientId.trim();
127
+ }
128
+ const response = await fetchImpl(url, {
129
+ method: "POST",
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ Accept: "application/json"
133
+ },
134
+ body: JSON.stringify(body),
135
+ cache: "no-store"
136
+ });
137
+ const parsed = await readJsonObjectFromResponse(response, {
138
+ invalidJsonMessage: "API key exchange returned invalid JSON",
139
+ invalidJsonCode: EXCHANGE_RESPONSE_ERROR,
140
+ failureLabel: "API key exchange failed",
141
+ defaultErrorCode: "api_key_exchange_failed"
142
+ });
143
+ const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
144
+ const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
145
+ const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
146
+ return normalizeDeviceExchangeResponse(
147
+ {
148
+ access_token: accessToken,
149
+ expires_in: typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 3600,
150
+ scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
151
+ balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
152
+ lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
153
+ },
154
+ { signerUrl }
155
+ );
156
+ }
157
+
158
+ // src/gateway/client/resolve-signer.ts
159
+ var boundFetch = (input, init) => globalThis.fetch(input, init);
160
+ async function resolveSignerToken(credentials, fetchImpl = boundFetch) {
161
+ if (credentials.type === "bearer") {
162
+ const token2 = credentials.accessToken.trim();
163
+ if (!token2) {
164
+ throw new Error("Signer bearer token is empty");
165
+ }
166
+ return token2;
167
+ }
168
+ const exchanged = await exchangeApiKeyForSigner({
169
+ facadeUrl: credentials.facadeUrl,
170
+ apiKey: credentials.apiKey,
171
+ scope: credentials.scope ?? "sign:job",
172
+ clientId: credentials.clientId,
173
+ fetch: fetchImpl
174
+ });
175
+ const token = exchanged.access_token?.trim() || exchanged.token?.accessToken?.trim() || exchanged.token?.access_token?.trim();
176
+ if (!token) {
177
+ throw new Error("API key exchange did not return a signer access token");
178
+ }
179
+ return token;
180
+ }
181
+
182
+ // src/gateway/client/browser-client.ts
183
+ function jsonErrorMessage(body, status, fallback) {
184
+ if (typeof body.message === "string") {
185
+ return body.message;
186
+ }
187
+ if (typeof body.error === "string") {
188
+ return body.error;
189
+ }
190
+ return `${fallback} (${status})`;
191
+ }
192
+ function resolveSubscribeSegmentSeq(requestedSeq, headerSeq) {
193
+ if (Number.isFinite(headerSeq)) {
194
+ return headerSeq;
195
+ }
196
+ if (requestedSeq >= 0) {
197
+ return requestedSeq;
198
+ }
199
+ return 0;
200
+ }
201
+ function isTrickleLeadingIndex(seq) {
202
+ return seq === TRICKLE_SEQ_LATEST || seq === TRICKLE_SEQ_CURRENT;
203
+ }
204
+ function parseHeaderInt(headers, name) {
205
+ const raw = headers.get(name);
206
+ if (!raw) {
207
+ return Number.NaN;
208
+ }
209
+ const parsed = Number.parseInt(raw, 10);
210
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
211
+ }
212
+ var BrowserGatewayClient = class {
213
+ constructor(options) {
214
+ this.options = options;
215
+ this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
216
+ }
217
+ options;
218
+ signerToken = null;
219
+ sessionId = null;
220
+ publishSeq = -1;
221
+ /** Next trickle GET index for orchestrator output (see TrickleSubscriber). */
222
+ subscribeSeq = TRICKLE_SEQ_CURRENT;
223
+ subscribeEmptyPolls = 0;
224
+ fetchImpl;
225
+ get baseUrl() {
226
+ return stripTrailingSlashes(this.options.baseUrl);
227
+ }
228
+ get signerAccessToken() {
229
+ return this.signerToken;
230
+ }
231
+ get activeSessionId() {
232
+ return this.sessionId;
233
+ }
234
+ get nextSubscribeSeq() {
235
+ return this.subscribeSeq;
236
+ }
237
+ async connect(credentials) {
238
+ this.signerToken = await resolveSignerToken(credentials, this.fetchImpl);
239
+ }
240
+ async startSession(request) {
241
+ if (!this.signerToken) {
242
+ throw new Error("Call connect() with signer credentials before startSession()");
243
+ }
244
+ const response = await this.fetchImpl(`${this.baseUrl}/api/gateway/sessions`, {
245
+ method: "POST",
246
+ headers: {
247
+ Authorization: `Bearer ${this.signerToken}`,
248
+ "Content-Type": "application/json",
249
+ Accept: "application/json"
250
+ },
251
+ body: JSON.stringify(request)
252
+ });
253
+ const body = await response.json().catch(() => ({}));
254
+ if (!response.ok) {
255
+ throw new Error(jsonErrorMessage(body, response.status, "startSession failed"));
256
+ }
257
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
258
+ const manifestId = typeof body.manifestId === "string" ? body.manifestId : "";
259
+ if (!sessionId) {
260
+ throw new Error("startSession response missing sessionId");
261
+ }
262
+ this.sessionId = sessionId;
263
+ const initialPublishSeq = typeof body.publishSeq === "number" && Number.isFinite(body.publishSeq) ? body.publishSeq : 0;
264
+ this.publishSeq = initialPublishSeq - 1;
265
+ const initialSubscribeSeq = typeof body.subscribeSeq === "number" && Number.isFinite(body.subscribeSeq) ? body.subscribeSeq : TRICKLE_SEQ_CURRENT;
266
+ this.subscribeSeq = initialSubscribeSeq;
267
+ this.subscribeEmptyPolls = 0;
268
+ const mimeType = typeof body.mimeType === "string" ? body.mimeType : void 0;
269
+ return {
270
+ sessionId,
271
+ manifestId,
272
+ mimeType,
273
+ publishSeq: initialPublishSeq,
274
+ subscribeSeq: initialSubscribeSeq
275
+ };
276
+ }
277
+ setSignerToken(accessToken) {
278
+ const token = accessToken.trim();
279
+ if (!token) {
280
+ throw new Error("Signer bearer token is empty");
281
+ }
282
+ this.signerToken = token;
283
+ }
284
+ async publishSegment(bytes, options) {
285
+ if (!this.sessionId || !this.signerToken) {
286
+ throw new Error("No active gateway session");
287
+ }
288
+ const seq = options?.seq ?? this.publishSeq + 1;
289
+ const part = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes);
290
+ const body = new Blob([part]);
291
+ const response = await this.fetchImpl(
292
+ `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}/publish/${seq}`,
293
+ {
294
+ method: "PUT",
295
+ headers: {
296
+ Authorization: `Bearer ${this.signerToken}`,
297
+ "Content-Type": options?.contentType ?? "application/octet-stream"
298
+ },
299
+ body
300
+ }
301
+ );
302
+ if (!response.ok) {
303
+ const text = await response.text();
304
+ throw new Error(`publishSegment failed (${response.status}): ${text.slice(0, 300)}`);
305
+ }
306
+ this.publishSeq = seq;
307
+ return { seq, ok: true };
308
+ }
309
+ /**
310
+ * Fetch the next orchestrator output segment (sequential walk from subscribeSeq).
311
+ * Matches livepeer-python-gateway TrickleSubscriber / MediaOutput segment iteration.
312
+ */
313
+ async subscribeOutputSegment() {
314
+ const chunks = [];
315
+ const segment = await this.subscribeOutputSegmentStream((chunk) => {
316
+ chunks.push(chunk);
317
+ });
318
+ if (!segment) {
319
+ return null;
320
+ }
321
+ const total = chunks.reduce((sum, part) => sum + part.byteLength, 0);
322
+ const joined = new Uint8Array(total);
323
+ let offset = 0;
324
+ for (const part of chunks) {
325
+ joined.set(part, offset);
326
+ offset += part.byteLength;
327
+ }
328
+ return { ...segment, data: joined.buffer, byteCount: total };
329
+ }
330
+ /**
331
+ * Stream the next orchestrator output segment incrementally.
332
+ * Mirrors Python SegmentReader behavior (consume bytes before segment closes).
333
+ */
334
+ async subscribeOutputSegmentStream(onChunk) {
335
+ if (!this.sessionId || !this.signerToken) {
336
+ throw new Error("No active gateway session");
337
+ }
338
+ const requestedSeq = this.subscribeSeq;
339
+ const url = new URL(
340
+ `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}/subscribe`
341
+ );
342
+ url.searchParams.set("seq", String(requestedSeq));
343
+ const response = await this.fetchImpl(url.toString(), {
344
+ method: "GET",
345
+ headers: {
346
+ Authorization: `Bearer ${this.signerToken}`
347
+ }
348
+ });
349
+ if (response.status === 204) {
350
+ this.handleSubscribeEmpty(requestedSeq, response.headers);
351
+ return null;
352
+ }
353
+ if (!response.ok) {
354
+ const text = await response.text();
355
+ throw new Error(`subscribeOutputSegment failed (${response.status}): ${text.slice(0, 300)}`);
356
+ }
357
+ const segmentSeq = parseHeaderInt(response.headers, "X-Gateway-Segment-Seq");
358
+ const latestSeq = parseHeaderInt(response.headers, "X-Gateway-Latest-Seq");
359
+ const resolvedSegmentSeq = resolveSubscribeSegmentSeq(requestedSeq, segmentSeq);
360
+ const resolvedLatest = Number.isFinite(latestSeq) ? latestSeq : resolvedSegmentSeq;
361
+ const byteCount = await this.readSubscribeBody(response, onChunk);
362
+ if (byteCount === 0) {
363
+ this.handleSubscribeEmpty(requestedSeq, response.headers);
364
+ return null;
365
+ }
366
+ this.advanceSubscribeSeq(requestedSeq, response.headers, resolvedLatest);
367
+ return {
368
+ segmentSeq: resolvedSegmentSeq,
369
+ latestSeq: resolvedLatest,
370
+ nextSeq: this.subscribeSeq,
371
+ byteCount
372
+ };
373
+ }
374
+ async readSubscribeBody(response, onChunk) {
375
+ if (!response.body) {
376
+ const data = await response.arrayBuffer();
377
+ if (data.byteLength === 0) {
378
+ return 0;
379
+ }
380
+ onChunk(new Uint8Array(data));
381
+ return data.byteLength;
382
+ }
383
+ const reader = response.body.getReader();
384
+ let bytesRead = 0;
385
+ try {
386
+ while (true) {
387
+ const { done, value } = await reader.read();
388
+ if (done) {
389
+ break;
390
+ }
391
+ if (value && value.byteLength > 0) {
392
+ bytesRead += value.byteLength;
393
+ onChunk(value);
394
+ }
395
+ }
396
+ } finally {
397
+ reader.releaseLock();
398
+ }
399
+ return bytesRead;
400
+ }
401
+ /** @deprecated Use subscribeOutputSegment() — live-edge-only polling does not advance output seq. */
402
+ async subscribeLiveSegment() {
403
+ return this.subscribeOutputSegment();
404
+ }
405
+ advanceSubscribeSeq(requestedSeq, headers, latestSeq) {
406
+ const nextHeader = parseHeaderInt(headers, "X-Gateway-Next-Seq");
407
+ if (Number.isFinite(nextHeader)) {
408
+ this.subscribeSeq = nextHeader;
409
+ } else {
410
+ const segmentSeq = parseHeaderInt(headers, "X-Gateway-Segment-Seq");
411
+ if (Number.isFinite(segmentSeq) && segmentSeq >= 0) {
412
+ this.subscribeSeq = segmentSeq + 1;
413
+ } else if (requestedSeq >= 0) {
414
+ this.subscribeSeq = requestedSeq + 1;
415
+ } else if (Number.isFinite(latestSeq) && latestSeq >= 0) {
416
+ this.subscribeSeq = latestSeq + 1;
417
+ } else {
418
+ this.subscribeSeq = TRICKLE_SEQ_LATEST;
419
+ }
420
+ }
421
+ if (Number.isFinite(latestSeq) && latestSeq >= 0 && this.subscribeSeq >= 0 && latestSeq - this.subscribeSeq > 2) {
422
+ this.subscribeSeq = latestSeq;
423
+ }
424
+ }
425
+ handleSubscribeEmpty(requestedSeq, headers) {
426
+ this.subscribeEmptyPolls += 1;
427
+ const latest = parseHeaderInt(headers, "X-Gateway-Latest-Seq");
428
+ if (Number.isFinite(latest) && this.applyLatestSeqOnEmptyPoll(requestedSeq, latest, headers)) {
429
+ return;
430
+ }
431
+ if (requestedSeq >= 0) {
432
+ this.subscribeSeq = requestedSeq;
433
+ return;
434
+ }
435
+ if (this.subscribeEmptyPolls >= 12) {
436
+ this.subscribeSeq = TRICKLE_SEQ_LATEST;
437
+ this.subscribeEmptyPolls = 0;
438
+ }
439
+ }
440
+ applyLatestSeqOnEmptyPoll(requestedSeq, latest, headers) {
441
+ const wait = headers.get("X-Gateway-Wait") === "1";
442
+ if (wait && requestedSeq >= 0 && latest < requestedSeq) {
443
+ this.subscribeSeq = requestedSeq;
444
+ return true;
445
+ }
446
+ if (isTrickleLeadingIndex(requestedSeq)) {
447
+ this.subscribeSeq = latest >= 0 ? latest : TRICKLE_SEQ_LATEST;
448
+ this.subscribeEmptyPolls = 0;
449
+ return true;
450
+ }
451
+ if (requestedSeq >= 0 && latest === requestedSeq) {
452
+ this.subscribeSeq = requestedSeq;
453
+ return true;
454
+ }
455
+ if (requestedSeq >= 0 && latest > requestedSeq) {
456
+ this.subscribeSeq = latest;
457
+ this.subscribeEmptyPolls = 0;
458
+ return true;
459
+ }
460
+ return false;
461
+ }
462
+ async subscribeSegment(seq) {
463
+ if (seq !== void 0) {
464
+ this.subscribeSeq = seq;
465
+ }
466
+ const segment = await this.subscribeOutputSegment();
467
+ return segment?.data ?? null;
468
+ }
469
+ async stop() {
470
+ if (!this.sessionId || !this.signerToken) {
471
+ return;
472
+ }
473
+ await this.fetchImpl(
474
+ `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}`,
475
+ {
476
+ method: "DELETE",
477
+ headers: {
478
+ Authorization: `Bearer ${this.signerToken}`
479
+ }
480
+ }
481
+ ).catch(() => void 0);
482
+ this.sessionId = null;
483
+ this.publishSeq = -1;
484
+ this.subscribeSeq = TRICKLE_SEQ_CURRENT;
485
+ this.subscribeEmptyPolls = 0;
486
+ }
487
+ };
488
+
489
+ exports.BrowserGatewayClient = BrowserGatewayClient;
490
+ exports.resolveSignerToken = resolveSignerToken;
491
+ //# sourceMappingURL=index.cjs.map
492
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/gateway/types.ts","../../../src/string-utils.ts","../../../src/errors.ts","../../../src/signer/fetch-json.ts","../../../src/signer/device-exchange.ts","../../../src/signer/api-key-exchange.ts","../../../src/gateway/client/resolve-signer.ts","../../../src/gateway/client/browser-client.ts"],"names":["token"],"mappings":"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","file":"index.cjs","sourcesContent":["/** Shared gateway types (browser + server). */\n\nexport const LIVE_VIDEO_TO_VIDEO_CAPABILITY_ID = 35;\n\nexport const DEFAULT_TRICKLE_MIME_TYPE = \"video/mp2t\";\n\nexport const DEFAULT_DISCOVERY_TIMEOUT_MS = 60_000;\n\n/** Trickle GET index: live edge (next write / most recent publish). */\nexport const TRICKLE_SEQ_LATEST = -1;\n\n/** Trickle GET index: current in-flight segment (nextWrite - 1). */\nexport const TRICKLE_SEQ_CURRENT = -2;\n\nexport type StartGatewaySessionRequest = {\n modelId: string;\n orchestratorUrl?: string;\n discoveryUrl?: string;\n params?: Record<string, unknown>;\n streamId?: string;\n requestId?: string;\n};\n\nexport type StartGatewaySessionResponse = {\n sessionId: string;\n manifestId: string;\n /** Trickle segment MIME (usually video/mp2t). */\n mimeType?: string;\n /** First trickle publish sequence index (from orch /next). */\n publishSeq?: number;\n /** Initial trickle subscribe index (default TRICKLE_SEQ_CURRENT / -2). */\n subscribeSeq?: number;\n};\n\nexport type GatewaySessionPublic = {\n sessionId: string;\n manifestId: string;\n mimeType: string;\n};\n\nexport type GatewaySegmentPublishResult = {\n seq: number;\n ok: boolean;\n};\n\nexport type GatewaySubscribeResult = {\n seq: number;\n data: ArrayBuffer;\n contentType: string;\n} | null;\n\n/** One completed trickle output segment from subscribeOutputSegment(). */\nexport type GatewayLiveSubscribeSegment = {\n data: ArrayBuffer;\n segmentSeq: number;\n latestSeq: number;\n /** Client index for the following GET (segmentSeq + 1 after a successful read). */\n nextSeq: number;\n /** Total bytes read from this segment response (used for progressive dedupe). */\n byteCount: number;\n};\n","/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n\nfunction endsWithIgnoreCase(value: string, suffix: string): boolean {\n if (suffix.length > value.length) {\n return false;\n }\n const start = value.length - suffix.length;\n for (let i = 0; i < suffix.length; i++) {\n const a = value.codePointAt(start + i) ?? 0;\n const b = suffix.codePointAt(i) ?? 0;\n if (a !== b && (a | 32) !== (b | 32)) {\n return false;\n }\n }\n return true;\n}\n\nfunction stripSuffixIgnoreCase(value: string, suffix: string): string {\n return endsWithIgnoreCase(value, suffix)\n ? value.slice(0, value.length - suffix.length)\n : value;\n}\n\n/** Issuer URL (`…/oidc`) → Builder API base (`…/api/v1`). Linear-time; no regex. */\nexport function stripOidcPathSuffix(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Issuer URL (`…/api/v1/oidc`) → host origin for signer/API-key routes. Linear-time; no regex. */\nexport function stripIssuerOriginFromOidcUrl(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/api/v1/oidc\");\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Validate gateway session ids before embedding in request URLs. */\nexport function isSafePathSegment(value: unknown): value is string {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 128) {\n return false;\n }\n for (let i = 0; i < value.length; i++) {\n const c = value.codePointAt(i) ?? 0;\n const ok =\n (c >= 48 && c <= 57) ||\n (c >= 65 && c <= 90) ||\n (c >= 97 && c <= 122) ||\n c === 95 ||\n c === 45;\n if (!ok) {\n return false;\n }\n }\n return true;\n}\n\n/** Parse and validate an http(s) facade origin (no path). */\nexport function parseHttpOrigin(raw: string | undefined, fallback: string): string {\n const trimmed = (raw ?? fallback).trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new TypeError(\"Origin must be a valid http(s) URL\");\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new TypeError(\"Origin must use http or https\");\n }\n return parsed.origin;\n}\n\n/** Build a validated DELETE URL for `/api/gateway/sessions/:id`. */\nexport function buildGatewaySessionDeleteUrl(origin: string, sessionId: string): URL {\n if (!isSafePathSegment(sessionId)) {\n throw new TypeError(\"Invalid gateway session id\");\n }\n return new URL(`/api/gateway/sessions/${encodeURIComponent(sessionId)}`, origin);\n}\n","export class PmtHouseError extends Error {\n readonly status: number;\n readonly code: string;\n readonly details?: unknown;\n\n constructor(\n message: string,\n {\n status = 500,\n code = \"pymthouse_error\",\n details,\n }: {\n status?: number;\n code?: string;\n details?: unknown;\n } = {},\n ) {\n super(message);\n this.name = \"PmtHouseError\";\n this.status = status;\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toPmtHouseError(\n error: unknown,\n fallbackMessage: string,\n): PmtHouseError {\n if (error instanceof PmtHouseError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new PmtHouseError(error.message || fallbackMessage, {\n code: \"unexpected_error\",\n status: 500,\n });\n }\n\n return new PmtHouseError(fallbackMessage, {\n code: \"unexpected_error\",\n status: 500,\n });\n}\n","import { PmtHouseError } from \"../errors.js\";\n\nfunction oauthFailureDescription(\n parsed: Record<string, unknown>,\n failureLabel: string,\n status: number,\n): string {\n if (typeof parsed.error_description === \"string\") {\n return parsed.error_description;\n }\n if (typeof parsed.error === \"string\") {\n return parsed.error;\n }\n return `${failureLabel} (${status})`;\n}\n\nexport type ReadJsonObjectFromResponseOptions = {\n invalidJsonMessage: string;\n invalidJsonCode: string;\n failureLabel: string;\n defaultErrorCode: string;\n};\n\nexport async function readJsonObjectFromResponse(\n response: Response,\n options: ReadJsonObjectFromResponseOptions,\n): Promise<Record<string, unknown>> {\n const text = await response.text();\n let parsed: Record<string, unknown>;\n try {\n parsed = text ? (JSON.parse(text) as Record<string, unknown>) : {};\n } catch {\n throw new PmtHouseError(options.invalidJsonMessage, {\n status: 502,\n code: options.invalidJsonCode,\n details: { status: response.status },\n });\n }\n\n if (!response.ok) {\n const description = oauthFailureDescription(parsed, options.failureLabel, response.status);\n throw new PmtHouseError(description, {\n status: response.status,\n code: typeof parsed.error === \"string\" ? parsed.error : options.defaultErrorCode,\n details: parsed,\n });\n }\n\n return parsed;\n}\n","import { loadAuthorizationServer } from \"../discovery.js\";\nimport { encodeClientSecretBasic } from \"../encoding.js\";\nimport { PmtHouseError } from \"../errors.js\";\nimport { stripTrailingSlashes } from \"../string-utils.js\";\nimport { readJsonObjectFromResponse } from \"./fetch-json.js\";\nimport { readExpiresIn, readStringField } from \"./json-fields.js\";\nimport { signerHandlerErrorResponse } from \"./handler-errors.js\";\nimport {\n LIVEPEER_REMOTE_SIGNER_AUDIENCE,\n parseMintUserSignerTokenResponse,\n} from \"./mint-token.js\";\nimport type {\n DeviceExchangeHandlerConfig,\n DeviceExchangeHandlerConfigRemote,\n DeviceExchangeMintContext,\n DeviceExchangeMintResult,\n DeviceExchangeRequestBody,\n DeviceExchangeResponse,\n ExchangeDeviceTokenForSignerOptions,\n MintSignerTokenFromDeviceTokenOptions,\n} from \"./types.js\";\n\nconst TOKEN_EXCHANGE_GRANT = \"urn:ietf:params:oauth:grant-type:token-exchange\";\nconst SUBJECT_ACCESS_TOKEN_TYPE = \"urn:ietf:params:oauth:token-type:access_token\";\nconst EXCHANGE_RESPONSE_ERROR = \"invalid_exchange_response\";\n\nexport function extractSignerAccessTokenFromExchangeBody(\n body: Record<string, unknown>,\n): string {\n const tokenObj = body.token;\n if (tokenObj !== null && typeof tokenObj === \"object\" && !Array.isArray(tokenObj)) {\n const nested = tokenObj as Record<string, unknown>;\n for (const key of [\"accessToken\", \"access_token\"] as const) {\n const value = nested[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n }\n for (const key of [\"accessToken\", \"access_token\"] as const) {\n const value = body[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n throw new PmtHouseError(\"Device exchange response missing signer access token\", {\n status: 502,\n code: \"invalid_exchange_response\",\n });\n}\n\nexport function normalizeDeviceExchangeResponse(\n minted: DeviceExchangeMintResult,\n options?: { signerUrl?: string },\n): DeviceExchangeResponse {\n const scope = minted.scope.trim() || \"sign:job\";\n const body: DeviceExchangeResponse = {\n access_token: minted.access_token,\n token_type: \"Bearer\",\n expires_in: minted.expires_in,\n scope,\n balanceUsdMicros: minted.balanceUsdMicros,\n lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,\n token: {\n accessToken: minted.access_token,\n access_token: minted.access_token,\n expiresIn: minted.expires_in,\n expires_in: minted.expires_in,\n scope,\n balanceUsdMicros: minted.balanceUsdMicros,\n lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,\n },\n };\n const signerUrl = options?.signerUrl?.trim();\n if (signerUrl) {\n body.signerUrl = signerUrl;\n }\n return body;\n}\n\nexport async function parseDeviceExchangeRequestBody(\n request: Request,\n): Promise<DeviceExchangeRequestBody> {\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n throw new PmtHouseError(\"Request body must be JSON\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) {\n throw new PmtHouseError(\"Request body must be a JSON object\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const record = body as Record<string, unknown>;\n const deviceTokenRaw = record.deviceToken;\n if (typeof deviceTokenRaw !== \"string\" || !deviceTokenRaw.trim()) {\n throw new PmtHouseError(\"Request body must include deviceToken\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const deviceToken = deviceTokenRaw.trim();\n const scope =\n typeof record.scope === \"string\" && record.scope.trim()\n ? record.scope.trim()\n : undefined;\n const clientId =\n typeof record.clientId === \"string\" && record.clientId.trim()\n ? record.clientId.trim()\n : undefined;\n return { deviceToken, scope, clientId };\n}\n\nexport async function mintSignerTokenFromDeviceToken(\n options: MintSignerTokenFromDeviceTokenOptions,\n): Promise<DeviceExchangeMintResult> {\n const fetchImpl = options.fetch ?? fetch;\n const issuerUrl = stripTrailingSlashes(options.issuerUrl);\n const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {\n allowInsecureHttp: options.allowInsecureHttp,\n });\n const tokenEndpoint = as.token_endpoint;\n if (!tokenEndpoint) {\n throw new PmtHouseError(\"OIDC discovery document is missing token_endpoint\", {\n status: 500,\n code: \"oidc_discovery_invalid\",\n });\n }\n\n const audience = options.audience?.trim() || LIVEPEER_REMOTE_SIGNER_AUDIENCE;\n const params = new URLSearchParams({\n grant_type: TOKEN_EXCHANGE_GRANT,\n subject_token: options.deviceToken,\n subject_token_type: SUBJECT_ACCESS_TOKEN_TYPE,\n audience,\n resource: audience,\n });\n if (options.scope?.trim()) {\n params.set(\"scope\", options.scope.trim());\n }\n\n const response = await fetchImpl(tokenEndpoint, {\n method: \"POST\",\n headers: {\n Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: params.toString(),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"Token endpoint returned invalid JSON\",\n invalidJsonCode: \"invalid_token_response\",\n failureLabel: \"Signer JWT exchange failed\",\n defaultErrorCode: \"token_exchange_failed\",\n });\n\n const cached = parseMintUserSignerTokenResponse(parsed);\n return {\n access_token: cached.jwt,\n expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),\n scope: readStringField(parsed, \"scope\", EXCHANGE_RESPONSE_ERROR),\n balanceUsdMicros: cached.balanceUsdMicros,\n lifetimeGrantedUsdMicros: cached.lifetimeGrantedUsdMicros,\n };\n}\n\nexport async function exchangeDeviceTokenForSigner(\n options: ExchangeDeviceTokenForSignerOptions,\n): Promise<DeviceExchangeResponse> {\n const fetchImpl = options.fetch ?? fetch;\n const url = `${stripTrailingSlashes(options.facadeUrl)}/api/signer/device/exchange`;\n const body: Record<string, string> = { deviceToken: options.deviceToken };\n if (options.scope?.trim()) {\n body.scope = options.scope.trim();\n }\n if (options.clientId?.trim()) {\n body.clientId = options.clientId.trim();\n }\n\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"Device exchange returned invalid JSON\",\n invalidJsonCode: EXCHANGE_RESPONSE_ERROR,\n failureLabel: \"Device exchange failed\",\n defaultErrorCode: \"device_exchange_failed\",\n });\n\n const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);\n const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;\n const signerUrl =\n typeof signerUrlRaw === \"string\" && signerUrlRaw.trim() ? signerUrlRaw.trim() : undefined;\n return normalizeDeviceExchangeResponse(\n {\n access_token: accessToken,\n expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),\n scope:\n typeof parsed.scope === \"string\" && parsed.scope.trim()\n ? parsed.scope.trim()\n : \"sign:job\",\n balanceUsdMicros:\n typeof parsed.balanceUsdMicros === \"string\" ? parsed.balanceUsdMicros : \"0\",\n lifetimeGrantedUsdMicros:\n typeof parsed.lifetimeGrantedUsdMicros === \"string\"\n ? parsed.lifetimeGrantedUsdMicros\n : \"0\",\n },\n { signerUrl },\n );\n}\n\ntype CreateDeviceExchangeHandlerInput =\n | DeviceExchangeHandlerConfig\n | DeviceExchangeHandlerConfigRemote;\n\nfunction resolveMint(\n config: CreateDeviceExchangeHandlerInput,\n): (deviceToken: string, context: DeviceExchangeMintContext) => Promise<DeviceExchangeMintResult> {\n if (\"mint\" in config && typeof config.mint === \"function\") {\n return config.mint;\n }\n return (deviceToken, context) =>\n mintSignerTokenFromDeviceToken({\n issuerUrl: config.issuerUrl,\n m2mClientId: config.m2mClientId,\n m2mClientSecret: config.m2mClientSecret,\n deviceToken,\n scope: context.scope,\n audience: config.audience,\n fetch: config.fetch,\n allowInsecureHttp: config.allowInsecureHttp,\n });\n}\n\nfunction resolveSignerUrlFromConfig(\n config: CreateDeviceExchangeHandlerInput,\n): string | Promise<string | undefined> | undefined {\n if (\"signerUrl\" in config && typeof config.signerUrl === \"string\" && config.signerUrl.trim()) {\n return config.signerUrl.trim();\n }\n if (\"getSignerUrl\" in config && typeof config.getSignerUrl === \"function\") {\n return config.getSignerUrl();\n }\n return undefined;\n}\n\nexport function createDeviceExchangeHandler(\n config: CreateDeviceExchangeHandlerInput,\n): (request: Request) => Promise<Response> {\n const mint = resolveMint(config);\n\n return async function deviceExchangeHandler(request: Request): Promise<Response> {\n try {\n if (request.method !== \"POST\") {\n return new Response(JSON.stringify({ error: \"method_not_allowed\" }), {\n status: 405,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n const parsed = await parseDeviceExchangeRequestBody(request);\n const minted = await mint(parsed.deviceToken, {\n scope: parsed.scope,\n clientId: parsed.clientId,\n });\n const signerUrlValue = await resolveSignerUrlFromConfig(config);\n const body = normalizeDeviceExchangeResponse(minted, {\n signerUrl: typeof signerUrlValue === \"string\" ? signerUrlValue : undefined,\n });\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"no-store\",\n },\n });\n } catch (error) {\n return signerHandlerErrorResponse(error);\n }\n };\n}\n","import { stripIssuerOriginFromOidcUrl, stripTrailingSlashes } from \"../string-utils.js\";\nimport { PmtHouseError } from \"../errors.js\";\nimport { readJsonObjectFromResponse } from \"./fetch-json.js\";\nimport { signerHandlerErrorResponse } from \"./handler-errors.js\";\nimport {\n extractSignerAccessTokenFromExchangeBody,\n mintSignerTokenFromDeviceToken,\n normalizeDeviceExchangeResponse,\n} from \"./device-exchange.js\";\nimport type {\n ApiKeyExchangeHandlerConfig,\n ApiKeyExchangeMintResult,\n ApiKeyExchangeRequestBody,\n DeviceExchangeResponse,\n ExchangeApiKeyForSignerOptions,\n} from \"./types.js\";\n\nconst EXCHANGE_RESPONSE_ERROR = \"invalid_exchange_response\";\n\nexport async function parseApiKeyExchangeRequestBody(\n request: Request,\n): Promise<ApiKeyExchangeRequestBody> {\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n throw new PmtHouseError(\"Request body must be JSON\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) {\n throw new PmtHouseError(\"Request body must be a JSON object\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const record = body as Record<string, unknown>;\n const apiKeyRaw = record.apiKey;\n if (typeof apiKeyRaw !== \"string\" || !apiKeyRaw.trim()) {\n throw new PmtHouseError(\"Request body must include apiKey\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const scope =\n typeof record.scope === \"string\" && record.scope.trim()\n ? record.scope.trim()\n : undefined;\n const clientId =\n typeof record.clientId === \"string\" && record.clientId.trim()\n ? record.clientId.trim()\n : undefined;\n return { apiKey: apiKeyRaw.trim(), scope, clientId };\n}\n\nexport async function mintUserAccessTokenFromApiKey(input: {\n issuerUrl: string;\n publicClientId: string;\n apiKey: string;\n scope?: string;\n fetch?: typeof fetch;\n}): Promise<{ access_token: string; expires_in: number; scope: string }> {\n const fetchImpl = input.fetch ?? fetch;\n const issuerOrigin = stripIssuerOriginFromOidcUrl(input.issuerUrl);\n const url = `${issuerOrigin}/api/v1/apps/${encodeURIComponent(input.publicClientId)}/auth/api-key/token`;\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${input.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(input.scope ? { scope: input.scope } : {}),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"API key token exchange returned invalid JSON\",\n invalidJsonCode: \"invalid_token_response\",\n failureLabel: \"API key token exchange failed\",\n defaultErrorCode: \"api_key_token_exchange_failed\",\n });\n\n const accessToken = parsed.access_token;\n if (typeof accessToken !== \"string\" || !accessToken.trim()) {\n throw new PmtHouseError(\"API key token exchange missing access_token\", {\n status: 502,\n code: EXCHANGE_RESPONSE_ERROR,\n });\n }\n\n const expiresIn =\n typeof parsed.expires_in === \"number\" && Number.isFinite(parsed.expires_in)\n ? parsed.expires_in\n : 900;\n const scope =\n typeof parsed.scope === \"string\" && parsed.scope.trim()\n ? parsed.scope.trim()\n : input.scope?.trim() || \"sign:job\";\n\n return {\n access_token: accessToken.trim(),\n expires_in: expiresIn,\n scope,\n };\n}\n\nexport async function mintSignerSessionFromApiKey(input: {\n issuerUrl: string;\n publicClientId: string;\n m2mClientId: string;\n m2mClientSecret: string;\n apiKey: string;\n scope?: string;\n audience?: string;\n fetch?: typeof fetch;\n allowInsecureHttp?: boolean;\n}): Promise<ApiKeyExchangeMintResult> {\n const userToken = await mintUserAccessTokenFromApiKey({\n issuerUrl: input.issuerUrl,\n publicClientId: input.publicClientId,\n apiKey: input.apiKey,\n scope: input.scope,\n fetch: input.fetch,\n });\n\n return mintSignerTokenFromDeviceToken({\n issuerUrl: input.issuerUrl,\n m2mClientId: input.m2mClientId,\n m2mClientSecret: input.m2mClientSecret,\n deviceToken: userToken.access_token,\n scope: userToken.scope,\n audience: input.audience,\n fetch: input.fetch,\n allowInsecureHttp: input.allowInsecureHttp,\n });\n}\n\nexport async function exchangeApiKeyForSigner(\n options: ExchangeApiKeyForSignerOptions,\n): Promise<DeviceExchangeResponse> {\n const fetchImpl = options.fetch ?? fetch;\n const url = `${stripTrailingSlashes(options.facadeUrl)}/api/pymthouse/keys/exchange`;\n const body: Record<string, string> = { apiKey: options.apiKey };\n if (options.scope?.trim()) {\n body.scope = options.scope.trim();\n }\n if (options.clientId?.trim()) {\n body.clientId = options.clientId.trim();\n }\n\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"API key exchange returned invalid JSON\",\n invalidJsonCode: EXCHANGE_RESPONSE_ERROR,\n failureLabel: \"API key exchange failed\",\n defaultErrorCode: \"api_key_exchange_failed\",\n });\n\n const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);\n const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;\n const signerUrl =\n typeof signerUrlRaw === \"string\" && signerUrlRaw.trim() ? signerUrlRaw.trim() : undefined;\n\n return normalizeDeviceExchangeResponse(\n {\n access_token: accessToken,\n expires_in:\n typeof parsed.expires_in === \"number\" && Number.isFinite(parsed.expires_in)\n ? parsed.expires_in\n : 3600,\n scope:\n typeof parsed.scope === \"string\" && parsed.scope.trim()\n ? parsed.scope.trim()\n : \"sign:job\",\n balanceUsdMicros:\n typeof parsed.balanceUsdMicros === \"string\" ? parsed.balanceUsdMicros : \"0\",\n lifetimeGrantedUsdMicros:\n typeof parsed.lifetimeGrantedUsdMicros === \"string\"\n ? parsed.lifetimeGrantedUsdMicros\n : \"0\",\n },\n { signerUrl },\n );\n}\n\nexport function createApiKeyExchangeHandler(\n config: ApiKeyExchangeHandlerConfig,\n): (request: Request) => Promise<Response> {\n const publicClientId = config.publicClientId.trim();\n\n return async function apiKeyExchangeHandler(request: Request): Promise<Response> {\n try {\n if (request.method !== \"POST\") {\n return new Response(JSON.stringify({ error: \"method_not_allowed\" }), {\n status: 405,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n const parsed = await parseApiKeyExchangeRequestBody(request);\n const effectiveClientId = parsed.clientId?.trim() || publicClientId;\n if (effectiveClientId !== publicClientId) {\n throw new PmtHouseError(\"clientId does not match configured public client\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n\n const minted = await mintSignerSessionFromApiKey({\n issuerUrl: config.issuerUrl,\n publicClientId,\n m2mClientId: config.m2mClientId,\n m2mClientSecret: config.m2mClientSecret,\n apiKey: parsed.apiKey,\n scope: parsed.scope,\n audience: config.audience,\n fetch: config.fetch,\n allowInsecureHttp: config.allowInsecureHttp,\n });\n\n const signerUrlValue =\n typeof config.signerUrl === \"string\" && config.signerUrl.trim()\n ? config.signerUrl.trim()\n : undefined;\n\n const body = normalizeDeviceExchangeResponse(minted, { signerUrl: signerUrlValue });\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"no-store\",\n },\n });\n } catch (error) {\n return signerHandlerErrorResponse(error);\n }\n };\n}\n","import { exchangeApiKeyForSigner } from \"../../signer/api-key-exchange.js\";\n\nexport type SignerCredentials =\n | { type: \"bearer\"; accessToken: string }\n | { type: \"apiKey\"; apiKey: string; facadeUrl: string; scope?: string; clientId?: string };\n\nconst boundFetch: typeof fetch = (input, init) => globalThis.fetch(input, init);\n\nexport async function resolveSignerToken(\n credentials: SignerCredentials,\n fetchImpl: typeof fetch = boundFetch,\n): Promise<string> {\n if (credentials.type === \"bearer\") {\n const token = credentials.accessToken.trim();\n if (!token) {\n throw new Error(\"Signer bearer token is empty\");\n }\n return token;\n }\n\n const exchanged = await exchangeApiKeyForSigner({\n facadeUrl: credentials.facadeUrl,\n apiKey: credentials.apiKey,\n scope: credentials.scope ?? \"sign:job\",\n clientId: credentials.clientId,\n fetch: fetchImpl,\n });\n\n const token =\n exchanged.access_token?.trim() ||\n exchanged.token?.accessToken?.trim() ||\n exchanged.token?.access_token?.trim();\n if (!token) {\n throw new Error(\"API key exchange did not return a signer access token\");\n }\n return token;\n}\n","import {\n TRICKLE_SEQ_CURRENT,\n TRICKLE_SEQ_LATEST,\n type GatewayLiveSubscribeSegment,\n type GatewaySegmentPublishResult,\n type StartGatewaySessionRequest,\n type StartGatewaySessionResponse,\n} from \"../types.js\";\nimport { stripTrailingSlashes } from \"../../string-utils.js\";\nimport { resolveSignerToken, type SignerCredentials } from \"./resolve-signer.js\";\n\nfunction jsonErrorMessage(\n body: Record<string, unknown>,\n status: number,\n fallback: string,\n): string {\n if (typeof body.message === \"string\") {\n return body.message;\n }\n if (typeof body.error === \"string\") {\n return body.error;\n }\n return `${fallback} (${status})`;\n}\n\nfunction resolveSubscribeSegmentSeq(requestedSeq: number, headerSeq: number): number {\n if (Number.isFinite(headerSeq)) {\n return headerSeq;\n }\n if (requestedSeq >= 0) {\n return requestedSeq;\n }\n return 0;\n}\n\nfunction isTrickleLeadingIndex(seq: number): boolean {\n return seq === TRICKLE_SEQ_LATEST || seq === TRICKLE_SEQ_CURRENT;\n}\n\nfunction parseHeaderInt(headers: Headers, name: string): number {\n const raw = headers.get(name);\n if (!raw) {\n return Number.NaN;\n }\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) ? parsed : Number.NaN;\n}\n\nexport type BrowserGatewayClientOptions = {\n /** Dashboard (or gateway relay) origin, e.g. https://dashboard.example.com */\n baseUrl: string;\n fetch?: typeof fetch;\n};\n\nexport class BrowserGatewayClient {\n private signerToken: string | null = null;\n private sessionId: string | null = null;\n private publishSeq = -1;\n /** Next trickle GET index for orchestrator output (see TrickleSubscriber). */\n private subscribeSeq = TRICKLE_SEQ_CURRENT;\n private subscribeEmptyPolls = 0;\n private readonly fetchImpl: typeof fetch;\n\n constructor(private readonly options: BrowserGatewayClientOptions) {\n // Unbound `fetch` throws \"Illegal invocation\" when called as a method.\n this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));\n }\n\n get baseUrl(): string {\n return stripTrailingSlashes(this.options.baseUrl);\n }\n\n get signerAccessToken(): string | null {\n return this.signerToken;\n }\n\n get activeSessionId(): string | null {\n return this.sessionId;\n }\n\n get nextSubscribeSeq(): number {\n return this.subscribeSeq;\n }\n\n async connect(credentials: SignerCredentials): Promise<void> {\n this.signerToken = await resolveSignerToken(credentials, this.fetchImpl);\n }\n\n async startSession(request: StartGatewaySessionRequest): Promise<StartGatewaySessionResponse> {\n if (!this.signerToken) {\n throw new Error(\"Call connect() with signer credentials before startSession()\");\n }\n\n const response = await this.fetchImpl(`${this.baseUrl}/api/gateway/sessions`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.signerToken}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(request),\n });\n\n const body = (await response.json().catch(() => ({}))) as Record<string, unknown>;\n if (!response.ok) {\n throw new Error(jsonErrorMessage(body, response.status, \"startSession failed\"));\n }\n\n const sessionId = typeof body.sessionId === \"string\" ? body.sessionId : \"\";\n const manifestId = typeof body.manifestId === \"string\" ? body.manifestId : \"\";\n if (!sessionId) {\n throw new Error(\"startSession response missing sessionId\");\n }\n\n this.sessionId = sessionId;\n const initialPublishSeq =\n typeof body.publishSeq === \"number\" && Number.isFinite(body.publishSeq)\n ? body.publishSeq\n : 0;\n this.publishSeq = initialPublishSeq - 1;\n const initialSubscribeSeq =\n typeof body.subscribeSeq === \"number\" && Number.isFinite(body.subscribeSeq)\n ? body.subscribeSeq\n : TRICKLE_SEQ_CURRENT;\n this.subscribeSeq = initialSubscribeSeq;\n this.subscribeEmptyPolls = 0;\n const mimeType = typeof body.mimeType === \"string\" ? body.mimeType : undefined;\n return {\n sessionId,\n manifestId,\n mimeType,\n publishSeq: initialPublishSeq,\n subscribeSeq: initialSubscribeSeq,\n };\n }\n\n setSignerToken(accessToken: string): void {\n const token = accessToken.trim();\n if (!token) {\n throw new Error(\"Signer bearer token is empty\");\n }\n this.signerToken = token;\n }\n\n async publishSegment(\n bytes: ArrayBuffer | Uint8Array,\n options?: { seq?: number; contentType?: string },\n ): Promise<GatewaySegmentPublishResult> {\n if (!this.sessionId || !this.signerToken) {\n throw new Error(\"No active gateway session\");\n }\n\n const seq = options?.seq ?? this.publishSeq + 1;\n const part =\n bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes);\n const body = new Blob([part]);\n\n const response = await this.fetchImpl(\n `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}/publish/${seq}`,\n {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${this.signerToken}`,\n \"Content-Type\": options?.contentType ?? \"application/octet-stream\",\n },\n body,\n },\n );\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`publishSegment failed (${response.status}): ${text.slice(0, 300)}`);\n }\n\n this.publishSeq = seq;\n return { seq, ok: true };\n }\n\n /**\n * Fetch the next orchestrator output segment (sequential walk from subscribeSeq).\n * Matches livepeer-python-gateway TrickleSubscriber / MediaOutput segment iteration.\n */\n async subscribeOutputSegment(): Promise<GatewayLiveSubscribeSegment | null> {\n const chunks: Uint8Array[] = [];\n const segment = await this.subscribeOutputSegmentStream((chunk) => {\n chunks.push(chunk);\n });\n if (!segment) {\n return null;\n }\n const total = chunks.reduce((sum, part) => sum + part.byteLength, 0);\n const joined = new Uint8Array(total);\n let offset = 0;\n for (const part of chunks) {\n joined.set(part, offset);\n offset += part.byteLength;\n }\n return { ...segment, data: joined.buffer, byteCount: total };\n }\n\n /**\n * Stream the next orchestrator output segment incrementally.\n * Mirrors Python SegmentReader behavior (consume bytes before segment closes).\n */\n async subscribeOutputSegmentStream(\n onChunk: (chunk: Uint8Array) => void,\n ): Promise<Omit<GatewayLiveSubscribeSegment, \"data\" | \"byteCount\"> & { byteCount: number } | null> {\n if (!this.sessionId || !this.signerToken) {\n throw new Error(\"No active gateway session\");\n }\n\n const requestedSeq = this.subscribeSeq;\n const url = new URL(\n `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}/subscribe`,\n );\n url.searchParams.set(\"seq\", String(requestedSeq));\n\n const response = await this.fetchImpl(url.toString(), {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${this.signerToken}`,\n },\n });\n\n if (response.status === 204) {\n this.handleSubscribeEmpty(requestedSeq, response.headers);\n return null;\n }\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`subscribeOutputSegment failed (${response.status}): ${text.slice(0, 300)}`);\n }\n\n const segmentSeq = parseHeaderInt(response.headers, \"X-Gateway-Segment-Seq\");\n const latestSeq = parseHeaderInt(response.headers, \"X-Gateway-Latest-Seq\");\n const resolvedSegmentSeq = resolveSubscribeSegmentSeq(requestedSeq, segmentSeq);\n const resolvedLatest = Number.isFinite(latestSeq) ? latestSeq : resolvedSegmentSeq;\n\n const byteCount = await this.readSubscribeBody(response, onChunk);\n if (byteCount === 0) {\n this.handleSubscribeEmpty(requestedSeq, response.headers);\n return null;\n }\n\n this.advanceSubscribeSeq(requestedSeq, response.headers, resolvedLatest);\n return {\n segmentSeq: resolvedSegmentSeq,\n latestSeq: resolvedLatest,\n nextSeq: this.subscribeSeq,\n byteCount,\n };\n }\n\n private async readSubscribeBody(\n response: Response,\n onChunk: (chunk: Uint8Array) => void,\n ): Promise<number> {\n if (!response.body) {\n const data = await response.arrayBuffer();\n if (data.byteLength === 0) {\n return 0;\n }\n onChunk(new Uint8Array(data));\n return data.byteLength;\n }\n\n const reader = response.body.getReader();\n let bytesRead = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (value && value.byteLength > 0) {\n bytesRead += value.byteLength;\n onChunk(value);\n }\n }\n } finally {\n reader.releaseLock();\n }\n return bytesRead;\n }\n\n /** @deprecated Use subscribeOutputSegment() — live-edge-only polling does not advance output seq. */\n async subscribeLiveSegment(): Promise<GatewayLiveSubscribeSegment | null> {\n return this.subscribeOutputSegment();\n }\n\n private advanceSubscribeSeq(\n requestedSeq: number,\n headers: Headers,\n latestSeq: number,\n ): void {\n const nextHeader = parseHeaderInt(headers, \"X-Gateway-Next-Seq\");\n if (Number.isFinite(nextHeader)) {\n this.subscribeSeq = nextHeader;\n } else {\n const segmentSeq = parseHeaderInt(headers, \"X-Gateway-Segment-Seq\");\n if (Number.isFinite(segmentSeq) && segmentSeq >= 0) {\n this.subscribeSeq = segmentSeq + 1;\n } else if (requestedSeq >= 0) {\n this.subscribeSeq = requestedSeq + 1;\n } else if (Number.isFinite(latestSeq) && latestSeq >= 0) {\n this.subscribeSeq = latestSeq + 1;\n } else {\n this.subscribeSeq = TRICKLE_SEQ_LATEST;\n }\n }\n\n // Skip a large output backlog (MediaOutput LagPolicy.LATEST).\n if (\n Number.isFinite(latestSeq) &&\n latestSeq >= 0 &&\n this.subscribeSeq >= 0 &&\n latestSeq - this.subscribeSeq > 2\n ) {\n this.subscribeSeq = latestSeq;\n }\n }\n\n private handleSubscribeEmpty(requestedSeq: number, headers: Headers): void {\n this.subscribeEmptyPolls += 1;\n const latest = parseHeaderInt(headers, \"X-Gateway-Latest-Seq\");\n if (Number.isFinite(latest) && this.applyLatestSeqOnEmptyPoll(requestedSeq, latest, headers)) {\n return;\n }\n if (requestedSeq >= 0) {\n this.subscribeSeq = requestedSeq;\n return;\n }\n if (this.subscribeEmptyPolls >= 12) {\n this.subscribeSeq = TRICKLE_SEQ_LATEST;\n this.subscribeEmptyPolls = 0;\n }\n }\n\n private applyLatestSeqOnEmptyPoll(\n requestedSeq: number,\n latest: number,\n headers: Headers,\n ): boolean {\n const wait = headers.get(\"X-Gateway-Wait\") === \"1\";\n if (wait && requestedSeq >= 0 && latest < requestedSeq) {\n this.subscribeSeq = requestedSeq;\n return true;\n }\n if (isTrickleLeadingIndex(requestedSeq)) {\n this.subscribeSeq = latest >= 0 ? latest : TRICKLE_SEQ_LATEST;\n this.subscribeEmptyPolls = 0;\n return true;\n }\n if (requestedSeq >= 0 && latest === requestedSeq) {\n this.subscribeSeq = requestedSeq;\n return true;\n }\n if (requestedSeq >= 0 && latest > requestedSeq) {\n this.subscribeSeq = latest;\n this.subscribeEmptyPolls = 0;\n return true;\n }\n return false;\n }\n\n async subscribeSegment(seq?: number): Promise<ArrayBuffer | null> {\n if (seq !== undefined) {\n this.subscribeSeq = seq;\n }\n const segment = await this.subscribeOutputSegment();\n return segment?.data ?? null;\n }\n\n async stop(): Promise<void> {\n if (!this.sessionId || !this.signerToken) {\n return;\n }\n\n await this.fetchImpl(\n `${this.baseUrl}/api/gateway/sessions/${encodeURIComponent(this.sessionId)}`,\n {\n method: \"DELETE\",\n headers: {\n Authorization: `Bearer ${this.signerToken}`,\n },\n },\n ).catch(() => undefined);\n\n this.sessionId = null;\n this.publishSeq = -1;\n this.subscribeSeq = TRICKLE_SEQ_CURRENT;\n this.subscribeEmptyPolls = 0;\n }\n}\n"]}