@pymthouse/builder-sdk 0.4.3 → 0.4.5

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 (60) hide show
  1. package/README.md +120 -5
  2. package/dist/{client-zCskUJag.d.ts → client-BhNz0ZAA.d.ts} +9 -3
  3. package/dist/{client-C0HgAugK.d.cts → client-GP-mTEI7.d.cts} +9 -3
  4. package/dist/device.d.cts +1 -1
  5. package/dist/device.d.ts +1 -1
  6. package/dist/env.cjs +40 -3
  7. package/dist/env.cjs.map +1 -1
  8. package/dist/env.d.cts +2 -2
  9. package/dist/env.d.ts +2 -2
  10. package/dist/env.js +40 -3
  11. package/dist/env.js.map +1 -1
  12. package/dist/errors-C9-V_zSi.d.cts +13 -0
  13. package/dist/errors-C9-V_zSi.d.ts +13 -0
  14. package/dist/{index-D5wdxNYy.d.cts → index-M0tsyotJ.d.cts} +2 -2
  15. package/dist/{index-DFJ6qcK0.d.ts → index-rC8smShg.d.ts} +2 -2
  16. package/dist/index.cjs +40 -3
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +6 -17
  19. package/dist/index.d.ts +6 -17
  20. package/dist/index.js +40 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/{proxy-KrA1vEmh.d.ts → proxy-CZLY0IfL.d.cts} +5 -2
  23. package/dist/{proxy-0wa8QZIU.d.cts → proxy-D36SpZ6k.d.ts} +5 -2
  24. package/dist/signer/gateway.cjs +542 -0
  25. package/dist/signer/gateway.cjs.map +1 -0
  26. package/dist/signer/gateway.d.cts +81 -0
  27. package/dist/signer/gateway.d.ts +81 -0
  28. package/dist/signer/gateway.js +538 -0
  29. package/dist/signer/gateway.js.map +1 -0
  30. package/dist/signer/server.cjs +225 -0
  31. package/dist/signer/server.cjs.map +1 -1
  32. package/dist/signer/server.d.cts +35 -4
  33. package/dist/signer/server.d.ts +35 -4
  34. package/dist/signer/server.js +219 -1
  35. package/dist/signer/server.js.map +1 -1
  36. package/dist/signer/webhook/adapters/api-key.d.cts +1 -1
  37. package/dist/signer/webhook/adapters/api-key.d.ts +1 -1
  38. package/dist/signer/webhook/adapters/composite.d.cts +1 -1
  39. package/dist/signer/webhook/adapters/composite.d.ts +1 -1
  40. package/dist/signer/webhook/adapters/oidc.cjs.map +1 -1
  41. package/dist/signer/webhook/adapters/oidc.d.cts +3 -3
  42. package/dist/signer/webhook/adapters/oidc.d.ts +3 -3
  43. package/dist/signer/webhook/adapters/oidc.js.map +1 -1
  44. package/dist/signer/webhook/adapters/trusted-headers.d.cts +1 -1
  45. package/dist/signer/webhook/adapters/trusted-headers.d.ts +1 -1
  46. package/dist/signer/webhook.cjs +40 -6
  47. package/dist/signer/webhook.cjs.map +1 -1
  48. package/dist/signer/webhook.d.cts +23 -6
  49. package/dist/signer/webhook.d.ts +23 -6
  50. package/dist/signer/webhook.js +37 -7
  51. package/dist/signer/webhook.js.map +1 -1
  52. package/dist/tokens.d.cts +1 -1
  53. package/dist/tokens.d.ts +1 -1
  54. package/dist/{types-BORaHW_x.d.cts → types-CcP67AZm.d.cts} +2 -0
  55. package/dist/{types-BORaHW_x.d.ts → types-CcP67AZm.d.ts} +2 -0
  56. package/dist/{verifier-Be9WAjFF.d.cts → verifier-D8z3spC0.d.cts} +2 -0
  57. package/dist/{verifier-Be9WAjFF.d.ts → verifier-D8z3spC0.d.ts} +2 -0
  58. package/dist/verify.d.cts +1 -1
  59. package/dist/verify.d.ts +1 -1
  60. package/package.json +6 -1
