Skip to content

feat(billing): mint user-scoped signer JWT for per-key remote signer (P7)#406

Merged
seanhanca merged 1 commit into
mainfrom
feat/per-key-signer-jwt-exchange
Jun 25, 2026
Merged

feat(billing): mint user-scoped signer JWT for per-key remote signer (P7)#406
seanhanca merged 1 commit into
mainfrom
feat/per-key-signer-jwt-exchange

Conversation

@seanhanca

@seanhanca seanhanca commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the per-key remote-signer path so it mints and forwards a user-scoped signer JWT (not the opaque pmth_ session) to the pymthouse signer DMZ, per the token-exchange doc (Option A) and the dashboard #3 / builder-sdk 0.4.6 pattern.

Root cause: the remote-signer DMZ identity webhook is OIDC/JWT-only — it verifies the bearer as a JWT (JWKS, aud=issuer, client_id, external_user_id) to attribute the billable /generate-live-payment ticket to the funded per-key wallet. Forwarding the opaque pmth_ session was rejected with Invalid JWT (502). (/sign-orchestrator-info is unauthenticated, which masked the asymmetry.)

Fix (additive, flag-gated per_key_remote_signer, default OFF):

  • Bump @pymthouse/builder-sdk 0.4.3 → 0.4.6 across the monorepo (web-next + developer-api fe/be), matching dashboard chore(ci): bump codecov/codecov-action from 4 to 5 #3.
  • Add mintUserSignerJwtForExternalUser() wrapping builder-sdk mintUserSignerToken (grant_type=client_credentials, scope=sign:mint_user_token, external_user_id baked in). aud defaults to the OIDC issuer URL — set automatically by the SDK (signerJwtAudience(issuer)); no aud knob (current issuers reject livepeer-remote-signer with invalid_target).
  • resolveSignerEndpoint() now mints a fresh user signer JWT, validates the DMZ base via assertDirectSignerBaseUrl (rejects dashboard /api/signer proxy URLs), and forwards Authorization: Bearer <jwt>. The externalUserId (= the key's billingAccountRef.accountId) is threaded from the validate front door.

Zero-regression

  • The JWT is minted only inside the already-flag-gated resolveSignerEndpoint. mintSignerSession (the flag-OFF default / Daydream path) still mints the opaque bundle byte-for-byte.
  • Front door keeps its fail-safe try/catch: any mint/routing error keeps the token-bundle form (never 500s).
  • No simple-infra change: the SIGNER_FROM_VALIDATE leg forwards signerSession.headers verbatim, so swapping pmth_ → JWT in the header is transparent.

Test plan

  • Unit: JWT minted + forwarded as Bearer eyJ… when flag ON; opaque pmth_ never leaks into the header.
  • Unit: per-instance vs global exchange config selection.
  • Unit (INV): flag OFF → token-bundle form byte-for-byte; resolver never called.
  • Unit: fail-safe — mint error / missing externalUserId / dashboard proxy URL → throws (front door keeps token bundle).
  • mintUserSignerJwtForExternalUser unit tests (issuer/creds/TTL/scope/insecure-http/error).
  • Full web-next suite green (1203 passed / 1 skipped); no new typecheck errors vs main baseline.
  • E2E on isolated preview: naap_ key → /keys/validate returns endpoint form with a real JWT (aud=issuer, external_user_id present) → canary SDK /inference/generate-live-payment 200 + OpenMeter increment.

Closes the NaaP side of the work tracked in pymthouse/pymthouse#164 (closed as wrong-layer; this is the correct caller-side JWT mint).

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Remote signer requests now include account-aware context, enabling more reliable user-scoped signing behavior.
    • Added support for minting and using user-scoped signer credentials, improving per-key remote signing flows.
  • Bug Fixes

    • Strengthened validation around signer endpoints to avoid using unsupported routing paths.
    • Improved fallback handling so signing continues to work when remote signer resolution fails.
  • Chores

    • Updated the builder SDK to a newer version across web and developer API packages.

…(P7)

The remote-signer DMZ identity webhook is OIDC/JWT-only: it verifies the
bearer as a JWT (JWKS, aud=issuer, client_id, external_user_id) to attribute
the billable /generate-live-payment ticket. Forwarding the opaque pmth_
session was rejected with `Invalid JWT` (502). Per the pymthouse
token-exchange doc (Option A) and dashboard #3 / builder-sdk 0.4.6, mint a
user-scoped signer JWT and forward `Authorization: Bearer <jwt>` instead.

- bump @pymthouse/builder-sdk 0.4.3 -> 0.4.6 (web-next + developer-api fe/be)
- add mintUserSignerJwtForExternalUser() wrapping builder-sdk
  mintUserSignerToken (client_credentials + sign:mint_user_token +
  external_user_id; aud defaults to issuer URL, set automatically by the SDK)
- resolveSignerEndpoint() now mints a fresh user signer JWT, validates the DMZ
  base via assertDirectSignerBaseUrl, and forwards the JWT as the bearer;
  threads externalUserId (= key's billingAccountRef.accountId) from the
  validate front door
- strictly flag-gated by per_key_remote_signer (default OFF); the opaque
  mintSignerSession + Daydream default path are byte-for-byte unchanged when
  OFF, and resolveSignerEndpoint still fails safe to the token-bundle form

Tests: JWT minted + forwarded when flag ON; opaque/legacy path unchanged when
OFF; fail-safe on mint error / missing externalUserId / proxy URL. Full
web-next suite green; no new typecheck errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
naap-platform Ready Ready Preview Jun 25, 2026 6:25pm

Request Review

@github-actions github-actions Bot added size/L Large PR (201-500 lines) scope/shell Shell app changes plugin/developer-api Developer API plugin labels Jun 25, 2026
@github-actions

Copy link
Copy Markdown

⚠️ This PR is large (411 lines changed). Consider splitting it into smaller PRs for easier review.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR adds user-scoped signer JWT minting for per-key remote signer validation, updates the billing adapter contract and Pymthouse implementation to use it, and passes externalUserId from the validate route. The builder SDK dependency is bumped in the affected packages.

Changes

User-scoped signer JWT flow

Layer / File(s) Summary
Signer JWT helper and SDK bump
apps/web-next/package.json, plugins/developer-api/*/package.json, apps/web-next/src/lib/pymthouse-client.ts, apps/web-next/src/lib/pymthouse-client.test.ts
The builder SDK version is updated in the affected packages, and pymthouse-client exports globalSignerExchangeConfig plus mintUserSignerJwtForExternalUser; tests cover JWT minting, scope overrides, expiry clamping, insecure HTTP handling, and SDK errors.
Billing adapter minting flow
apps/web-next/src/lib/billing/adapter.ts, apps/web-next/src/lib/billing/pymthouse-adapter.ts, apps/web-next/src/lib/billing/pymthouse-adapter.test.ts
BillingProviderAdapter.resolveSignerEndpoint accepts externalUserId, and PymthouseAdapter.resolveSignerEndpoint validates the signer base URL, selects signer exchange config, mints a user-scoped JWT, and returns it in the Authorization header; tests cover routing, injected config, and error cases.
Validate route passes externalUserId
apps/web-next/src/app/api/v1/keys/validate/route.ts, apps/web-next/src/app/api/v1/keys/validate/route.test.ts
The validate route passes ref.accountId as externalUserId to signer endpoint resolution, and the route test expects the new two-argument call.

Sequence Diagram(s)

sequenceDiagram
  participant route as "validate route"
  participant adapter as "PymthouseAdapter.resolveSignerEndpoint"
  participant helper as "mintUserSignerJwtForExternalUser"
  participant sdk as "mintUserSignerToken"

  route->>adapter: resolveSignerEndpoint(session, { externalUserId })
  adapter->>helper: mint user-scoped signer JWT
  helper->>sdk: mintUserSignerToken(issuer, M2M credentials, externalUserId, scope)
  sdk-->>helper: jwt, expiresAt, scope, balanceUsdMicros
  helper-->>adapter: UserSignerJwt
  adapter-->>route: Authorization: Bearer <jwt>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • livepeer/naap#405: Updates the same resolveSignerEndpoint-based per-key remote signer flow and adapter contract.
  • livepeer/naap#403: Changes the signer-exchange configuration plumbing that globalSignerExchangeConfig() and PymthouseAdapter now use.
  • livepeer/naap#383: Touches the same Pymthouse signer-exchange flow that the new user-scoped JWT minting builds on.

Suggested labels

scope/sdk

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: minting a user-scoped signer JWT for the per-key remote signer flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/per-key-signer-jwt-exchange

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seanhanca
seanhanca merged commit 02a87ae into main Jun 25, 2026
26 of 27 checks passed
@seanhanca
seanhanca deleted the feat/per-key-signer-jwt-exchange branch June 25, 2026 18:35
seanhanca added a commit that referenced this pull request Jul 20, 2026
…t superseded/dead

mintUserSignerJwtForExternalUser is coded, merged (#406/#410) and reached prod
behind per_key_remote_signer, but is now orphaned: neither main nor
feat/opaque-session-signer-endpoint call it. Superseded by composite-bearer
(#421, Fix A) and api-key signer-session exchange (#412/#424). Plan file
NAAP-SIGNER-JWT-EXCHANGE-PLAN.md does not exist; SDK mintUserSignerToken grant
was abandoned upstream (500).

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plugin/developer-api Developer API plugin scope/shell Shell app changes size/L Large PR (201-500 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant