@pymthouse/builder-sdk 0.4.4 → 0.4.6

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 (44) hide show
  1. package/README.md +120 -5
  2. package/dist/{client-C0HgAugK.d.cts → client-CEBVgCD7.d.cts} +28 -4
  3. package/dist/{client-zCskUJag.d.ts → client-D-p6v8ju.d.ts} +28 -4
  4. package/dist/device.d.cts +1 -1
  5. package/dist/device.d.ts +1 -1
  6. package/dist/env.cjs +64 -9
  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 +64 -9
  11. package/dist/env.js.map +1 -1
  12. package/dist/{index-CAIAYJv7.d.cts → index-M0tsyotJ.d.cts} +1 -1
  13. package/dist/{index-BL1wpOki.d.ts → index-rC8smShg.d.ts} +1 -1
  14. package/dist/index.cjs +64 -9
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +4 -4
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +64 -9
  19. package/dist/index.js.map +1 -1
  20. package/dist/{proxy-KrA1vEmh.d.ts → proxy-CZLY0IfL.d.cts} +5 -2
  21. package/dist/{proxy-0wa8QZIU.d.cts → proxy-D36SpZ6k.d.ts} +5 -2
  22. package/dist/signer/gateway.cjs +542 -0
  23. package/dist/signer/gateway.cjs.map +1 -0
  24. package/dist/signer/gateway.d.cts +81 -0
  25. package/dist/signer/gateway.d.ts +81 -0
  26. package/dist/signer/gateway.js +538 -0
  27. package/dist/signer/gateway.js.map +1 -0
  28. package/dist/signer/server.cjs +225 -0
  29. package/dist/signer/server.cjs.map +1 -1
  30. package/dist/signer/server.d.cts +35 -4
  31. package/dist/signer/server.d.ts +35 -4
  32. package/dist/signer/server.js +219 -1
  33. package/dist/signer/server.js.map +1 -1
  34. package/dist/signer/webhook/adapters/oidc.d.cts +2 -2
  35. package/dist/signer/webhook/adapters/oidc.d.ts +2 -2
  36. package/dist/signer/webhook.d.cts +3 -3
  37. package/dist/signer/webhook.d.ts +3 -3
  38. package/dist/tokens.d.cts +1 -1
  39. package/dist/tokens.d.ts +1 -1
  40. package/dist/{types-BORaHW_x.d.cts → types-CcP67AZm.d.cts} +2 -0
  41. package/dist/{types-BORaHW_x.d.ts → types-CcP67AZm.d.ts} +2 -0
  42. package/dist/verify.d.cts +1 -1
  43. package/dist/verify.d.ts +1 -1
  44. 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.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;
@@ -52,6 +58,16 @@ declare class PmtHouseClient {
52
58
  exchangeForSignerSession(input: {
53
59
  userJwt: string;
54
60
  resource?: string;
61
+ /**
62
+ * When true, omit the RFC 8707 `resource` parameter entirely. This selects
63
+ * the documented PymtHouse gateway/opaque signer-session exchange
64
+ * (long-lived `pmth_*` token) rather than the signer-JWT path that a
65
+ * `resource = issuer` indicator routes to. Takes precedence over
66
+ * {@link resource}.
67
+ */
68
+ omitResource?: boolean;
69
+ /** Optional `scope` for the exchange (e.g. `sign:job`). Omitted when unset. */
70
+ scope?: string;
55
71
  }): Promise<TokenExchangeResponse>;
56
72
  /**
57
73
  * Mint a short-lived per-user JWT with the Builder API, then exchange it for
@@ -109,7 +125,15 @@ declare class PmtHouseClient {
109
125
  signal?: AbortSignal;
110
126
  }): Promise<GetAppManifestResult>;
111
127
  /**
112
- * Upsert an external user, mint a short-lived JWT, and exchange for an opaque signer session.
128
+ * Upsert an external user, mint a short-lived JWT, and exchange it for a
129
+ * long-lived opaque (`pmth_*`) signer session.
130
+ *
131
+ * Performs the *documented* remote-signer-session exchange (see
132
+ * `builder-api.md` → "Remote signer session exchange"): the RFC 8693 token
133
+ * exchange is sent with `scope=sign:job` and **no `resource` indicator**,
134
+ * which selects the PymtHouse gateway/opaque path. A prior implementation set
135
+ * `resource = issuer`, which routed to the signer-JWT path and returned a JWT
136
+ * that {@link parseSignerSessionExchange} then rejected as non-opaque.
113
137
  */
114
138
  mintSignerSessionForExternalUser(input: MintSignerSessionForExternalUserInput): Promise<SignerSessionToken>;
115
139
  /**
@@ -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;
@@ -52,6 +58,16 @@ declare class PmtHouseClient {
52
58
  exchangeForSignerSession(input: {
53
59
  userJwt: string;
54
60
  resource?: string;
61
+ /**
62
+ * When true, omit the RFC 8707 `resource` parameter entirely. This selects
63
+ * the documented PymtHouse gateway/opaque signer-session exchange
64
+ * (long-lived `pmth_*` token) rather than the signer-JWT path that a
65
+ * `resource = issuer` indicator routes to. Takes precedence over
66
+ * {@link resource}.
67
+ */
68
+ omitResource?: boolean;
69
+ /** Optional `scope` for the exchange (e.g. `sign:job`). Omitted when unset. */
70
+ scope?: string;
55
71
  }): Promise<TokenExchangeResponse>;