package/README.md CHANGED
@@ -70,20 +70,78 @@ For advanced flows that already have a user JWT, call
70
70
 
71
71
  ### Dashboard API keys (long-lived `pmth_*`)
72
72
 
73
- Create a key in the Dashboard **API keys** page, then exchange it for a signer
74
- session without repeating device login:
73
+ Create a key in the Dashboard **API keys** page, then exchange it for a short-lived
74
+ signer JWT without repeating device login. The facade mints credentials only;
75
+ signing RPCs go **directly to the remote signer DMZ** returned as `signerUrl`.
76
+
77
+ ```ts
78
+ import {
79
+ DIRECT_SIGNER_PATHS,
80
+ exchangeApiKeyForSigner,
81
+ signerEndpointUrl,
82
+ } from "@pymthouse/builder-sdk/signer/server";
83
+
84
+ const session = await exchangeApiKeyForSigner({
85
+ facadeUrl: process.env.DASHBOARD_ORIGIN!, // exchange only
86
+ apiKey: process.env.PMTH_API_KEY!,
87
+ scope: "sign:job",
88
+ clientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID!,
89
+ });
90
+
91
+ const signerBase = session.signerUrl!; // remote signer DMZ from exchange
92
+ const orchInfo = await fetch(
93
+ signerEndpointUrl(signerBase, DIRECT_SIGNER_PATHS.signOrchestratorInfo),
94
+ {
95
+ method: "POST",
96
+ headers: {
97
+ Authorization: `Bearer ${session.access_token}`,
98
+ "Content-Type": "application/json",
99
+ },
100
+ body: JSON.stringify({ /* ... */ }),
101
+ },
102
+ );
103
+ ```
104
+
105
+ Or via `PmtHouseClient` (returns `signerUrl` when using `facadeUrl`):
75
106
 
76
107
  ```ts
77
108
  const session = await client.exchangeApiKeyForSignerSession({
78
109
  apiKey: process.env.PMTH_API_KEY!,
79
- facadeUrl: process.env.DASHBOARD_ORIGIN!, // e.g. https://dashboard.example.com
110
+ facadeUrl: process.env.DASHBOARD_ORIGIN!,
80
111
  scope: "sign:job",
81
112
  });
82
- // session.access_token — opaque signer bearer for discovery / gateway
113
+ // session.access_token — short-lived signer JWT
114
+ // session.signerUrl — remote signer DMZ base (call signer RPCs here directly)
83
115
  ```
84
116
 
85
117
  See `examples/stream-with-api-key.mjs` for a minimal Node script.
86
118
 
119
+ ### Exchange then direct signer (architecture)
120
+
121
+ ```mermaid
122
+ sequenceDiagram
123
+ participant Client
124
+ participant Facade as PlatformFacade
125
+ participant Issuer as PymtHouseIssuer
126
+ participant Signer as RemoteSignerDMZ
127
+
128
+ Client->>Facade: POST /api/pymthouse/keys/exchange (pmth_*)
129
+ Facade->>Issuer: api-key token + M2M token exchange
130
+ Issuer-->>Facade: short-lived signer JWT + signerUrl
131
+ Facade-->>Client: access_token + signerUrl
132
+ Client->>Signer: POST {signerUrl}/sign-orchestrator-info (Bearer JWT)
133
+ ```
134
+
135
+ **Do not** point `signerUrl` or gateway `--token signer` at dashboard
136
+ `/api/signer/*` proxy routes. Those proxies are removed; use the remote signer
137
+ DMZ URL from the exchange response (or `getSignerRouting().remoteDmzUrl`).
138
+
139
+ **Migration:** if you previously configured
140
+ `signer: https://dashboard.example.com/api/signer`, change to the remote signer
141
+ base returned by exchange (`signerUrl`) or routing (`remoteDmzUrl`). Keep
142
+ `facadeUrl` / `billing` pointed at the dashboard/platform origin for exchange
143
+ only (`/api/pymthouse/keys/exchange` or `/api/signer/device/exchange`).
144
+
87
145
  Integrators can use the higher-level workflow helpers:
