feat(billing): mint user-scoped signer JWT for per-key remote signer (P7)#406
Conversation
…(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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis 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 ChangesUser-scoped signer JWT flow
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>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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
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. Comment |
…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>
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-paymentticket to the funded per-key wallet. Forwarding the opaquepmth_session was rejected withInvalid JWT(502). (/sign-orchestrator-infois unauthenticated, which masked the asymmetry.)Fix (additive, flag-gated
per_key_remote_signer, default OFF):@pymthouse/builder-sdk0.4.3 → 0.4.6across the monorepo (web-next + developer-api fe/be), matching dashboard chore(ci): bump codecov/codecov-action from 4 to 5 #3.mintUserSignerJwtForExternalUser()wrapping builder-sdkmintUserSignerToken(grant_type=client_credentials,scope=sign:mint_user_token,external_user_idbaked in).auddefaults to the OIDC issuer URL — set automatically by the SDK (signerJwtAudience(issuer)); noaudknob (current issuers rejectlivepeer-remote-signerwithinvalid_target).resolveSignerEndpoint()now mints a fresh user signer JWT, validates the DMZ base viaassertDirectSignerBaseUrl(rejects dashboard/api/signerproxy URLs), and forwardsAuthorization: Bearer <jwt>. TheexternalUserId(= the key'sbillingAccountRef.accountId) is threaded from the validate front door.Zero-regression
resolveSignerEndpoint.mintSignerSession(the flag-OFF default / Daydream path) still mints the opaque bundle byte-for-byte.simple-infrachange: theSIGNER_FROM_VALIDATEleg forwardssignerSession.headersverbatim, so swappingpmth_→ JWT in the header is transparent.Test plan
Bearer eyJ…when flag ON; opaquepmth_never leaks into the header.externalUserId/ dashboard proxy URL → throws (front door keeps token bundle).mintUserSignerJwtForExternalUserunit tests (issuer/creds/TTL/scope/insecure-http/error).mainbaseline.naap_key →/keys/validatereturns endpoint form with a real JWT (aud=issuer,external_user_idpresent) → canary SDK/inference→/generate-live-payment200 + 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
Bug Fixes
Chores