56
72
  /**
57
73
  * Mint a short-lived per-user JWT with the Builder API, then exchange it for
@@ -109,7 +125,15 @@ declare class PmtHouseClient {
109
125
  signal?: AbortSignal;
110
126
  }): Promise<GetAppManifestResult>;
111
127
  /**
112
- * Upsert an external user, mint a short-lived JWT, and exchange for an opaque signer session.
128
+ * Upsert an external user, mint a short-lived JWT, and exchange it for a
129
+ * long-lived opaque (`pmth_*`) signer session.
130
+ *
131
+ * Performs the *documented* remote-signer-session exchange (see
132
+ * `builder-api.md` → "Remote signer session exchange"): the RFC 8693 token
133
+ * exchange is sent with `scope=sign:job` and **no `resource` indicator**,
134
+ * which selects the PymtHouse gateway/opaque path. A prior implementation set
135
+ * `resource = issuer`, which routed to the signer-JWT path and returned a JWT
136
+ * that {@link parseSignerSessionExchange} then rejected as non-opaque.
113
137
  */
114
138
  mintSignerSessionForExternalUser(input: MintSignerSessionForExternalUserInput): Promise<SignerSessionToken>;
115
139
  /**
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({
@@ -1189,8 +1226,13 @@ var PmtHouseClient = class {
1189
1226
  params.set("subject_token", input.userJwt);
1190
1227
  params.set("subject_token_type", SUBJECT_ACCESS_TOKEN_TYPE2);
1191
1228
  params.set("requested_token_type", REQUESTED_ACCESS_TOKEN_TYPE);
1192
- const resourceCandidate = typeof input.resource === "string" && input.resource.trim() !== "" ? input.resource.trim() : this.issuerUrl;
1193
- params.set("resource", stripTrailingSlashes(resourceCandidate));
1229
+ if (typeof input.scope === "string" && input.scope.trim() !== "") {
1230
+ params.set("scope", input.scope.trim());
1231
+ }
1232
+ if (!input.omitResource) {
1233
+ const resourceCandidate = typeof input.resource === "string" && input.resource.trim() !== "" ? input.resource.trim() : this.issuerUrl;
1234
+ params.set("resource", stripTrailingSlashes(resourceCandidate));
1235
+ }
1194
1236
  try {
1195
1237
  const response = await oauth4webapi.genericTokenEndpointRequest(
1196
1238
  as,
@@ -1476,18 +1518,31 @@ var PmtHouseClient = class {
1476
1518
  };
1477
1519
  }
1478
1520
  /**
1479
- * Upsert an external user, mint a short-lived JWT, and exchange for an opaque signer session.
1521
+ * Upsert an external user, mint a short-lived JWT, and exchange it for a
1522
+ * long-lived opaque (`pmth_*`) signer session.
1523
+ *
1524
+ * Performs the *documented* remote-signer-session exchange (see
1525
+ * `builder-api.md` → "Remote signer session exchange"): the RFC 8693 token
1526
+ * exchange is sent with `scope=sign:job` and **no `resource` indicator**,
1527
+ * which selects the PymtHouse gateway/opaque path. A prior implementation set
1528
+ * `resource = issuer`, which routed to the signer-JWT path and returned a JWT
1529
+ * that {@link parseSignerSessionExchange} then rejected as non-opaque.
1480
1530
  */
1481
1531
  async mintSignerSessionForExternalUser(input) {
1532
+ const scope = input.scope ?? SIGN_JOB_SCOPE;
1482
1533
  await this.upsertAppUser({
1483
1534
  externalUserId: input.externalUserId,
1484
1535
  email: input.email,
1485
1536
  status: "active"
1486
1537
  });
1487
- const exchange = await this.mintUserSignerSessionToken({
1538
+ const userToken = await this.mintUserAccessToken({
1488
1539
  externalUserId: input.externalUserId,
1489
- scope: input.scope ?? SIGN_JOB_SCOPE,
1490
- resource: this.issuerUrl
1540
+ scope
1541
+ });
1542
+ const exchange = await this.exchangeForSignerSession({
1543
+ userJwt: userToken.access_token,
1544
+ omitResource: true,
1545
+ scope
1491
1546
  });
1492
1547
  return parseSignerSessionExchange(exchange);
1493
1548
  }