88
146
 
89
147
  ```ts
@@ -170,11 +228,64 @@ const customConfig = {
170
228
  Env vars align with `auth0-livepeer` bootstrap output (`.env.livepeer`). For Auth0,
171
229
  set `CLAIM_CLIENT_ID=azp` and `USAGE_SUBJECT_TYPE=auth0_user_id`.
172
230
 
231
+ ## Gateway `--token` helper
232
+
233
+ The [livepeer-python-gateway](https://github.com/livepeer/livepeer-python-gateway)
234
+ `--token` is a **base64-encoded JSON** bundle (not a JWT). `buildGatewayToken`
235
+ assembles one client-side from values you already have, and `mintGatewayToken`
236
+ mints a signer JWT first as a convenience.
237
+
238
+ Two gateway auth modes:
239
+
240
+ - **`signerJwt`** — you mint a signer JWT and forward it as
241
+ `signer_headers.Authorization = "Bearer <jwt>"`. The gateway only reads the
242
+ JWT `exp`; it cannot refresh on its own (pre-mint or refresh externally).
243
+ - **`pmthApiKey`** — the gateway holds a `pmth_*` API key + the billing URL and
244
+ performs the exchange + auto-refresh itself (`api_key` + `billing` top-level).
245
+
246
+ ```ts
247
+ import {
248
+ buildGatewayToken,
249
+ mintGatewayToken,
250
+ } from "@pymthouse/builder-sdk/signer/gateway";
251
+
252
+ // Pure assembly: pre-minted signer JWT mode
253
+ const token = buildGatewayToken({
254
+ signer: "https://signer.example/generate-live-payment",
255
+ auth: { kind: "signerJwt", accessToken: userSignerJwt },
256
+ });
257
+
258
+ // Pure assembly: gateway self-refreshes via platform exchange, signs directly to signer
259
+ const apiKeyToken = buildGatewayToken({
260
+ signer: "https://signer.example",
261
+ auth: {
262
+ kind: "pmthApiKey",
263
+ apiKey: process.env.PMTH_API_KEY!,
264
+ billing: "https://dashboard.example.com",
265
+ },
266
+ });
267
+
268
+ // Convenience: mint a signer JWT (M2M client_credentials) then assemble
269
+ const minted = await mintGatewayToken({
270
+ source: "m2m",
271
+ signer: "https://signer.example/generate-live-payment",
272
+ issuerUrl: process.env.PYMTHOUSE_ISSUER_URL!,
273
+ m2mClientId: process.env.PYMTHOUSE_M2M_CLIENT_ID!,
274
+ m2mClientSecret: process.env.PYMTHOUSE_M2M_CLIENT_SECRET!,
275
+ externalUserId: "naap-user-123",
276
+ });
277
+ // Pass `token` straight to the gateway: `--token <token>`
278
+ ```
279
+
280
+ Use `decodeGatewayToken(token)` to inspect a bundle in tests/debugging.
281
+
173
282
  ## Subpath exports
174
283
 
175
284
  | Import | Purpose |
176
285
  |--------|---------|
177
286
  | `@pymthouse/builder-sdk` | `PmtHouseClient`, usage helpers, manifest parsers, token helpers |
287
+ | `@pymthouse/builder-sdk/signer/server` | Exchange handlers, direct signer URL helpers, minting, `buildGatewayToken`/`mintGatewayToken` |
288
+ | `@pymthouse/builder-sdk/signer/gateway` | Gateway `--token` assembler (`buildGatewayToken`, `mintGatewayToken`, `decodeGatewayToken`) |
178
289
  | `@pymthouse/builder-sdk/signer/webhook` | Identity webhook for `-remoteSignerWebhookUrl` |
179
290
  | `@pymthouse/builder-sdk/config` | `isPymthouseConfigured`, `readPymthouseEnv` (Edge/middleware-safe) |
180
291
  | `@pymthouse/builder-sdk/tokens` | Signer session TTL, JWT shape helpers, `parseSignerSessionExchange` |
@@ -204,7 +315,11 @@ const summary = summarizeUsageForExternalUser(usage, externalUserId);
204
315
 
205
316
  **Retail estimates:** `getUsage({ includeRetail: true, groupBy: "pipeline_model" })` adds `endUserBillableUsdMicros` / fiat rows when the active plan has retail rates.
206
317
 
207
- **Metering:** sign directly against the remote signer DMZ with `createDirectSignerProxyHandler` or `forwardDirectSignerRequest`. Usage is emitted asynchronously by go-livepeer to Kafka and ingested by the OpenMeter collector. The PymtHouse `/api/signer/*` HTTP proxy and synchronous HTTP signed-ticket ingest are removed.
318
+ **Metering:** after exchange, sign directly against the remote signer DMZ with
319
+ `forwardToSigner`, `forwardDirectSignerRequest`, or plain `fetch` to
320
+ `{signerUrl}/{path}`. Usage is emitted asynchronously by go-livepeer to Kafka
321
+ and ingested by the OpenMeter collector. Dashboard `/api/signer/*` HTTP proxy
322
+ routes and synchronous HTTP signed-ticket ingest are removed.
208
323
 
209
324
  **Routing:** `getSignerRouting()` returns the remote DMZ URL, webhook URL, and migration hints (`directDmz` / `deprecatedHostedFacade`).
210
325
 
@@ -1,5 +1,5 @@
1
1
  import { SignerSessionToken } from './tokens.js';
2
- import { P as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, a as ParsedDeviceApprovalRedirect, A as AppUserRecord, U as UpsertAppUserInput, M as MintUserAccessTokenInput, b as MintUserAccessTokenResponse, T as TokenExchangeResponse, D as DeviceApprovalInput, C as ClientCredentialsTokenResponse, c as MintUserSignerSessionTokenInput, d as UsageQueryInput, e as UsageApiResponse, S as SignedTicketIngestInput, f as SignedTicketIngestResult, g as SignerRoutingResponse, L as ListBillingProductsResult, h as PlanSyncResult, i as UsageBalanceResponse, j as UserAllowancesResponse, k as UserAllowanceGrantInput, l as GrantSource, m as UserSubscriptionResponse, n as MeScopeUsagePayload, o as GetAppManifestResult, p as MintSignerSessionForExternalUserInput, q as ApproveDeviceLoginInput } from './types-BORaHW_x.js';
2
+ import { P as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, a as ParsedDeviceApprovalRedirect, A as AppUserRecord, U as UpsertAppUserInput, M as MintUserAccessTokenInput, b as MintUserAccessTokenResponse, T as TokenExchangeResponse, D as DeviceApprovalInput, C as ClientCredentialsTokenResponse, c as MintUserSignerSessionTokenInput, d as UsageQueryInput, e as UsageApiResponse, S as SignedTicketIngestInput, f as SignedTicketIngestResult, g as SignerRoutingResponse, L as ListBillingProductsResult, h as PlanSyncResult, i as UsageBalanceResponse, j as UserAllowancesResponse, k as UserAllowanceGrantInput, l as GrantSource, m as UserSubscriptionResponse, n as MeScopeUsagePayload, o as GetAppManifestResult, p as MintSignerSessionForExternalUserInput, q as ApproveDeviceLoginInput } from './types-CcP67AZm.js';
3
3
 
4
4
  /**
5
5
  * Normalize RFC 8628 user codes for comparison and resource URIs (uppercase, strip separators).
@@ -39,8 +39,14 @@ declare class PmtHouseClient {
39
39
  scope?: string;
40
40
  }): Promise<MintUserAccessTokenResponse>;
41
41
  /**
42
- * Exchange a dashboard API key for a signer session via a trusted facade (recommended)
43
- * or directly when M2M credentials are available on this client.
42
+ * Exchange a dashboard API key for a short-lived signer JWT via a trusted facade.
43
+ *
44
+ * `facadeUrl` is used only for `POST {facadeUrl}/api/pymthouse/keys/exchange`.
45
+ * After exchange, call signer RPCs directly at `signerUrl` from the response
46
+ * (e.g. `{signerUrl}/sign-orchestrator-info`), not via dashboard `/api/signer/*`.
47
+ *
48
+ * When M2M credentials are available on this client, omit `facadeUrl` to exchange
49
+ * directly against the PymtHouse issuer.
44
50
  */
