@pymthouse/builder-sdk 0.3.0 → 0.4.1-rc.1

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 (103) hide show
  1. package/README.md +54 -28
  2. package/dist/{client-BHfjDvIe.d.ts → client-CauCfGa7.d.ts} +1 -1
  3. package/dist/{client-CvhJEhjV.d.cts → client-D1Xz-xlx.d.cts} +1 -1
  4. package/dist/config.cjs +0 -21
  5. package/dist/config.cjs.map +1 -1
  6. package/dist/config.d.cts +1 -5
  7. package/dist/config.d.ts +1 -5
  8. package/dist/config.js +1 -20
  9. package/dist/config.js.map +1 -1
  10. package/dist/device-initiate.cjs.map +1 -1
  11. package/dist/device-initiate.js.map +1 -1
  12. package/dist/device.cjs.map +1 -1
  13. package/dist/device.d.cts +1 -1
  14. package/dist/device.d.ts +1 -1
  15. package/dist/device.js.map +1 -1
  16. package/dist/env.cjs +13 -4
  17. package/dist/env.cjs.map +1 -1
  18. package/dist/env.d.cts +2 -2
  19. package/dist/env.d.ts +2 -2
  20. package/dist/env.js +13 -4
  21. package/dist/env.js.map +1 -1
  22. package/dist/index-BTDKEorK.d.ts +64 -0
  23. package/dist/index-BixH4VIG.d.cts +64 -0
  24. package/dist/index.cjs +13 -4
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +29 -5
  27. package/dist/index.d.ts +29 -5
  28. package/dist/index.js +13 -4
  29. package/dist/index.js.map +1 -1
  30. package/dist/{ingest-DoKJTWU9.d.ts → proxy-JrT6raU_.d.cts} +5 -42
  31. package/dist/{ingest-B3Yi8Tb1.d.cts → proxy-U32DFNuj.d.ts} +5 -42
  32. package/dist/signer/server.cjs +799 -895
  33. package/dist/signer/server.cjs.map +1 -1
  34. package/dist/signer/server.d.cts +9 -13
  35. package/dist/signer/server.d.ts +9 -13
  36. package/dist/signer/server.js +799 -893
  37. package/dist/signer/server.js.map +1 -1
  38. package/dist/signer/webhook/adapters/api-key.cjs +78 -0
  39. package/dist/signer/webhook/adapters/api-key.cjs.map +1 -0
  40. package/dist/signer/webhook/adapters/api-key.d.cts +18 -0
  41. package/dist/signer/webhook/adapters/api-key.d.ts +18 -0
  42. package/dist/signer/webhook/adapters/api-key.js +76 -0
  43. package/dist/signer/webhook/adapters/api-key.js.map +1 -0
  44. package/dist/signer/webhook/adapters/composite.cjs +60 -0
  45. package/dist/signer/webhook/adapters/composite.cjs.map +1 -0
  46. package/dist/signer/webhook/adapters/composite.d.cts +5 -0
  47. package/dist/signer/webhook/adapters/composite.d.ts +5 -0
  48. package/dist/signer/webhook/adapters/composite.js +58 -0
  49. package/dist/signer/webhook/adapters/composite.js.map +1 -0
  50. package/dist/signer/webhook/adapters/oauth1.cjs +18 -0
  51. package/dist/signer/webhook/adapters/oauth1.cjs.map +1 -0
  52. package/dist/signer/webhook/adapters/oauth1.d.cts +19 -0
  53. package/dist/signer/webhook/adapters/oauth1.d.ts +19 -0
  54. package/dist/signer/webhook/adapters/oauth1.js +16 -0
  55. package/dist/signer/webhook/adapters/oauth1.js.map +1 -0
  56. package/dist/signer/webhook/adapters/oidc.cjs +522 -0
  57. package/dist/signer/webhook/adapters/oidc.cjs.map +1 -0
  58. package/dist/signer/webhook/adapters/oidc.d.cts +4 -0
  59. package/dist/signer/webhook/adapters/oidc.d.ts +4 -0
  60. package/dist/signer/webhook/adapters/oidc.js +515 -0
  61. package/dist/signer/webhook/adapters/oidc.js.map +1 -0
  62. package/dist/signer/webhook/adapters/trusted-headers.cjs +103 -0
  63. package/dist/signer/webhook/adapters/trusted-headers.cjs.map +1 -0
  64. package/dist/signer/webhook/adapters/trusted-headers.d.cts +18 -0
  65. package/dist/signer/webhook/adapters/trusted-headers.d.ts +18 -0
  66. package/dist/signer/webhook/adapters/trusted-headers.js +99 -0
  67. package/dist/signer/webhook/adapters/trusted-headers.js.map +1 -0
  68. package/dist/signer/webhook.cjs +747 -0
  69. package/dist/signer/webhook.cjs.map +1 -0
  70. package/dist/signer/webhook.d.cts +26 -0
  71. package/dist/signer/webhook.d.ts +26 -0
  72. package/dist/signer/webhook.js +721 -0
  73. package/dist/signer/webhook.js.map +1 -0
  74. package/dist/tokens.d.cts +1 -1
  75. package/dist/tokens.d.ts +1 -1
  76. package/dist/{types-_R1AwEZp.d.cts → types-BORaHW_x.d.cts} +5 -5
  77. package/dist/{types-_R1AwEZp.d.ts → types-BORaHW_x.d.ts} +5 -5
  78. package/dist/verifier-B-WFDMz6.d.cts +48 -0
  79. package/dist/verifier-B-WFDMz6.d.ts +48 -0
  80. package/dist/verify.cjs.map +1 -1
  81. package/dist/verify.d.cts +1 -1
  82. package/dist/verify.d.ts +1 -1
  83. package/dist/verify.js.map +1 -1
  84. package/package.json +30 -30
  85. package/dist/gateway/client/index.cjs +0 -492
  86. package/dist/gateway/client/index.cjs.map +0 -1
  87. package/dist/gateway/client/index.d.cts +0 -63
  88. package/dist/gateway/client/index.d.ts +0 -63
  89. package/dist/gateway/client/index.js +0 -489
  90. package/dist/gateway/client/index.js.map +0 -1
  91. package/dist/gateway/index.cjs +0 -16
  92. package/dist/gateway/index.cjs.map +0 -1
  93. package/dist/gateway/index.d.cts +0 -52
  94. package/dist/gateway/index.d.ts +0 -52
  95. package/dist/gateway/index.js +0 -10
  96. package/dist/gateway/index.js.map +0 -1
  97. package/dist/gateway/server/index.cjs +0 -1248
  98. package/dist/gateway/server/index.cjs.map +0 -1
  99. package/dist/gateway/server/index.d.cts +0 -31
  100. package/dist/gateway/server/index.d.ts +0 -31
  101. package/dist/gateway/server/index.js +0 -1233
  102. package/dist/gateway/server/index.js.map +0 -1
  103. package/gateway/proto/lp_rpc.proto +0 -542
@@ -19,8 +19,14 @@ var PmtHouseError = class extends Error {
19
19
  };
20
20
 
21
21
  // src/signer/handler-errors.ts
