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