45
51
  exchangeApiKeyForSignerSession(input: {
46
52
  apiKey: string;
@@ -1,5 +1,5 @@
1
1
  import { SignerSessionToken } from './tokens.cjs';
2
- import { P as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, a as ParsedDeviceApprovalRedirect, A as AppUserRecord, U as UpsertAppUserInput, M as MintUserAccessTokenInput, b as MintUserAccessTokenResponse, T as TokenExchangeResponse, D as DeviceApprovalInput, C as ClientCredentialsTokenResponse, c as MintUserSignerSessionTokenInput, d as UsageQueryInput, e as UsageApiResponse, S as SignedTicketIngestInput, f as SignedTicketIngestResult, g as SignerRoutingResponse, L as ListBillingProductsResult, h as PlanSyncResult, i as UsageBalanceResponse, j as UserAllowancesResponse, k as UserAllowanceGrantInput, l as GrantSource, m as UserSubscriptionResponse, n as MeScopeUsagePayload, o as GetAppManifestResult, p as MintSignerSessionForExternalUserInput, q as ApproveDeviceLoginInput } from './types-BORaHW_x.cjs';
2
+ import { P as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, a as ParsedDeviceApprovalRedirect, A as AppUserRecord, U as UpsertAppUserInput, M as MintUserAccessTokenInput, b as MintUserAccessTokenResponse, T as TokenExchangeResponse, D as DeviceApprovalInput, C as ClientCredentialsTokenResponse, c as MintUserSignerSessionTokenInput, d as UsageQueryInput, e as UsageApiResponse, S as SignedTicketIngestInput, f as SignedTicketIngestResult, g as SignerRoutingResponse, L as ListBillingProductsResult, h as PlanSyncResult, i as UsageBalanceResponse, j as UserAllowancesResponse, k as UserAllowanceGrantInput, l as GrantSource, m as UserSubscriptionResponse, n as MeScopeUsagePayload, o as GetAppManifestResult, p as MintSignerSessionForExternalUserInput, q as ApproveDeviceLoginInput } from './types-CcP67AZm.cjs';
3
3
 
4
4
  /**
5
5
  * Normalize RFC 8628 user codes for comparison and resource URIs (uppercase, strip separators).
@@ -39,8 +39,14 @@ declare class PmtHouseClient {
39
39
  scope?: string;
40
40
  }): Promise<MintUserAccessTokenResponse>;
41
41
  /**
42
- * Exchange a dashboard API key for a signer session via a trusted facade (recommended)
43
- * or directly when M2M credentials are available on this client.
42
+ * Exchange a dashboard API key for a short-lived signer JWT via a trusted facade.
43
+ *
44
+ * `facadeUrl` is used only for `POST {facadeUrl}/api/pymthouse/keys/exchange`.
45
+ * After exchange, call signer RPCs directly at `signerUrl` from the response
46
+ * (e.g. `{signerUrl}/sign-orchestrator-info`), not via dashboard `/api/signer/*`.
47
+ *
48
+ * When M2M credentials are available on this client, omit `facadeUrl` to exchange
49
+ * directly against the PymtHouse issuer.
44
50
  */
45
51
  exchangeApiKeyForSignerSession(input: {
46
52
  apiKey: string;
package/dist/device.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as oauth4webapi from 'oauth4webapi';
2
- import { F as FetchLike } from './types-BORaHW_x.cjs';
2
+ import { F as FetchLike } from './types-CcP67AZm.cjs';
3
3
 
4
4
  interface PollDeviceTokenOptions {
5
5
  issuerUrl: string;
package/dist/device.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as oauth4webapi from 'oauth4webapi';
2
- import { F as FetchLike } from './types-BORaHW_x.js';
2
+ import { F as FetchLike } from './types-CcP67AZm.js';
3
3
 
4
4
  interface PollDeviceTokenOptions {
5
5
  issuerUrl: string;
package/dist/env.cjs CHANGED
@@ -307,6 +307,32 @@ var init_mint_token = __esm({
307
307
  }
308
308
  });
309
309
 
310
+ // src/signer/direct-signer.ts
311
+ function assertDirectSignerBaseUrl(signerBaseUrl) {
312
+ let parsed;
313
+ try {
314
+ parsed = new URL(signerBaseUrl.trim());
315
+ } catch {
316
+ throw new PmtHouseError("signer URL must be an absolute http(s) URL", {
317
+ status: 400,
318
+ code: "invalid_signer_url"
319
+ });
320
+ }
321
+ const pathname = stripTrailingSlashes(parsed.pathname);
322
+ if (pathname === "/api/signer" || pathname.startsWith("/api/signer/")) {
323
+ throw new PmtHouseError(
324
+ "signer URL must be the remote signer DMZ base, not a dashboard /api/signer/* proxy path. Exchange at the platform facade, then call signer endpoints directly using signerUrl from the exchange response.",
325
+ { status: 400, code: "invalid_signer_url" }
326
+ );
327
+ }
328
+ }
329
+ var init_direct_signer = __esm({
330
+ "src/signer/direct-signer.ts"() {
331
+ init_string_utils();
332
+ init_errors();
333
+ }
334
+ });
335
+
310
336
  // src/signer/device-exchange.ts
311
337
  function extractSignerAccessTokenFromExchangeBody(body) {
312
338
  const tokenObj = body.token;
@@ -539,6 +565,9 @@ async function exchangeApiKeyForSigner(options) {
539
565
  const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
540
566
  const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
541
567
  const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
568
+ if (signerUrl) {
569
+ assertDirectSignerBaseUrl(signerUrl);
570
+ }
542
571
  return normalizeDeviceExchangeResponse(
543
572
  {
544
573
  access_token: accessToken,
@@ -601,6 +630,7 @@ var init_api_key_exchange = __esm({
601
630
  init_fetch_json();
602
631
  init_handler_errors();
603
632
  init_device_exchange();
633
+ init_direct_signer();
604
634
  EXCHANGE_RESPONSE_ERROR2 = "invalid_exchange_response";
605
635
  }
606
636
  });
@@ -1097,8 +1127,14 @@ var PmtHouseClient = class {
1097
1127
  });
1098
1128
  }
1099
1129
  /**
1100
- * Exchange a dashboard API key for a signer session via a trusted facade (recommended)
1101
- * or directly when M2M credentials are available on this client.
1130
+ * Exchange a dashboard API key for a short-lived signer JWT via a trusted facade.
1131
+ *
1132
+ * `facadeUrl` is used only for `POST {facadeUrl}/api/pymthouse/keys/exchange`.
1133
+ * After exchange, call signer RPCs directly at `signerUrl` from the response
1134
+ * (e.g. `{signerUrl}/sign-orchestrator-info`), not via dashboard `/api/signer/*`.
1135
+ *
1136
+ * When M2M credentials are available on this client, omit `facadeUrl` to exchange
1137
+ * directly against the PymtHouse issuer.
1102
1138
  */
1103
1139
  async exchangeApiKeyForSignerSession(input) {
1104
1140
  if (input.facadeUrl?.trim()) {
@@ -1115,7 +1151,8 @@ var PmtHouseClient = class {
1115
1151
  token_type: exchanged.token_type,
1116
1152
  expires_in: exchanged.expires_in,
1117
1153
  scope: exchanged.scope,
1118
- issued_token_type: "urn:ietf:params:oauth:token-type:access_token"
1154
+ issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
1155
+ signerUrl: exchanged.signerUrl
1119
1156
  };
1120
1157
  }
1121
1158
  const userToken = await this.exchangeApiKeyForUserAccessToken({