22
- function signerHandlerErrorResponse(error) {
22
+ function isPmtHouseError(error) {
23
23
  if (error instanceof PmtHouseError) {
24
+ return true;
25
+ }
26
+ return error instanceof Error && typeof error.status === "number" && typeof error.code === "string";
27
+ }
28
+ function signerHandlerErrorResponse(error) {
29
+ if (isPmtHouseError(error)) {
24
30
  return new Response(
25
31
  JSON.stringify({
26
32
  error: error.code,
@@ -48,17 +54,14 @@ function createSignerTokenManager(options) {
48
54
  const ttlRefreshRatio = options.ttlRefreshRatio ?? 0.8;
49
55
  const cache = /* @__PURE__ */ new Map();
50
56
  const inflight = /* @__PURE__ */ new Map();
51
- function keyFor(externalUserId) {
52
- return cacheKey(options.publicClientId, externalUserId);
53
- }
54
57
  function isUsable(entry, now, forceRefresh) {
55
58
  if (forceRefresh) return false;
56
59
  if (now >= entry.expiresAt) return false;
57
60
  if (now >= entry.refreshAt) return false;
58
61
  return true;
59
62
  }
60
- async function refresh(externalUserId) {
61
- const key = keyFor(externalUserId);
63
+ async function refresh(publicClientId, externalUserId) {
64
+ const key = cacheKey(publicClientId, externalUserId);
62
65
  const existing = inflight.get(key);
63
66
  if (existing) {
64
67
  return existing;
@@ -79,22 +82,22 @@ function createSignerTokenManager(options) {
79
82
  return promise;
80
83
  }
81
84
  return {
82
- peek(externalUserId) {
83
- return cache.get(keyFor(externalUserId));
85
+ peek(publicClientId, externalUserId) {
86
+ return cache.get(cacheKey(publicClientId, externalUserId));
84
87
  },
85
- invalidate(externalUserId) {
86
- const key = keyFor(externalUserId);
88
+ invalidate(publicClientId, externalUserId) {
89
+ const key = cacheKey(publicClientId, externalUserId);
87
90
  cache.delete(key);
88
91
  inflight.delete(key);
89
92
  },
90
- async getToken(externalUserId, getOptions = {}) {
93
+ async getToken(publicClientId, externalUserId, getOptions = {}) {
91
94
  const now = Date.now();
92
- const key = keyFor(externalUserId);
95
+ const key = cacheKey(publicClientId, externalUserId);
93
96
  const cached = cache.get(key);
94
97
  if (cached && isUsable(cached, now, getOptions.forceRefresh === true)) {
95
98
  return cached;
96
99
  }
97
- return refresh(externalUserId);
100
+ return refresh(publicClientId, externalUserId);
98
101
  }
99
102
  };
100
103
  }
@@ -212,13 +215,6 @@ async function forwardDirectSignerRequest(options) {
212
215
  return fetchImpl(target, init);
213
216
  }
214
217
 
215
- // src/encoding.ts
216
- function encodeClientSecretBasic(clientId, clientSecret) {
217
- const raw = `${clientId}:${clientSecret}`;
218
- const b64 = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(Array.from(new TextEncoder().encode(raw), (c) => String.fromCharCode(c)).join(""));
219
- return `Basic ${b64}`;
220
- }
221
-
222
218
  // src/string-utils.ts
223
219
  function stripTrailingSlashes(value) {
224
220
  let end = value.length;
@@ -250,986 +246,896 @@ function stripIssuerOriginFromOidcUrl(issuerUrl) {
250
246
  base = stripSuffixIgnoreCase(base, "/oidc");
251
247
  return stripTrailingSlashes(base);
252
248
  }
253
-
254
- // src/ingest.ts
255
- function signerSnapshotToIngestPayload(input) {
256
- return {
257
- requestId: input.snapshot.requestId,
258
- externalUserId: input.externalUserId,
259
- networkFeeUsdMicros: input.snapshot.computedFeeUsdMicros.toString(),
260
- feeWei: input.snapshot.computedFeeWei,
261
- pixels: input.snapshot.pixels,
262
- pipeline: input.snapshot.pipeline,
263
- modelId: input.snapshot.modelId,
264
- gatewayRequestId: input.gatewayRequestId,
265
- ethUsdPrice: input.snapshot.ethUsdPrice,
266
- ethUsdRoundId: input.snapshot.ethUsdRoundId,
267
- ethUsdObservedAt: input.snapshot.ethUsdObservedAt
268
- };
269
- }
270
- function ingestUrl(issuerUrl, publicClientId) {
271
- const origin = new URL(stripTrailingSlashes(issuerUrl)).origin;
272
- return `${origin}/api/v1/apps/${encodeURIComponent(publicClientId)}/usage/signed-tickets`;
249
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
250
+ var discoveryCache = /* @__PURE__ */ new Map();
251
+ function normalizedIssuerKey(issuerUrl) {
252
+ return stripTrailingSlashes(issuerUrl);
273
253
  }
274
- async function readJsonResponse(response) {
275
- const text = await response.text();
276
- if (!text.trim()) {
277
- return {};
254
+ async function loadAuthorizationServer(issuerUrl, fetchImpl, options = {}) {
255
+ const key = normalizedIssuerKey(issuerUrl);
256
+ const now = Date.now();
257
+ const cached = discoveryCache.get(key);
258
+ if (!options.force && cached && now - cached.fetchedAt < CACHE_TTL_MS) {
259
+ return cached.as;
260
+ }
261
+ const issuerIdentifier = new URL(key);
262
+ const discoveryOpts = {
263
+ algorithm: "oidc",
264
+ [customFetch]: fetchImpl
265
+ };
266
+ if (options.allowInsecureHttp) {
267
+ discoveryOpts[allowInsecureRequests] = true;
278
268
  }
269
+ let response;
279
270
  try {
280
- const parsed = JSON.parse(text);
281
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
282
- } catch {
283
- return {};
271
+ response = await discoveryRequest(issuerIdentifier, discoveryOpts);
272
+ } catch (e) {
273
+ throw mapDiscoveryNetworkError(e);
284
274
  }
285
- }
286
- async function ingestSignedTicket(options) {
287
- const fetchImpl = options.fetch ?? fetch;
288
- const url = ingestUrl(options.issuerUrl, options.publicClientId);
289
- const response = await fetchImpl(url, {
290
- method: "POST",
291
- headers: {
292
- Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
293
- "Content-Type": "application/json",
294
- Accept: "application/json"
295
- },
296
- body: JSON.stringify(options.ticket),
297
- cache: "no-store"
298
- });
299
- const body = await readJsonResponse(response);
300
- if (!response.ok) {
301
- const message = typeof body.error === "string" ? body.error : `Signed-ticket ingest failed (${response.status})`;
302
- throw new PmtHouseError(message, {
303
- status: response.status,
304
- code: "ingest_failed",
305
- details: body
306
- });
275
+ let as;
276
+ try {
277
+ as = await processDiscoveryResponse(issuerIdentifier, response);
278
+ } catch (e) {
279
+ throw mapOAuthDiscoveryError(e);
307
280
  }
308
- return {
309
- ingested: Boolean(body.ingested),
310
- duplicate: Boolean(body.duplicate),
311
- source: body.source === "openmeter" ? "openmeter" : "disabled"
312
- };
313
- }
314
-
315
- // src/signer/proxy.ts
316
- var HTTP_DMZ_TOKEN_MAX_ENTRIES = 100;
317
- var HTTP_DMZ_TOKEN_TTL_MS = 3.5 * 60 * 1e3;
318
- var DEFAULT_PROBE_SUBJECT = "signer-reachability-probe";
319
- var httpDmzTokenCache = /* @__PURE__ */ new Map();
320
- function normalizeSignerBaseUrl(base) {
321
- return stripTrailingSlashes(base);
281
+ discoveryCache.set(key, { as, fetchedAt: now });
282
+ return as;
322
283
  }
323
- function joinSignerUrl(baseUrl, path) {
324
- if (path.startsWith("/")) {
325
- return `${baseUrl}${path}`;
284
+ function mapOAuthDiscoveryError(error) {
285
+ if (error instanceof PmtHouseError) {
286
+ return error;
326
287
  }
327
- return `${baseUrl}/${path}`;
328
- }
329
- function aliasBodyString(raw) {
330
- if (raw === void 0 || raw === null) {
331
- return null;
288
+ if (error instanceof Error) {
289
+ return new PmtHouseError(error.message, {
290
+ status: 500,
291
+ code: "oidc_discovery_invalid",
292
+ details: { cause: error.cause }
293
+ });
332
294
  }
333
- if (typeof raw === "string") {
334
- return raw.length > 0 ? raw : null;
295
+ return new PmtHouseError("OIDC discovery failed", {
296
+ status: 500,
297
+ code: "oidc_discovery_invalid"
298
+ });
299
+ }
300
+ function mapDiscoveryNetworkError(error) {
301
+ if (error instanceof PmtHouseError) {
302
+ return error;
335
303
  }
336
- if (typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint") {
337
- return String(raw);
304
+ if (error instanceof Error) {
305
+ return new PmtHouseError(`Failed to load OIDC discovery: ${error.message}`, {
306
+ status: 502,
307
+ code: "oidc_discovery_failed"
308
+ });
338
309
  }
339
- return null;
310
+ return new PmtHouseError("Failed to load OIDC discovery", {
311
+ status: 502,
312
+ code: "oidc_discovery_failed"
313
+ });
340
314
  }
341
- function resolveSignerBaseUrl(input) {
342
- if (input.testSignerUrl?.trim()) {
343
- return normalizeSignerBaseUrl(input.testSignerUrl);
344
- }
345
- const legacyBareSignerPort = 8081;
346
- const rawPort = input.storedPort ?? input.defaultPort ?? 8080;
347
- const port = rawPort === legacyBareSignerPort ? 8080 : rawPort;
348
- const base = input.envUrl?.trim() || input.storedUrl?.trim() || `http://127.0.0.1:${port}`;
349
- return normalizeSignerBaseUrl(base);
315
+
316
+ // src/encoding.ts
317
+ function encodeClientSecretBasic(clientId, clientSecret) {
318
+ const raw = `${clientId}:${clientSecret}`;
319
+ const b64 = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(Array.from(new TextEncoder().encode(raw), (c) => String.fromCharCode(c)).join(""));
320
+ return `Basic ${b64}`;
350
321
  }
351
- async function getCachedDmzBearerToken(subject, gate, getDmzToken) {
352
- const cacheKey2 = `${gate}:${subject}`;
353
- const now = Date.now();
354
- const cached = httpDmzTokenCache.get(cacheKey2);
355
- if (cached && cached.expMs > now + 15e3) {
356
- httpDmzTokenCache.delete(cacheKey2);
357
- httpDmzTokenCache.set(cacheKey2, cached);
358
- return cached.token;
322
+
323
+ // src/signer/fetch-json.ts
324
+ function oauthFailureDescription(parsed, failureLabel, status) {
325
+ if (typeof parsed.error_description === "string") {
326
+ return parsed.error_description;
359
327
  }
360
- const token = await getDmzToken(subject, gate);
361
- httpDmzTokenCache.set(cacheKey2, { token, expMs: now + HTTP_DMZ_TOKEN_TTL_MS });
362
- if (httpDmzTokenCache.size > HTTP_DMZ_TOKEN_MAX_ENTRIES) {
363
- const oldest = httpDmzTokenCache.keys().next().value;
364
- if (oldest !== void 0) {
365
- httpDmzTokenCache.delete(oldest);
366
- }
328
+ if (typeof parsed.error === "string") {
329
+ return parsed.error;
367
330
  }
368
- return token;
331
+ return `${failureLabel} (${status})`;
369
332
  }
370
- async function readSignerUpstreamBody(response) {
333
+ async function readJsonObjectFromResponse(response, options) {
371
334
  const text = await response.text();
372
- if (!text.trim()) {
373
- return {};
374
- }
335
+ let parsed;
375
336
  try {
376
- return JSON.parse(text);
337
+ parsed = text ? JSON.parse(text) : {};
377
338
  } catch {
378
- return {
379
- error: "Signer DMZ returned a non-JSON body (often Apache auth failure)",
380
- upstreamStatus: response.status,
381
- detail: text.slice(0, 800)
382
- };
339
+ throw new PmtHouseError(options.invalidJsonMessage, {
340
+ status: 502,
341
+ code: options.invalidJsonCode,
342
+ details: { status: response.status }
343
+ });
383
344
  }
384
- }
385
- function pickConflictingStringAliases(body, ...keys) {
386
- const values = keys.map((key) => {
387
- const value = aliasBodyString(body[key]);
388
- return value === null ? null : { key, value };
389
- }).filter((entry) => entry !== null);
390
- const first = values[0];
391
- const conflict = values.find((entry) => entry.value !== first?.value);
392
- if (first && conflict) {
393
- return {
394
- ok: false,
395
- message: `Conflicting ${keys.join("/")} in request body`
396
- };
345
+ if (!response.ok) {
346
+ const description = oauthFailureDescription(parsed, options.failureLabel, response.status);
347
+ throw new PmtHouseError(description, {
348
+ status: response.status,
349
+ code: typeof parsed.error === "string" ? parsed.error : options.defaultErrorCode,
350
+ details: parsed
351
+ });
397
352
  }
398
- return { ok: true, value: first?.value };
353
+ return parsed;
399
354
  }
400
- function pickConflictingNumberAliases(body, ...keys) {
401
- const parseNum = (value) => {
402
- if (typeof value === "number" && Number.isFinite(value)) {
403
- return value;
404
- }
405
- if (typeof value === "string" && value.trim() !== "") {
406
- const parsed = Number(value);
407
- return Number.isFinite(parsed) ? parsed : void 0;
408
- }
409
- return void 0;
410
- };
411
- const values = keys.map((key) => {
412
- const value = parseNum(body[key]);
413
- return value === void 0 ? null : { key, value };
414
- }).filter((entry) => entry !== null);
415
- const first = values[0];
416
- const conflict = values.find((entry) => entry.value !== first?.value);
417
- if (first && conflict) {
418
- return {
419
- ok: false,
420
- message: `Conflicting ${keys.join("/")} in request body`
421
- };
355
+
356
+ // src/signer/json-fields.ts
357
+ function readStringField(body, key, errorCode, messagePrefix = "Response") {
358
+ const value = body[key];
359
+ if (typeof value !== "string" || !value.trim()) {
360
+ throw new PmtHouseError(`${messagePrefix} missing ${key}`, {
361
+ status: 502,
362
+ code: errorCode
363
+ });
422
364
  }
423
- return { ok: true, value: first?.value };
365
+ return value.trim();
424
366
  }
425
- function pickString(obj, ...keys) {
426
- for (const key of keys) {
427
- const value = obj[key];
428
- if (typeof value === "string" && value.trim()) {
429
- return value.trim();
430
- }
431
- if (typeof value === "number" && Number.isFinite(value)) {
432
- return String(Math.trunc(value));
433
- }
367
+ function readExpiresIn(body, errorCode) {
368
+ const expiresIn = body.expires_in;
369
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
370
+ throw new PmtHouseError("Response missing expires_in", {
371
+ status: 502,
372
+ code: errorCode
373
+ });
434
374
  }
435
- return "";
375
+ return Math.floor(expiresIn);
436
376
  }
437
- function parseSignerUsageSnapshot(body) {
438
- if (body === null || typeof body !== "object" || Array.isArray(body)) {
439
- return null;
440
- }
441
- const record = body;
442
- const usageRaw = record.usage;
443
- if (usageRaw === null || typeof usageRaw !== "object" || Array.isArray(usageRaw)) {
444
- return null;
445
- }
446
- const usage = usageRaw;
447
- const computedFeeWei = pickString(usage, "computed_fee_wei", "computedFeeWei");
448
- const usdMicrosStr = pickString(usage, "computed_fee_usd_micros", "computedFeeUsdMicros");
449
- const requestId = pickString(usage, "request_id", "requestId");
450
- if (!computedFeeWei || !usdMicrosStr || !requestId) {
451
- return null;
452
- }
453
- let computedFeeUsdMicros;
454
- try {
455
- computedFeeUsdMicros = BigInt(usdMicrosStr);
456
- } catch {
457
- return null;
458
- }
377
+
378
+ // src/signer/mint-token.ts
379
+ var SIGN_MINT_USER_TOKEN_SCOPE = "sign:mint_user_token";
380
+ var LIVEPEER_REMOTE_SIGNER_AUDIENCE = "livepeer-remote-signer";
381
+ var DEFAULT_TTL_REFRESH_RATIO = 0.8;
382
+ var TOKEN_RESPONSE_ERROR = "invalid_token_response";
383
+ function signerJwtAudience(issuerUrl) {
384
+ return stripTrailingSlashes(issuerUrl);
385
+ }
386
+ function parseMintUserSignerTokenResponse(body, ttlRefreshRatio = DEFAULT_TTL_REFRESH_RATIO) {
387
+ const accessToken = readStringField(body, "access_token", TOKEN_RESPONSE_ERROR, "Token response");
388
+ const expiresIn = readExpiresIn(body, TOKEN_RESPONSE_ERROR);
389
+ const balanceUsdMicros = readStringField(
390
+ body,
391
+ "balanceUsdMicros",
392
+ TOKEN_RESPONSE_ERROR,
393
+ "Token response"
394
+ );
395
+ const lifetimeGrantedUsdMicros = readStringField(
396
+ body,
397
+ "lifetimeGrantedUsdMicros",
398
+ TOKEN_RESPONSE_ERROR,
399
+ "Token response"
400
+ );
401
+ const now = Date.now();
402
+ const expiresAt = now + expiresIn * 1e3;
403
+ const refreshAt = now + Math.floor(expiresIn * 1e3 * ttlRefreshRatio);
459
404
  return {
460
- requestId,
461
- computedFeeWei,
462
- computedFeeUsdMicros,
463
- ethUsdPrice: pickString(usage, "eth_usd_price", "ethUsdPrice") || void 0,
464
- ethUsdRoundId: pickString(usage, "eth_usd_round_id", "ethUsdRoundId") || void 0,
465
- ethUsdObservedAt: pickString(usage, "eth_usd_updated_at", "ethUsdUpdatedAt", "eth_usd_observed_at") || void 0,
466
- pixels: pickString(usage, "pixels") || void 0,
467
- billableSecs: pickString(usage, "billable_secs", "billableSecs") || void 0,
468
- pipeline: pickString(usage, "pipeline") || void 0,
469
- modelId: pickString(usage, "model_id", "modelId") || void 0
405
+ jwt: accessToken,
406
+ expiresAt,
407
+ refreshAt,
408
+ balanceUsdMicros,
409
+ lifetimeGrantedUsdMicros
470
410
  };
471
411
  }
472
- function stripSignerUsageFromResponse(body) {
473
- if (body !== null && typeof body === "object" && !Array.isArray(body)) {
474
- delete body.usage;
475
- }
476
- }
477
- async function forwardToSigner(options) {
412
+ async function mintUserSignerToken(options) {
478
413
  const fetchImpl = options.fetch ?? fetch;
479
- const baseUrl = normalizeSignerBaseUrl(options.baseUrl);
480
- const url = joinSignerUrl(baseUrl, options.path);
481
- const timeoutMs = options.timeoutMs ?? 3e4;
482
- const controller = new AbortController();
483
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
484
- const headers = { "Content-Type": "application/json" };
485
- const attachJwt = options.forwardJwt ?? true;
486
- if (attachJwt) {
487
- const token = await getCachedDmzBearerToken(
488
- options.subject,
489
- "http",
490
- options.getDmzToken
491
- );
492
- headers.Authorization = `Bearer ${token}`;
414
+ const issuerUrl = stripTrailingSlashes(options.issuerUrl);
415
+ const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
416
+ allowInsecureHttp: options.allowInsecureHttp
417
+ });
418
+ const tokenEndpoint = as.token_endpoint;
419
+ if (!tokenEndpoint) {
420
+ throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
421
+ status: 500,
422
+ code: "oidc_discovery_invalid"
423
+ });
493
424
  }
494
- if (options.extraHeaders) {
495
- for (const [name, value] of Object.entries(options.extraHeaders)) {
496
- if (value.trim()) {
497
- headers[name] = value.trim();
425
+ const audience = signerJwtAudience(issuerUrl);
426
+ const body = new URLSearchParams({
427
+ grant_type: "client_credentials",
428
+ scope: SIGN_MINT_USER_TOKEN_SCOPE,
429
+ external_user_id: options.externalUserId,
430
+ audience
431
+ });
432
+ const response = await fetchImpl(tokenEndpoint, {
433
+ method: "POST",
434
+ headers: {
435
+ Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
436
+ "Content-Type": "application/x-www-form-urlencoded",
437
+ Accept: "application/json"
438
+ },
439
+ body: body.toString(),
440
+ cache: "no-store"
441
+ });
442
+ const parsed = await readJsonObjectFromResponse(response, {
443
+ invalidJsonMessage: "Token endpoint returned invalid JSON",
444
+ invalidJsonCode: TOKEN_RESPONSE_ERROR,
445
+ failureLabel: "Token mint failed",
446
+ defaultErrorCode: "token_mint_failed"
447
+ });
448
+ return parseMintUserSignerTokenResponse(parsed);
449
+ }
450
+
451
+ // src/signer/device-exchange.ts
452
+ var TOKEN_EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";
453
+ var SUBJECT_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
454
+ var EXCHANGE_RESPONSE_ERROR = "invalid_exchange_response";
455
+ function extractSignerAccessTokenFromExchangeBody(body) {
456
+ const tokenObj = body.token;
457
+ if (tokenObj !== null && typeof tokenObj === "object" && !Array.isArray(tokenObj)) {
458
+ const nested = tokenObj;
459
+ for (const key of ["accessToken", "access_token"]) {
460
+ const value = nested[key];
461
+ if (typeof value === "string" && value.trim()) {
462
+ return value.trim();
498
463
  }
499
464
  }
500
465
  }
501
- try {
502
- const response = await fetchImpl(url, {
503
- method: options.method,
504
- headers,
505
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
506
- signal: controller.signal
507
- });
508
- return {
509
- response,
510
- requestUrl: url,
511
- authorizationHeader: headers.Authorization
512
- };
513
- } finally {
514
- clearTimeout(timeout);
466
+ for (const key of ["accessToken", "access_token"]) {
467
+ const value = body[key];
468
+ if (typeof value === "string" && value.trim()) {
469
+ return value.trim();
470
+ }
515
471
  }
472
+ throw new PmtHouseError("Device exchange response missing signer access token", {
473
+ status: 502,
474
+ code: "invalid_exchange_response"
475
+ });
516
476
  }
517
- function createSignerProbeContext(options) {
518
- return {
519
- fetchImpl: options.fetch ?? fetch,
520
- signerUrl: normalizeSignerBaseUrl(options.signerUrl),
521
- timeoutMs: options.timeoutMs ?? 5e3,
522
- probeSubject: options.probeSubject ?? DEFAULT_PROBE_SUBJECT,
523
- useJwt: options.forwardJwt ?? true,
524
- getDmzToken: options.getDmzToken
477
+ function normalizeDeviceExchangeResponse(minted, options) {
478
+ const scope = minted.scope.trim() || "sign:job";
479
+ const body = {
480
+ access_token: minted.access_token,
481
+ token_type: "Bearer",
482
+ expires_in: minted.expires_in,
483
+ scope,
484
+ balanceUsdMicros: minted.balanceUsdMicros,
485
+ lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,
486
+ token: {
487
+ accessToken: minted.access_token,
488
+ access_token: minted.access_token,
489
+ expiresIn: minted.expires_in,
490
+ expires_in: minted.expires_in,
491
+ scope,
492
+ balanceUsdMicros: minted.balanceUsdMicros,
493
+ lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros
494
+ }
525
495
  };
526
- }
527
- function reachableResult(ethAddress) {
528
- return { reachable: true, ethAddress };
529
- }
530
- async function fetchSignerStatus(ctx, headers) {
531
- const response = await ctx.fetchImpl(`${ctx.signerUrl}/status`, {
532
- headers,
533
- signal: AbortSignal.timeout(ctx.timeoutMs)
534
- });
535
- if (!response.ok) {
536
- return { ok: false };
496
+ const signerUrl = options?.signerUrl?.trim();
497
+ if (signerUrl) {
498
+ body.signerUrl = signerUrl;
537
499
  }
538
- const data = await readSignerUpstreamBody(response);
539
- const ethAddress = typeof data.Address === "string" && data.Address || typeof data.address === "string" && data.address || void 0;
540
- return { ok: true, ethAddress };
500
+ return body;
541
501
  }
542
- async function tryJwtStatus(ctx) {
543
- if (!ctx.useJwt) {
544
- return null;
545
- }
502
+ async function parseDeviceExchangeRequestBody(request) {
503
+ let body;
546
504
  try {
547
- const token = await ctx.getDmzToken(ctx.probeSubject, "http");
548
- const { ok, ethAddress } = await fetchSignerStatus(ctx, {
549
- Authorization: `Bearer ${token}`
550
- });
551
- return ok ? reachableResult(ethAddress) : null;
505
+ body = await request.json();
552
506
  } catch {
553
- return null;
507
+ throw new PmtHouseError("Request body must be JSON", {
508
+ status: 400,
509
+ code: "invalid_request"
510
+ });
554
511
  }
555
- }
556
- async function tryPlainStatus(ctx) {
557
- try {
558
- const { ok, ethAddress } = await fetchSignerStatus(ctx, {});
559
- return ok ? reachableResult(ethAddress) : null;
560
- } catch {
561
- return null;
512
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
513
+ throw new PmtHouseError("Request body must be a JSON object", {
514
+ status: 400,
515
+ code: "invalid_request"
516
+ });
562
517
  }
563
- }
564
- async function trySigningProbe(ctx) {
565
- try {
566
- const token = await ctx.getDmzToken(ctx.probeSubject, "http");
567
- const response = await ctx.fetchImpl(`${ctx.signerUrl}/sign-orchestrator-info`, {
568
- method: "POST",
569
- headers: {
570
- Authorization: `Bearer ${token}`,
571
- "Content-Type": "application/json"
572
- },
573
- body: "{}",
574
- signal: AbortSignal.timeout(ctx.timeoutMs)
518
+ const record = body;
519
+ const deviceTokenRaw = record.deviceToken;
520
+ if (typeof deviceTokenRaw !== "string" || !deviceTokenRaw.trim()) {
521
+ throw new PmtHouseError("Request body must include deviceToken", {
522
+ status: 400,
523
+ code: "invalid_request"
575
524
  });
576
- return response.ok;
577
- } catch {
578
- return false;
579
525
  }
526
+ const deviceToken = deviceTokenRaw.trim();
527
+ const scope = typeof record.scope === "string" && record.scope.trim() ? record.scope.trim() : void 0;
528
+ const clientId = typeof record.clientId === "string" && record.clientId.trim() ? record.clientId.trim() : void 0;
529
+ return { deviceToken, scope, clientId };
580
530
  }
581
- async function runSignerReachabilityProbes(ctx) {
582
- const jwtResult = await tryJwtStatus(ctx);
583
- if (jwtResult) {
584
- return jwtResult;
531
+ async function mintSignerTokenFromDeviceToken(options) {
532
+ const fetchImpl = options.fetch ?? fetch;
533
+ const issuerUrl = stripTrailingSlashes(options.issuerUrl);
534
+ const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
535
+ allowInsecureHttp: options.allowInsecureHttp
536
+ });
537
+ const tokenEndpoint = as.token_endpoint;
538
+ if (!tokenEndpoint) {
539
+ throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
540
+ status: 500,
541
+ code: "oidc_discovery_invalid"
542
+ });
585
543
  }
586
- const plainResult = await tryPlainStatus(ctx);
587
- if (plainResult) {
588
- return plainResult;
589
- }
590
- if (await trySigningProbe(ctx)) {
591
- return reachableResult();
592
- }
593
- return { reachable: false };
594
- }
595
- async function probeSignerHttpReachability(options) {
596
- const ctx = createSignerProbeContext(options);
597
- try {
598
- const health = await ctx.fetchImpl(`${ctx.signerUrl}/healthz`, {
599
- signal: AbortSignal.timeout(ctx.timeoutMs)
600
- });
601
- if (health.ok) {
602
- return runSignerReachabilityProbes(ctx);
603
- }
604
- } catch {
605
- }
606
- return runSignerReachabilityProbes(ctx);
607
- }
608
-
609
- // src/signer/types.ts
610
- function resolvesToHostedMetering(mode) {
611
- return mode === "pymthouse_hosted";
612
- }
613
-
614
- // src/signer/metering.ts
615
- async function forwardWithOptionalMetering(input) {
616
- const upstream = await input.forward();
617
- const mode = input.config.metering?.mode;
618
- if (!resolvesToHostedMetering(mode)) {
619
- return upstream;
620
- }
621
- const body = await readSignerUpstreamBody(upstream);
622
- const snapshot = upstream.ok && body !== null && typeof body === "object" ? parseSignerUsageSnapshot(body) : null;
623
- if (snapshot) {
624
- stripSignerUsageFromResponse(body);
625
- }
626
- if (upstream.ok && snapshot && snapshot.computedFeeUsdMicros > 0n) {
627
- try {
628
- await ingestSignedTicket({
629
- issuerUrl: input.config.pymthouseIssuerUrl,
630
- publicClientId: input.config.pymthouseClientId,
631
- m2mClientId: input.config.pymthouseM2MClientId,
632
- m2mClientSecret: input.config.pymthouseM2MClientSecret,
633
- ticket: signerSnapshotToIngestPayload({
634
- snapshot,
635
- externalUserId: input.externalUserId
636
- }),
637
- fetch: input.config.fetch
638
- });
639
- } catch (err) {
640
- console.warn("[builder-sdk] signed-ticket ingest failed:", err);
641
- }
642
- }
643
- const headers = new Headers(upstream.headers);
644
- if (!headers.has("content-type")) {
645
- headers.set("Content-Type", "application/json");
646
- }
647
- return new Response(JSON.stringify(body), {
648
- status: upstream.status,
649
- statusText: upstream.statusText,
650
- headers
544
+ const audience = options.audience?.trim() || signerJwtAudience(issuerUrl);
545
+ const params = new URLSearchParams({
546
+ grant_type: TOKEN_EXCHANGE_GRANT,
547
+ subject_token: options.deviceToken,
548
+ subject_token_type: SUBJECT_ACCESS_TOKEN_TYPE,
549
+ audience,
550
+ resource: audience
651
551
  });
652
- }
653
- var CACHE_TTL_MS = 5 * 60 * 1e3;
654
- var discoveryCache = /* @__PURE__ */ new Map();
655
- function normalizedIssuerKey(issuerUrl) {
656
- return stripTrailingSlashes(issuerUrl);
657
- }
658
- async function loadAuthorizationServer(issuerUrl, fetchImpl, options = {}) {
659
- const key = normalizedIssuerKey(issuerUrl);
660
- const now = Date.now();
661
- const cached = discoveryCache.get(key);
662
- if (!options.force && cached && now - cached.fetchedAt < CACHE_TTL_MS) {
663
- return cached.as;
552
+ if (options.scope?.trim()) {
553
+ params.set("scope", options.scope.trim());
664
554
  }
665
- const issuerIdentifier = new URL(key);
666
- const discoveryOpts = {
667
- algorithm: "oidc",
668
- [customFetch]: fetchImpl
555
+ const response = await fetchImpl(tokenEndpoint, {
556
+ method: "POST",
557
+ headers: {
558
+ Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
559
+ "Content-Type": "application/x-www-form-urlencoded",
560
+ Accept: "application/json"
561
+ },
562
+ body: params.toString(),
563
+ cache: "no-store"
564
+ });
565
+ const parsed = await readJsonObjectFromResponse(response, {
566
+ invalidJsonMessage: "Token endpoint returned invalid JSON",
567
+ invalidJsonCode: "invalid_token_response",
568
+ failureLabel: "Signer JWT exchange failed",
569
+ defaultErrorCode: "token_exchange_failed"
570
+ });
571
+ const cached = parseMintUserSignerTokenResponse(parsed);
572
+ return {
573
+ access_token: cached.jwt,
574
+ expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),
575
+ scope: readStringField(parsed, "scope", EXCHANGE_RESPONSE_ERROR),
576
+ balanceUsdMicros: cached.balanceUsdMicros,
577
+ lifetimeGrantedUsdMicros: cached.lifetimeGrantedUsdMicros
669
578
  };
670
- if (options.allowInsecureHttp) {
671
- discoveryOpts[allowInsecureRequests] = true;
672
- }
673
- let response;
674
- try {
675
- response = await discoveryRequest(issuerIdentifier, discoveryOpts);
676
- } catch (e) {
677
- throw mapDiscoveryNetworkError(e);
678
- }
679
- let as;
680
- try {
681
- as = await processDiscoveryResponse(issuerIdentifier, response);
682
- } catch (e) {
683
- throw mapOAuthDiscoveryError(e);
684
- }
685
- discoveryCache.set(key, { as, fetchedAt: now });
686
- return as;
687
579
  }
688
- function mapOAuthDiscoveryError(error) {
689
- if (error instanceof PmtHouseError) {
690
- return error;
580
+ async function exchangeDeviceTokenForSigner(options) {
581
+ const fetchImpl = options.fetch ?? fetch;
582
+ const url = `${stripTrailingSlashes(options.facadeUrl)}/api/signer/device/exchange`;
583
+ const body = { deviceToken: options.deviceToken };
584
+ if (options.scope?.trim()) {
585
+ body.scope = options.scope.trim();
691
586
  }
692
- if (error instanceof Error) {
693
- return new PmtHouseError(error.message, {
694
- status: 500,
695
- code: "oidc_discovery_invalid",
696
- details: { cause: error.cause }
697
- });
587
+ if (options.clientId?.trim()) {
588
+ body.clientId = options.clientId.trim();
698
589
  }
699
- return new PmtHouseError("OIDC discovery failed", {
700
- status: 500,
701
- code: "oidc_discovery_invalid"
590
+ const response = await fetchImpl(url, {
591
+ method: "POST",
592
+ headers: {
593
+ "Content-Type": "application/json",
594
+ Accept: "application/json"
595
+ },
596
+ body: JSON.stringify(body),
597
+ cache: "no-store"
598
+ });
599
+ const parsed = await readJsonObjectFromResponse(response, {
600
+ invalidJsonMessage: "Device exchange returned invalid JSON",
601
+ invalidJsonCode: EXCHANGE_RESPONSE_ERROR,
602
+ failureLabel: "Device exchange failed",
603
+ defaultErrorCode: "device_exchange_failed"
702
604
  });
605
+ const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
606
+ const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
607
+ const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
608
+ return normalizeDeviceExchangeResponse(
609
+ {
610
+ access_token: accessToken,
611
+ expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),
612
+ scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
613
+ balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
614
+ lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
615
+ },
616
+ { signerUrl }
617
+ );
703
618
  }
704
- function mapDiscoveryNetworkError(error) {
705
- if (error instanceof PmtHouseError) {
706
- return error;
707
- }
708
- if (error instanceof Error) {
709
- return new PmtHouseError(`Failed to load OIDC discovery: ${error.message}`, {
710
- status: 502,
711
- code: "oidc_discovery_failed"
712
- });
619
+ function resolveMint(config) {
620
+ if ("mint" in config && typeof config.mint === "function") {
621
+ return config.mint;
713
622
  }
714
- return new PmtHouseError("Failed to load OIDC discovery", {
715
- status: 502,
716
- code: "oidc_discovery_failed"
623
+ return (deviceToken, context) => mintSignerTokenFromDeviceToken({
624
+ issuerUrl: config.issuerUrl,
625
+ m2mClientId: config.m2mClientId,
626
+ m2mClientSecret: config.m2mClientSecret,
627
+ deviceToken,
628
+ scope: context.scope,
629
+ audience: config.audience,
630
+ fetch: config.fetch,
631
+ allowInsecureHttp: config.allowInsecureHttp
717
632
  });
718
633
  }
719
-
720
- // src/signer/fetch-json.ts
721
- function oauthFailureDescription(parsed, failureLabel, status) {
722
- if (typeof parsed.error_description === "string") {
723
- return parsed.error_description;
634
+ function resolveSignerUrlFromConfig(config) {
635
+ if ("signerUrl" in config && typeof config.signerUrl === "string" && config.signerUrl.trim()) {
636
+ return config.signerUrl.trim();
724
637
  }
725
- if (typeof parsed.error === "string") {
726
- return parsed.error;
638
+ if ("getSignerUrl" in config && typeof config.getSignerUrl === "function") {
639
+ return config.getSignerUrl();
727
640
  }
728
- return `${failureLabel} (${status})`;
641
+ return void 0;
729
642
  }
730
- async function readJsonObjectFromResponse(response, options) {
731
- const text = await response.text();
732
- let parsed;
643
+ function createDeviceExchangeHandler(config) {
644
+ const mint = resolveMint(config);
645
+ return async function deviceExchangeHandler(request) {
646
+ try {
647
+ if (request.method !== "POST") {
648
+ return new Response(JSON.stringify({ error: "method_not_allowed" }), {
649
+ status: 405,
650
+ headers: { "Content-Type": "application/json" }
651
+ });
652
+ }
653
+ const parsed = await parseDeviceExchangeRequestBody(request);
654
+ const minted = await mint(parsed.deviceToken, {
655
+ scope: parsed.scope,
656
+ clientId: parsed.clientId
657
+ });
658
+ const signerUrlValue = await resolveSignerUrlFromConfig(config);
659
+ const body = normalizeDeviceExchangeResponse(minted, {
660
+ signerUrl: typeof signerUrlValue === "string" ? signerUrlValue : void 0
661
+ });
662
+ return new Response(JSON.stringify(body), {
663
+ status: 200,
664
+ headers: {
665
+ "Content-Type": "application/json",
666
+ "Cache-Control": "no-store"
667
+ }
668
+ });
669
+ } catch (error) {
670
+ return signerHandlerErrorResponse(error);
671
+ }
672
+ };
673
+ }
674
+
675
+ // src/signer/api-key-exchange.ts
676
+ var EXCHANGE_RESPONSE_ERROR2 = "invalid_exchange_response";
677
+ async function parseApiKeyExchangeRequestBody(request) {
678
+ let body;
733
679
  try {
734
- parsed = text ? JSON.parse(text) : {};
680
+ body = await request.json();
735
681
  } catch {
736
- throw new PmtHouseError(options.invalidJsonMessage, {
737
- status: 502,
738
- code: options.invalidJsonCode,
739
- details: { status: response.status }
682
+ throw new PmtHouseError("Request body must be JSON", {
683
+ status: 400,
684
+ code: "invalid_request"
740
685
  });
741
686
  }
742
- if (!response.ok) {
743
- const description = oauthFailureDescription(parsed, options.failureLabel, response.status);
744
- throw new PmtHouseError(description, {
745
- status: response.status,
746
- code: typeof parsed.error === "string" ? parsed.error : options.defaultErrorCode,
747
- details: parsed
687
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
688
+ throw new PmtHouseError("Request body must be a JSON object", {
689
+ status: 400,
690
+ code: "invalid_request"
748
691
  });
749
692
  }
750
- return parsed;
751
- }
752
-
753
- // src/signer/json-fields.ts
754
- function readStringField(body, key, errorCode, messagePrefix = "Response") {
755
- const value = body[key];
756
- if (typeof value !== "string" || !value.trim()) {
757
- throw new PmtHouseError(`${messagePrefix} missing ${key}`, {
758
- status: 502,
759
- code: errorCode
693
+ const record = body;
694
+ const apiKeyRaw = record.apiKey;
695
+ if (typeof apiKeyRaw !== "string" || !apiKeyRaw.trim()) {
696
+ throw new PmtHouseError("Request body must include apiKey", {
697
+ status: 400,
698
+ code: "invalid_request"
760
699
  });
761
700
  }
762
- return value.trim();
701
+ const scope = typeof record.scope === "string" && record.scope.trim() ? record.scope.trim() : void 0;
702
+ const clientId = typeof record.clientId === "string" && record.clientId.trim() ? record.clientId.trim() : void 0;
703
+ return { apiKey: apiKeyRaw.trim(), scope, clientId };
763
704
  }
764
- function readExpiresIn(body, errorCode) {
765
- const expiresIn = body.expires_in;
766
- if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
767
- throw new PmtHouseError("Response missing expires_in", {
705
+ async function mintUserAccessTokenFromApiKey(input) {
706
+ const fetchImpl = input.fetch ?? fetch;
707
+ const issuerOrigin = stripIssuerOriginFromOidcUrl(input.issuerUrl);
708
+ const url = `${issuerOrigin}/api/v1/apps/${encodeURIComponent(input.publicClientId)}/auth/api-key/token`;
709
+ const response = await fetchImpl(url, {
710
+ method: "POST",
711
+ headers: {
712
+ Authorization: `Bearer ${input.apiKey}`,
713
+ "Content-Type": "application/json",
714
+ Accept: "application/json"
715
+ },
716
+ body: JSON.stringify(input.scope ? { scope: input.scope } : {}),
717
+ cache: "no-store"
718
+ });
719
+ const parsed = await readJsonObjectFromResponse(response, {
720
+ invalidJsonMessage: "API key token exchange returned invalid JSON",
721
+ invalidJsonCode: "invalid_token_response",
722
+ failureLabel: "API key token exchange failed",
723
+ defaultErrorCode: "api_key_token_exchange_failed"
724
+ });
725
+ const accessToken = parsed.access_token;
726
+ if (typeof accessToken !== "string" || !accessToken.trim()) {
727
+ throw new PmtHouseError("API key token exchange missing access_token", {
768
728
  status: 502,
769
- code: errorCode
729
+ code: EXCHANGE_RESPONSE_ERROR2
770
730
  });
771
731
  }
772
- return Math.floor(expiresIn);
773
- }
774
-
775
- // src/signer/mint-token.ts
776
- var SIGN_MINT_USER_TOKEN_SCOPE = "sign:mint_user_token";
777
- var LIVEPEER_REMOTE_SIGNER_AUDIENCE = "livepeer-remote-signer";
778
- var DEFAULT_TTL_REFRESH_RATIO = 0.8;
779
- var TOKEN_RESPONSE_ERROR = "invalid_token_response";
780
- function parseMintUserSignerTokenResponse(body, ttlRefreshRatio = DEFAULT_TTL_REFRESH_RATIO) {
781
- const accessToken = readStringField(body, "access_token", TOKEN_RESPONSE_ERROR, "Token response");
782
- const expiresIn = readExpiresIn(body, TOKEN_RESPONSE_ERROR);
783
- const balanceUsdMicros = readStringField(
784
- body,
785
- "balanceUsdMicros",
786
- TOKEN_RESPONSE_ERROR,
787
- "Token response"
788
- );
789
- const lifetimeGrantedUsdMicros = readStringField(
790
- body,
791
- "lifetimeGrantedUsdMicros",
792
- TOKEN_RESPONSE_ERROR,
793
- "Token response"
794
- );
795
- const now = Date.now();
796
- const expiresAt = now + expiresIn * 1e3;
797
- const refreshAt = now + Math.floor(expiresIn * 1e3 * ttlRefreshRatio);
732
+ const expiresIn = typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 900;
733
+ const scope = typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : input.scope?.trim() || "sign:job";
798
734
  return {
799
- jwt: accessToken,
800
- expiresAt,
801
- refreshAt,
802
- balanceUsdMicros,
803
- lifetimeGrantedUsdMicros
735
+ access_token: accessToken.trim(),
736
+ expires_in: expiresIn,
737
+ scope
804
738
  };
805
739
  }
806
- async function mintUserSignerToken(options) {
807
- const fetchImpl = options.fetch ?? fetch;
808
- const issuerUrl = stripTrailingSlashes(options.issuerUrl);
809
- const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
810
- allowInsecureHttp: options.allowInsecureHttp
740
+ async function mintSignerSessionFromApiKey(input) {
741
+ const userToken = await mintUserAccessTokenFromApiKey({
742
+ issuerUrl: input.issuerUrl,
743
+ publicClientId: input.publicClientId,
744
+ apiKey: input.apiKey,
745
+ scope: input.scope,
746
+ fetch: input.fetch
811
747
  });
812
- const tokenEndpoint = as.token_endpoint;
813
- if (!tokenEndpoint) {
814
- throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
815
- status: 500,
816
- code: "oidc_discovery_invalid"
817
- });
818
- }
819
- const body = new URLSearchParams({
820
- grant_type: "client_credentials",
821
- scope: SIGN_MINT_USER_TOKEN_SCOPE,
822
- external_user_id: options.externalUserId,
823
- audience: LIVEPEER_REMOTE_SIGNER_AUDIENCE
748
+ return mintSignerTokenFromDeviceToken({
749
+ issuerUrl: input.issuerUrl,
750
+ m2mClientId: input.m2mClientId,
751
+ m2mClientSecret: input.m2mClientSecret,
752
+ deviceToken: userToken.access_token,
753
+ scope: userToken.scope,
754
+ audience: input.audience,
755
+ fetch: input.fetch,
756
+ allowInsecureHttp: input.allowInsecureHttp
824
757
  });
825
- const response = await fetchImpl(tokenEndpoint, {
758
+ }
759
+ async function exchangeApiKeyForSigner(options) {
760
+ const fetchImpl = options.fetch ?? fetch;
761
+ const url = `${stripTrailingSlashes(options.facadeUrl)}/api/pymthouse/keys/exchange`;
762
+ const body = { apiKey: options.apiKey };
763
+ if (options.scope?.trim()) {
764
+ body.scope = options.scope.trim();
765
+ }
766
+ if (options.clientId?.trim()) {
767
+ body.clientId = options.clientId.trim();
768
+ }
769
+ const response = await fetchImpl(url, {
826
770
  method: "POST",
827
771
  headers: {
828
- Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
829
- "Content-Type": "application/x-www-form-urlencoded",
772
+ "Content-Type": "application/json",
830
773
  Accept: "application/json"
831
774
  },
832
- body: body.toString(),
775
+ body: JSON.stringify(body),
833
776
  cache: "no-store"
834
777
  });
835
778
  const parsed = await readJsonObjectFromResponse(response, {
836
- invalidJsonMessage: "Token endpoint returned invalid JSON",
837
- invalidJsonCode: TOKEN_RESPONSE_ERROR,
838
- failureLabel: "Token mint failed",
839
- defaultErrorCode: "token_mint_failed"
779
+ invalidJsonMessage: "API key exchange returned invalid JSON",
780
+ invalidJsonCode: EXCHANGE_RESPONSE_ERROR2,
781
+ failureLabel: "API key exchange failed",
782
+ defaultErrorCode: "api_key_exchange_failed"
840
783
  });
841
- return parseMintUserSignerTokenResponse(parsed);
784
+ const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
785
+ const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
786
+ const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
787
+ return normalizeDeviceExchangeResponse(
788
+ {
789
+ access_token: accessToken,
790
+ expires_in: typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 3600,
791
+ scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
792
+ balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
793
+ lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
794
+ },
795
+ { signerUrl }
796
+ );
842
797
  }
843
-
844
- // src/signer/device-exchange.ts
845
- var TOKEN_EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";
846
- var SUBJECT_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
847
- var EXCHANGE_RESPONSE_ERROR = "invalid_exchange_response";
848
- function extractSignerAccessTokenFromExchangeBody(body) {
849
- const tokenObj = body.token;
850
- if (tokenObj !== null && typeof tokenObj === "object" && !Array.isArray(tokenObj)) {
851
- const nested = tokenObj;
852
- for (const key of ["accessToken", "access_token"]) {
853
- const value = nested[key];
854
- if (typeof value === "string" && value.trim()) {
855
- return value.trim();
798
+ function createApiKeyExchangeHandler(config) {
799
+ const publicClientId = config.publicClientId.trim();
800
+ return async function apiKeyExchangeHandler(request) {
801
+ try {
802
+ if (request.method !== "POST") {
803
+ return new Response(JSON.stringify({ error: "method_not_allowed" }), {
804
+ status: 405,
805
+ headers: { "Content-Type": "application/json" }
806
+ });
856
807
  }
808
+ const parsed = await parseApiKeyExchangeRequestBody(request);
809
+ const effectiveClientId = parsed.clientId?.trim() || publicClientId;
810
+ if (effectiveClientId !== publicClientId) {
811
+ throw new PmtHouseError("clientId does not match configured public client", {
812
+ status: 400,
813
+ code: "invalid_request"
814
+ });
815
+ }
816
+ const minted = await mintSignerSessionFromApiKey({
817
+ issuerUrl: config.issuerUrl,
818
+ publicClientId,
819
+ m2mClientId: config.m2mClientId,
820
+ m2mClientSecret: config.m2mClientSecret,
821
+ apiKey: parsed.apiKey,
822
+ scope: parsed.scope,
823
+ audience: config.audience,
824
+ fetch: config.fetch,
825
+ allowInsecureHttp: config.allowInsecureHttp
826
+ });
827
+ const signerUrlValue = typeof config.signerUrl === "string" && config.signerUrl.trim() ? config.signerUrl.trim() : void 0;
828
+ const body = normalizeDeviceExchangeResponse(minted, { signerUrl: signerUrlValue });
829
+ return new Response(JSON.stringify(body), {
830
+ status: 200,
831
+ headers: {
832
+ "Content-Type": "application/json",
833
+ "Cache-Control": "no-store"
834
+ }
835
+ });
836
+ } catch (error) {
837
+ return signerHandlerErrorResponse(error);
838
+ }
839
+ };
840
+ }
841
+
842
+ // src/signer/proxy.ts
843
+ var HTTP_DMZ_TOKEN_MAX_ENTRIES = 100;
844
+ var HTTP_DMZ_TOKEN_TTL_MS = 3.5 * 60 * 1e3;
845
+ var DEFAULT_PROBE_SUBJECT = "signer-reachability-probe";
846
+ var httpDmzTokenCache = /* @__PURE__ */ new Map();
847
+ function normalizeSignerBaseUrl(base) {
848
+ return stripTrailingSlashes(base);
849
+ }
850
+ function joinSignerUrl(baseUrl, path) {
851
+ if (path.startsWith("/")) {
852
+ return `${baseUrl}${path}`;
853
+ }
854
+ return `${baseUrl}/${path}`;
855
+ }
856
+ function aliasBodyString(raw) {
857
+ if (raw === void 0 || raw === null) {
858
+ return null;
859
+ }
860
+ if (typeof raw === "string") {
861
+ return raw.length > 0 ? raw : null;
862
+ }
863
+ if (typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint") {
864
+ return String(raw);
865
+ }
866
+ return null;
867
+ }
868
+ function resolveSignerBaseUrl(input) {
869
+ if (input.testSignerUrl?.trim()) {
870
+ return normalizeSignerBaseUrl(input.testSignerUrl);
871
+ }
872
+ const legacyBareSignerPort = 8081;
873
+ const rawPort = input.storedPort ?? input.defaultPort ?? 8080;
874
+ const port = rawPort === legacyBareSignerPort ? 8080 : rawPort;
875
+ const base = input.envUrl?.trim() || input.storedUrl?.trim() || `http://127.0.0.1:${port}`;
876
+ return normalizeSignerBaseUrl(base);
877
+ }
878
+ async function getCachedDmzBearerToken(subject, gate, getDmzToken) {
879
+ const cacheKey2 = `${gate}:${subject}`;
880
+ const now = Date.now();
881
+ const cached = httpDmzTokenCache.get(cacheKey2);
882
+ if (cached && cached.expMs > now + 15e3) {
883
+ httpDmzTokenCache.delete(cacheKey2);
884
+ httpDmzTokenCache.set(cacheKey2, cached);
885
+ return cached.token;
886
+ }
887
+ const token = await getDmzToken(subject, gate);
888
+ httpDmzTokenCache.set(cacheKey2, { token, expMs: now + HTTP_DMZ_TOKEN_TTL_MS });
889
+ if (httpDmzTokenCache.size > HTTP_DMZ_TOKEN_MAX_ENTRIES) {
890
+ const oldest = httpDmzTokenCache.keys().next().value;
891
+ if (oldest !== void 0) {
892
+ httpDmzTokenCache.delete(oldest);
893
+ }
894
+ }
895
+ return token;
896
+ }
897
+ async function readSignerUpstreamBody(response) {
898
+ const text = await response.text();
899
+ if (!text.trim()) {
900
+ return {};
901
+ }
902
+ try {
903
+ return JSON.parse(text);
904
+ } catch {
905
+ return {
906
+ error: "Signer DMZ returned a non-JSON body (often Apache auth failure)",
907
+ upstreamStatus: response.status,
908
+ detail: text.slice(0, 800)
909
+ };
910
+ }
911
+ }
912
+ function pickConflictingStringAliases(body, ...keys) {
913
+ const values = keys.map((key) => {
914
+ const value = aliasBodyString(body[key]);
915
+ return value === null ? null : { key, value };
916
+ }).filter((entry) => entry !== null);
917
+ const first = values[0];
918
+ const conflict = values.find((entry) => entry.value !== first?.value);
919
+ if (first && conflict) {
920
+ return {
921
+ ok: false,
922
+ message: `Conflicting ${keys.join("/")} in request body`
923
+ };
924
+ }
925
+ return { ok: true, value: first?.value };
926
+ }
927
+ function pickConflictingNumberAliases(body, ...keys) {
928
+ const parseNum = (value) => {
929
+ if (typeof value === "number" && Number.isFinite(value)) {
930
+ return value;
931
+ }
932
+ if (typeof value === "string" && value.trim() !== "") {
933
+ const parsed = Number(value);
934
+ return Number.isFinite(parsed) ? parsed : void 0;
857
935
  }
936
+ return void 0;
937
+ };
938
+ const values = keys.map((key) => {
939
+ const value = parseNum(body[key]);
940
+ return value === void 0 ? null : { key, value };
941
+ }).filter((entry) => entry !== null);
942
+ const first = values[0];
943
+ const conflict = values.find((entry) => entry.value !== first?.value);
944
+ if (first && conflict) {
945
+ return {
946
+ ok: false,
947
+ message: `Conflicting ${keys.join("/")} in request body`
948
+ };
858
949
  }
859
- for (const key of ["accessToken", "access_token"]) {
860
- const value = body[key];
950
+ return { ok: true, value: first?.value };
951
+ }
952
+ function pickString(obj, ...keys) {
953
+ for (const key of keys) {
954
+ const value = obj[key];
861
955
  if (typeof value === "string" && value.trim()) {
862
956
  return value.trim();
863
957
  }
864
- }
865
- throw new PmtHouseError("Device exchange response missing signer access token", {
866
- status: 502,
867
- code: "invalid_exchange_response"
868
- });
869
- }
870
- function normalizeDeviceExchangeResponse(minted, options) {
871
- const scope = minted.scope.trim() || "sign:job";
872
- const body = {
873
- access_token: minted.access_token,
874
- token_type: "Bearer",
875
- expires_in: minted.expires_in,
876
- scope,
877
- balanceUsdMicros: minted.balanceUsdMicros,
878
- lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,
879
- token: {
880
- accessToken: minted.access_token,
881
- access_token: minted.access_token,
882
- expiresIn: minted.expires_in,
883
- expires_in: minted.expires_in,
884
- scope,
885
- balanceUsdMicros: minted.balanceUsdMicros,
886
- lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros
958
+ if (typeof value === "number" && Number.isFinite(value)) {
959
+ return String(Math.trunc(value));
887
960
  }
888
- };
889
- const signerUrl = options?.signerUrl?.trim();
890
- if (signerUrl) {
891
- body.signerUrl = signerUrl;
892
961
  }
893
- return body;
962
+ return "";
894
963
  }
895
- async function parseDeviceExchangeRequestBody(request) {
896
- let body;
897
- try {
898
- body = await request.json();
899
- } catch {
900
- throw new PmtHouseError("Request body must be JSON", {
901
- status: 400,
902
- code: "invalid_request"
903
- });
904
- }
964
+ function parseSignerUsageSnapshot(body) {
905
965
  if (body === null || typeof body !== "object" || Array.isArray(body)) {
906
- throw new PmtHouseError("Request body must be a JSON object", {
907
- status: 400,
908
- code: "invalid_request"
909
- });
966
+ return null;
910
967
  }
911
968
  const record = body;
912
- const deviceTokenRaw = record.deviceToken;
913
- if (typeof deviceTokenRaw !== "string" || !deviceTokenRaw.trim()) {
914
- throw new PmtHouseError("Request body must include deviceToken", {
915
- status: 400,
916
- code: "invalid_request"
917
- });
969
+ const usageRaw = record.usage;
970
+ if (usageRaw === null || typeof usageRaw !== "object" || Array.isArray(usageRaw)) {
971
+ return null;
918
972
  }
919
- const deviceToken = deviceTokenRaw.trim();
920
- const scope = typeof record.scope === "string" && record.scope.trim() ? record.scope.trim() : void 0;
921
- const clientId = typeof record.clientId === "string" && record.clientId.trim() ? record.clientId.trim() : void 0;
922
- return { deviceToken, scope, clientId };
923
- }
924
- async function mintSignerTokenFromDeviceToken(options) {
925
- const fetchImpl = options.fetch ?? fetch;
926
- const issuerUrl = stripTrailingSlashes(options.issuerUrl);
927
- const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
928
- allowInsecureHttp: options.allowInsecureHttp
929
- });
930
- const tokenEndpoint = as.token_endpoint;
931
- if (!tokenEndpoint) {
932
- throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
933
- status: 500,
934
- code: "oidc_discovery_invalid"
935
- });
973
+ const usage = usageRaw;
974
+ const computedFeeWei = pickString(usage, "computed_fee_wei", "computedFeeWei");
975
+ const usdMicrosStr = pickString(usage, "computed_fee_usd_micros", "computedFeeUsdMicros");
976
+ const requestId = pickString(usage, "request_id", "requestId");
977
+ if (!computedFeeWei || !usdMicrosStr || !requestId) {
978
+ return null;
936
979
  }
937
- const audience = options.audience?.trim() || LIVEPEER_REMOTE_SIGNER_AUDIENCE;
938
- const params = new URLSearchParams({
939
- grant_type: TOKEN_EXCHANGE_GRANT,
940
- subject_token: options.deviceToken,
941
- subject_token_type: SUBJECT_ACCESS_TOKEN_TYPE,
942
- audience,
943
- resource: audience
944
- });
945
- if (options.scope?.trim()) {
946
- params.set("scope", options.scope.trim());
980
+ let computedFeeUsdMicros;
981
+ try {
982
+ computedFeeUsdMicros = BigInt(usdMicrosStr);
983
+ } catch {
984
+ return null;
947
985
  }
948
- const response = await fetchImpl(tokenEndpoint, {
949
- method: "POST",
950
- headers: {
951
- Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
952
- "Content-Type": "application/x-www-form-urlencoded",
953
- Accept: "application/json"
954
- },
955
- body: params.toString(),
956
- cache: "no-store"
957
- });
958
- const parsed = await readJsonObjectFromResponse(response, {
959
- invalidJsonMessage: "Token endpoint returned invalid JSON",
960
- invalidJsonCode: "invalid_token_response",
961
- failureLabel: "Signer JWT exchange failed",
962
- defaultErrorCode: "token_exchange_failed"
963
- });
964
- const cached = parseMintUserSignerTokenResponse(parsed);
965
986
  return {
966
- access_token: cached.jwt,
967
- expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),
968
- scope: readStringField(parsed, "scope", EXCHANGE_RESPONSE_ERROR),
969
- balanceUsdMicros: cached.balanceUsdMicros,
970
- lifetimeGrantedUsdMicros: cached.lifetimeGrantedUsdMicros
987
+ requestId,
988
+ computedFeeWei,
989
+ computedFeeUsdMicros,
990
+ ethUsdPrice: pickString(usage, "eth_usd_price", "ethUsdPrice") || void 0,
991
+ ethUsdRoundId: pickString(usage, "eth_usd_round_id", "ethUsdRoundId") || void 0,
992
+ ethUsdObservedAt: pickString(usage, "eth_usd_updated_at", "ethUsdUpdatedAt", "eth_usd_observed_at") || void 0,
993
+ pixels: pickString(usage, "pixels") || void 0,
994
+ billableSecs: pickString(usage, "billable_secs", "billableSecs") || void 0,
995
+ pipeline: pickString(usage, "pipeline") || void 0,
996
+ modelId: pickString(usage, "model_id", "modelId") || void 0
971
997
  };
972
998
  }
973
- async function exchangeDeviceTokenForSigner(options) {
999
+ function stripSignerUsageFromResponse(body) {
1000
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
1001
+ delete body.usage;
1002
+ }
1003
+ }
1004
+ async function forwardToSigner(options) {
974
1005
  const fetchImpl = options.fetch ?? fetch;
975
- const url = `${stripTrailingSlashes(options.facadeUrl)}/api/signer/device/exchange`;
976
- const body = { deviceToken: options.deviceToken };
977
- if (options.scope?.trim()) {
978
- body.scope = options.scope.trim();
1006
+ const baseUrl = normalizeSignerBaseUrl(options.baseUrl);
1007
+ const url = joinSignerUrl(baseUrl, options.path);
1008
+ const timeoutMs = options.timeoutMs ?? 3e4;
1009
+ const controller = new AbortController();
1010
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1011
+ const headers = { "Content-Type": "application/json" };
1012
+ const explicitAuth = options.authorization?.trim();
1013
+ if (explicitAuth) {
1014
+ headers.Authorization = explicitAuth.startsWith("Bearer ") ? explicitAuth : `Bearer ${explicitAuth}`;
1015
+ } else {
1016
+ const attachJwt = options.forwardJwt ?? true;
1017
+ if (attachJwt) {
1018
+ const token = await getCachedDmzBearerToken(
1019
+ options.subject,
1020
+ "http",
1021
+ options.getDmzToken
1022
+ );
1023
+ headers.Authorization = `Bearer ${token}`;
1024
+ }
979
1025
  }
980
- if (options.clientId?.trim()) {
981
- body.clientId = options.clientId.trim();
1026
+ if (options.extraHeaders) {
1027
+ for (const [name, value] of Object.entries(options.extraHeaders)) {
1028
+ if (value.trim()) {
1029
+ headers[name] = value.trim();
1030
+ }
1031
+ }
982
1032
  }
983
- const response = await fetchImpl(url, {
984
- method: "POST",
985
- headers: {
986
- "Content-Type": "application/json",
987
- Accept: "application/json"
988
- },
989
- body: JSON.stringify(body),
990
- cache: "no-store"
991
- });
992
- const parsed = await readJsonObjectFromResponse(response, {
993
- invalidJsonMessage: "Device exchange returned invalid JSON",
994
- invalidJsonCode: EXCHANGE_RESPONSE_ERROR,
995
- failureLabel: "Device exchange failed",
996
- defaultErrorCode: "device_exchange_failed"
997
- });
998
- const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
999
- const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
1000
- const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
1001
- return normalizeDeviceExchangeResponse(
1002
- {
1003
- access_token: accessToken,
1004
- expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),
1005
- scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
1006
- balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
1007
- lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
1008
- },
1009
- { signerUrl }
1010
- );
1011
- }
1012
- function resolveMint(config) {
1013
- if ("mint" in config && typeof config.mint === "function") {
1014
- return config.mint;
1033
+ try {
1034
+ const response = await fetchImpl(url, {
1035
+ method: options.method,
1036
+ headers,
1037
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1038
+ signal: controller.signal
1039
+ });
1040
+ return {
1041
+ response,
1042
+ requestUrl: url,
1043
+ authorizationHeader: headers.Authorization
1044
+ };
1045
+ } finally {
1046
+ clearTimeout(timeout);
1015
1047
  }
1016
- return (deviceToken, context) => mintSignerTokenFromDeviceToken({
1017
- issuerUrl: config.issuerUrl,
1018
- m2mClientId: config.m2mClientId,
1019
- m2mClientSecret: config.m2mClientSecret,
1020
- deviceToken,
1021
- scope: context.scope,
1022
- audience: config.audience,
1023
- fetch: config.fetch,
1024
- allowInsecureHttp: config.allowInsecureHttp
1048
+ }
1049
+ function createSignerProbeContext(options) {
1050
+ return {
1051
+ fetchImpl: options.fetch ?? fetch,
1052
+ signerUrl: normalizeSignerBaseUrl(options.signerUrl),
1053
+ timeoutMs: options.timeoutMs ?? 5e3,
1054
+ probeSubject: options.probeSubject ?? DEFAULT_PROBE_SUBJECT,
1055
+ useJwt: options.forwardJwt ?? true,
1056
+ getDmzToken: options.getDmzToken
1057
+ };
1058
+ }
1059
+ function reachableResult(ethAddress) {
1060
+ return { reachable: true, ethAddress };
1061
+ }
1062
+ async function fetchSignerStatus(ctx, headers) {
1063
+ const response = await ctx.fetchImpl(`${ctx.signerUrl}/status`, {
1064
+ headers,
1065
+ signal: AbortSignal.timeout(ctx.timeoutMs)
1025
1066
  });
1067
+ if (!response.ok) {
1068
+ return { ok: false };
1069
+ }
1070
+ const data = await readSignerUpstreamBody(response);
1071
+ const ethAddress = typeof data.Address === "string" && data.Address || typeof data.address === "string" && data.address || void 0;
1072
+ return { ok: true, ethAddress };
1026
1073
  }
1027
- function resolveSignerUrlFromConfig(config) {
1028
- if ("signerUrl" in config && typeof config.signerUrl === "string" && config.signerUrl.trim()) {
1029
- return config.signerUrl.trim();
1074
+ async function tryJwtStatus(ctx) {
1075
+ if (!ctx.useJwt) {
1076
+ return null;
1030
1077
  }
1031
- if ("getSignerUrl" in config && typeof config.getSignerUrl === "function") {
1032
- return config.getSignerUrl();
1078
+ try {
1079
+ const token = await ctx.getDmzToken(ctx.probeSubject, "http");
1080
+ const { ok, ethAddress } = await fetchSignerStatus(ctx, {
1081
+ Authorization: `Bearer ${token}`
1082
+ });
1083
+ return ok ? reachableResult(ethAddress) : null;
1084
+ } catch {
1085
+ return null;
1033
1086
  }
1034
- return void 0;
1035
- }
1036
- function createDeviceExchangeHandler(config) {
1037
- const mint = resolveMint(config);
1038
- return async function deviceExchangeHandler(request) {
1039
- try {
1040
- if (request.method !== "POST") {
1041
- return new Response(JSON.stringify({ error: "method_not_allowed" }), {
1042
- status: 405,
1043
- headers: { "Content-Type": "application/json" }
1044
- });
1045
- }
1046
- const parsed = await parseDeviceExchangeRequestBody(request);
1047
- const minted = await mint(parsed.deviceToken, {
1048
- scope: parsed.scope,
1049
- clientId: parsed.clientId
1050
- });
1051
- const signerUrlValue = await resolveSignerUrlFromConfig(config);
1052
- const body = normalizeDeviceExchangeResponse(minted, {
1053
- signerUrl: typeof signerUrlValue === "string" ? signerUrlValue : void 0
1054
- });
1055
- return new Response(JSON.stringify(body), {
1056
- status: 200,
1057
- headers: {
1058
- "Content-Type": "application/json",
1059
- "Cache-Control": "no-store"
1060
- }
1061
- });
1062
- } catch (error) {
1063
- return signerHandlerErrorResponse(error);
1064
- }
1065
- };
1066
1087
  }
1067
-
1068
- // src/signer/api-key-exchange.ts
1069
- var EXCHANGE_RESPONSE_ERROR2 = "invalid_exchange_response";
1070
- async function parseApiKeyExchangeRequestBody(request) {
1071
- let body;
1088
+ async function tryPlainStatus(ctx) {
1072
1089
  try {
1073
- body = await request.json();
1090
+ const { ok, ethAddress } = await fetchSignerStatus(ctx, {});
1091
+ return ok ? reachableResult(ethAddress) : null;
1074
1092
  } catch {
1075
- throw new PmtHouseError("Request body must be JSON", {
1076
- status: 400,
1077
- code: "invalid_request"
1078
- });
1079
- }
1080
- if (body === null || typeof body !== "object" || Array.isArray(body)) {
1081
- throw new PmtHouseError("Request body must be a JSON object", {
1082
- status: 400,
1083
- code: "invalid_request"
1084
- });
1085
- }
1086
- const record = body;
1087
- const apiKeyRaw = record.apiKey;
1088
- if (typeof apiKeyRaw !== "string" || !apiKeyRaw.trim()) {
1089
- throw new PmtHouseError("Request body must include apiKey", {
1090
- status: 400,
1091
- code: "invalid_request"
1092
- });
1093
+ return null;
1093
1094
  }
1094
- const scope = typeof record.scope === "string" && record.scope.trim() ? record.scope.trim() : void 0;
1095
- const clientId = typeof record.clientId === "string" && record.clientId.trim() ? record.clientId.trim() : void 0;
1096
- return { apiKey: apiKeyRaw.trim(), scope, clientId };
1097
1095
  }
1098
- async function mintUserAccessTokenFromApiKey(input) {
1099
- const fetchImpl = input.fetch ?? fetch;
1100
- const issuerOrigin = stripIssuerOriginFromOidcUrl(input.issuerUrl);
1101
- const url = `${issuerOrigin}/api/v1/apps/${encodeURIComponent(input.publicClientId)}/auth/api-key/token`;
1102
- const response = await fetchImpl(url, {
1103
- method: "POST",
1104
- headers: {
1105
- Authorization: `Bearer ${input.apiKey}`,
1106
- "Content-Type": "application/json",
1107
- Accept: "application/json"
1108
- },
1109
- body: JSON.stringify(input.scope ? { scope: input.scope } : {}),
1110
- cache: "no-store"
1111
- });
1112
- const parsed = await readJsonObjectFromResponse(response, {
1113
- invalidJsonMessage: "API key token exchange returned invalid JSON",
1114
- invalidJsonCode: "invalid_token_response",
1115
- failureLabel: "API key token exchange failed",
1116
- defaultErrorCode: "api_key_token_exchange_failed"
1117
- });
1118
- const accessToken = parsed.access_token;
1119
- if (typeof accessToken !== "string" || !accessToken.trim()) {
1120
- throw new PmtHouseError("API key token exchange missing access_token", {
1121
- status: 502,
1122
- code: EXCHANGE_RESPONSE_ERROR2
1096
+ async function trySigningProbe(ctx) {
1097
+ try {
1098
+ const token = await ctx.getDmzToken(ctx.probeSubject, "http");
1099
+ const response = await ctx.fetchImpl(`${ctx.signerUrl}/sign-orchestrator-info`, {
1100
+ method: "POST",
1101
+ headers: {
1102
+ Authorization: `Bearer ${token}`,
1103
+ "Content-Type": "application/json"
1104
+ },
1105
+ body: "{}",
1106
+ signal: AbortSignal.timeout(ctx.timeoutMs)
1123
1107
  });
1108
+ return response.ok;
1109
+ } catch {
1110
+ return false;
1124
1111
  }
1125
- const expiresIn = typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 900;
1126
- const scope = typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : input.scope?.trim() || "sign:job";
1127
- return {
1128
- access_token: accessToken.trim(),
1129
- expires_in: expiresIn,
1130
- scope
1131
- };
1132
- }
1133
- async function mintSignerSessionFromApiKey(input) {
1134
- const userToken = await mintUserAccessTokenFromApiKey({
1135
- issuerUrl: input.issuerUrl,
1136
- publicClientId: input.publicClientId,
1137
- apiKey: input.apiKey,
1138
- scope: input.scope,
1139
- fetch: input.fetch
1140
- });
1141
- return mintSignerTokenFromDeviceToken({
1142
- issuerUrl: input.issuerUrl,
1143
- m2mClientId: input.m2mClientId,
1144
- m2mClientSecret: input.m2mClientSecret,
1145
- deviceToken: userToken.access_token,
1146
- scope: userToken.scope,
1147
- audience: input.audience,
1148
- fetch: input.fetch,
1149
- allowInsecureHttp: input.allowInsecureHttp
1150
- });
1151
1112
  }
1152
- async function exchangeApiKeyForSigner(options) {
1153
- const fetchImpl = options.fetch ?? fetch;
1154
- const url = `${stripTrailingSlashes(options.facadeUrl)}/api/pymthouse/keys/exchange`;
1155
- const body = { apiKey: options.apiKey };
1156
- if (options.scope?.trim()) {
1157
- body.scope = options.scope.trim();
1113
+ async function runSignerReachabilityProbes(ctx) {
1114
+ const jwtResult = await tryJwtStatus(ctx);
1115
+ if (jwtResult) {
1116
+ return jwtResult;
1158
1117
  }
1159
- if (options.clientId?.trim()) {
1160
- body.clientId = options.clientId.trim();
1118
+ const plainResult = await tryPlainStatus(ctx);
1119
+ if (plainResult) {
1120
+ return plainResult;
1161
1121
  }
1162
- const response = await fetchImpl(url, {
1163
- method: "POST",
1164
- headers: {
1165
- "Content-Type": "application/json",
1166
- Accept: "application/json"
1167
- },
1168
- body: JSON.stringify(body),
1169
- cache: "no-store"
1170
- });
1171
- const parsed = await readJsonObjectFromResponse(response, {
1172
- invalidJsonMessage: "API key exchange returned invalid JSON",
1173
- invalidJsonCode: EXCHANGE_RESPONSE_ERROR2,
1174
- failureLabel: "API key exchange failed",
1175
- defaultErrorCode: "api_key_exchange_failed"
1176
- });
1177
- const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
1178
- const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
1179
- const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
1180
- return normalizeDeviceExchangeResponse(
1181
- {
1182
- access_token: accessToken,
1183
- expires_in: typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 3600,
1184
- scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
1185
- balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
1186
- lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
1187
- },
1188
- { signerUrl }
1189
- );
1122
+ if (await trySigningProbe(ctx)) {
1123
+ return reachableResult();
1124
+ }
1125
+ return { reachable: false };
1190
1126
  }
1191
- function createApiKeyExchangeHandler(config) {
1192
- const publicClientId = config.publicClientId.trim();
1193
- return async function apiKeyExchangeHandler(request) {
1194
- try {
1195
- if (request.method !== "POST") {
1196
- return new Response(JSON.stringify({ error: "method_not_allowed" }), {
1197
- status: 405,
1198
- headers: { "Content-Type": "application/json" }
1199
- });
1200
- }
1201
- const parsed = await parseApiKeyExchangeRequestBody(request);
1202
- const effectiveClientId = parsed.clientId?.trim() || publicClientId;
1203
- if (effectiveClientId !== publicClientId) {
1204
- throw new PmtHouseError("clientId does not match configured public client", {
1205
- status: 400,
1206
- code: "invalid_request"
1207
- });
1208
- }
1209
- const minted = await mintSignerSessionFromApiKey({
1210
- issuerUrl: config.issuerUrl,
1211
- publicClientId,
1212
- m2mClientId: config.m2mClientId,
1213
- m2mClientSecret: config.m2mClientSecret,
1214
- apiKey: parsed.apiKey,
1215
- scope: parsed.scope,
1216
- audience: config.audience,
1217
- fetch: config.fetch,
1218
- allowInsecureHttp: config.allowInsecureHttp
1219
- });
1220
- const signerUrlValue = typeof config.signerUrl === "string" && config.signerUrl.trim() ? config.signerUrl.trim() : void 0;
1221
- const body = normalizeDeviceExchangeResponse(minted, { signerUrl: signerUrlValue });
1222
- return new Response(JSON.stringify(body), {
1223
- status: 200,
1224
- headers: {
1225
- "Content-Type": "application/json",
1226
- "Cache-Control": "no-store"
1227
- }
1228
- });
1229
- } catch (error) {
1230
- return signerHandlerErrorResponse(error);
1127
+ async function probeSignerHttpReachability(options) {
1128
+ const ctx = createSignerProbeContext(options);
1129
+ try {
1130
+ const health = await ctx.fetchImpl(`${ctx.signerUrl}/healthz`, {
1131
+ signal: AbortSignal.timeout(ctx.timeoutMs)
1132
+ });
1133
+ if (health.ok) {
1134
+ return runSignerReachabilityProbes(ctx);
1231
1135
  }
1232
- };
1136
+ } catch {
1137
+ }
1138
+ return runSignerReachabilityProbes(ctx);
1233
1139
  }
1234
1140
 
1235
1141
  // src/signer/server.ts
@@ -1250,7 +1156,6 @@ function toResponse(result) {
1250
1156
  }
1251
1157
  function createDirectSignerProxyHandler(config) {
1252
1158
  const tokenManager = createSignerTokenManager({
1253
- publicClientId: config.pymthouseClientId,
1254
1159
  mint: (externalUserId) => mintUserSignerToken({
1255
1160
  issuerUrl: config.pymthouseIssuerUrl,
1256
1161
  m2mClientId: config.pymthouseM2MClientId,
@@ -1293,39 +1198,40 @@ function createDirectSignerProxyHandler(config) {
1293
1198
  code: "invalid_external_user_id"
1294
1199
  });
1295
1200
  }
1296
- let token = await tokenManager.getToken(externalUserId);
1201
+ const publicClientId = config.resolvePublicClientId ? (await config.resolvePublicClientId(session)).trim() : config.pymthouseClientId.trim();
1202
+ if (!publicClientId) {
1203
+ throw new PmtHouseError("resolvePublicClientId returned an empty id", {
1204
+ status: 500,
1205
+ code: "invalid_client_id"
1206
+ });
1207
+ }
1208
+ let token = await tokenManager.getToken(publicClientId, externalUserId);
1297
1209
  const blocked = await runBeforeSign(token, externalUserId, request);
1298
1210
  if (blocked) {
1299
1211
  return blocked;
1300
1212
  }
1301
- let upstream = await forwardWithOptionalMetering({
1302
- config,
1303
- externalUserId,
1304
- forward: () => forwardOnce(token, request)
1305
- });
1213
+ let upstream = await forwardOnce(token, request);
1306
1214
  if (upstream.status === 401) {
1307
- tokenManager.invalidate(externalUserId);
1308
- token = await tokenManager.getToken(externalUserId, { forceRefresh: true });
1215
+ tokenManager.invalidate(publicClientId, externalUserId);
1216
+ token = await tokenManager.getToken(publicClientId, externalUserId, {
1217
+ forceRefresh: true
1218
+ });
1309
1219
  const retryBlocked = await runBeforeSign(token, externalUserId, request);
1310
1220
  if (retryBlocked) {
1311
1221
  return retryBlocked;
1312
1222
  }
1313
- upstream = await forwardWithOptionalMetering({
1314
- config,
1315
- externalUserId,
1316
- forward: () => forwardOnce(token, request)
1317
- });
1223
+ upstream = await forwardOnce(token, request);
1318
1224
  }
1319
1225
  return upstream;
1320
1226
  } catch (error) {
1321
1227
  return signerHandlerErrorResponse(error);
1322
1228
  }
1323
1229
  };
1324
- handler.getCachedUsage = (externalUserId) => tokenManager.peek(externalUserId);
1325
- handler.invalidateToken = (externalUserId) => tokenManager.invalidate(externalUserId);
1230
+ handler.getCachedUsage = (externalUserId) => tokenManager.peek(config.pymthouseClientId, externalUserId);
1231
+ handler.invalidateToken = (externalUserId) => tokenManager.invalidate(config.pymthouseClientId, externalUserId);
1326
1232
  return handler;
1327
1233
  }
1328
1234
 
1329
- export { LIVEPEER_REMOTE_SIGNER_AUDIENCE, SIGN_MINT_USER_TOKEN_SCOPE, createApiKeyExchangeHandler, createDeviceExchangeHandler, createDirectSignerProxyHandler, createSignerTokenManager, decodeJwtPayload, exchangeApiKeyForSigner, exchangeDeviceTokenForSigner, extractSignerAccessTokenFromExchangeBody, forwardDirectSignerRequest, forwardToSigner, forwardWithOptionalMetering, getCachedDmzBearerToken, identityFromJwtPayload, ingestSignedTicket, livepeerIdentityHeaders, mintSignerSessionFromApiKey, mintSignerTokenFromDeviceToken, mintUserAccessTokenFromApiKey, mintUserSignerToken, normalizeDeviceExchangeResponse, normalizeSignerBaseUrl, parseApiKeyExchangeRequestBody, parseDeviceExchangeRequestBody, parseMintUserSignerTokenResponse, parseSignerUsageSnapshot, pickConflictingNumberAliases, pickConflictingStringAliases, probeSignerHttpReachability, readSignerUpstreamBody, resolveSignerBaseUrl, signerSnapshotToIngestPayload, stripSignerUsageFromResponse };
1235
+ export { LIVEPEER_REMOTE_SIGNER_AUDIENCE, SIGN_MINT_USER_TOKEN_SCOPE, createApiKeyExchangeHandler, createDeviceExchangeHandler, createDirectSignerProxyHandler, createSignerTokenManager, decodeJwtPayload, exchangeApiKeyForSigner, exchangeDeviceTokenForSigner, extractSignerAccessTokenFromExchangeBody, forwardDirectSignerRequest, forwardToSigner, getCachedDmzBearerToken, identityFromJwtPayload, livepeerIdentityHeaders, mintSignerSessionFromApiKey, mintSignerTokenFromDeviceToken, mintUserAccessTokenFromApiKey, mintUserSignerToken, normalizeDeviceExchangeResponse, normalizeSignerBaseUrl, parseApiKeyExchangeRequestBody, parseDeviceExchangeRequestBody, parseMintUserSignerTokenResponse, parseSignerUsageSnapshot, pickConflictingNumberAliases, pickConflictingStringAliases, probeSignerHttpReachability, readSignerUpstreamBody, resolveSignerBaseUrl, signerJwtAudience, stripSignerUsageFromResponse };
1330
1236
  //# sourceMappingURL=server.js.map
1331
1237
  //# sourceMappingURL=server.js.map