@pymthouse/builder-sdk
v0.4.1
Published
PymtHouse Builder API and OIDC client (OpenID-certified oauth4webapi)
Downloads
880
Maintainers
Readme
@pymthouse/builder-sdk
Source repository: pymthouse/builder-sdk. The npm package name is @pymthouse/builder-sdk.
TypeScript client for the PymtHouse Builder API, Usage API, and OIDC issuer surfaces.
OAuth/OIDC protocol calls use oauth4webapi (OpenID-certified relying-party implementation). PymtHouse-specific REST paths and helpers live in PmtHouseClient.
Install
pnpm add @pymthouse/builder-sdkMaintainers: see docs/RELEASING.md for trusted publishing and re-running failed releases.
Quick start
import { PmtHouseClient } from "@pymthouse/builder-sdk";
import {
createPmtHouseClientFromEnv,
getPymthouseBaseUrl,
} from "@pymthouse/builder-sdk/env";
const client = createPmtHouseClientFromEnv();
const base = getPymthouseBaseUrl();
const discovery = await client.getDiscovery();Or construct explicitly:
import { PmtHouseClient } from "@pymthouse/builder-sdk";
const client = new PmtHouseClient({
issuerUrl: process.env.PYMTHOUSE_ISSUER_URL!,
publicClientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID!,
m2mClientId: process.env.PYMTHOUSE_M2M_CLIENT_ID!,
m2mClientSecret: process.env.PYMTHOUSE_M2M_CLIENT_SECRET!,
allowInsecureHttp: process.env.PYMTHOUSE_ISSUER_URL?.startsWith("http:"),
});User tokens: short-lived JWT or long-lived signer session
Use mintUserAccessToken() when your backend needs the short-lived
Builder-minted user JWT directly:
const userJwt = await client.mintUserAccessToken({
externalUserId: "naap-user-123",
scope: "sign:job",
});Use mintUserSignerSessionToken() when you want the user-facing opaque
pmth_... signer session. This first mints the short-lived user JWT, then
performs the RFC 8693 token exchange with the confidential M2M client:
const signerSession = await client.mintUserSignerSessionToken({
externalUserId: "naap-user-123",
scope: "sign:job",
});For advanced flows that already have a user JWT, call
exchangeForSignerSession({ userJwt }) directly.
Dashboard API keys (long-lived pmth_*)
Create a key in the Dashboard API keys page, then exchange it for a signer session without repeating device login:
const session = await client.exchangeApiKeyForSignerSession({
apiKey: process.env.PMTH_API_KEY!,
facadeUrl: process.env.DASHBOARD_ORIGIN!, // e.g. https://dashboard.example.com
scope: "sign:job",
});
// session.access_token — opaque signer bearer for discovery / gatewaySee examples/stream-with-api-key.mjs for a minimal Node script.
Integrators can use the higher-level workflow helpers:
const session = await client.mintSignerSessionForExternalUser({
externalUserId: "naap-user-123",
email: "[email protected]",
});
// session.accessToken is opaque pmth_…
await client.approveDeviceLogin({
externalUserId: "naap-user-123",
userCode: "ABCD-EFGH",
publicClientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID,
});Usage API: session-scoped scope=me BFF helper
const payload = await client.fetchUsageForExternalUser({
externalUserId: "naap-user-123",
startDate,
endDate,
});
// payload.currentUser includes fiat totals + merged pipelineModelsApp manifest
const { manifest, etag, notModified } = await client.getAppManifest({
ifNoneMatch: cachedEtag ?? undefined,
});Remote signer identity webhook
For go-livepeer -remoteSignerWebhookUrl deployments, builder-sdk provides the
reference integration security webhook that validates end-user credentials and
returns UsageIdentity to the signer (POST /authorize).
Transport (signer shared-secret auth, wire protocol) is separate from end-user
auth strategies (EndUserAuthVerifier). OIDC/JWT is the default; an API-key
adapter and a composite "first match" adapter are also provided, and you can
plug in any custom verifier.
import {
createApiKeyEndUserVerifier,
createOidcRemoteSignerWebhookConfig,
createRemoteSignerAuthorizeHandler,
type EndUserAuthVerifier,
} from "@pymthouse/builder-sdk/signer/webhook";
// OIDC (default): Auth0, pymthouse issuer, etc.
const authorize = createRemoteSignerAuthorizeHandler(
createOidcRemoteSignerWebhookConfig({
webhookSecret: process.env.WEBHOOK_SECRET!,
jwtIssuer: process.env.JWT_ISSUER!,
jwtAudience: process.env.JWT_AUDIENCE!,
claimMapping: { claimClientId: "azp", usageSubjectType: "auth0_user_id" },
}),
);
// API key: resolve your own keys to a UsageIdentity
const apiKeyVerifier = createApiKeyEndUserVerifier({
issuer: process.env.JWT_ISSUER!,
resolveApiKey: async (key) => (await lookup(key)) ?? null,
});
// Custom provider: implement EndUserAuthVerifier
const customConfig = {
webhookSecret: process.env.WEBHOOK_SECRET!,
endUserAuth: {
kind: "custom",
verify: async ({ authorization, payload, request }) => {
// validate provider credentials, return UsageIdentity
return { identity: { ... }, expiry: Math.trunc(Date.now() / 1000) + 300 };
},
} satisfies EndUserAuthVerifier,
};Env vars align with auth0-livepeer bootstrap output (.env.livepeer). For Auth0,
set CLAIM_CLIENT_ID=azp and USAGE_SUBJECT_TYPE=auth0_user_id.
Subpath exports
| Import | Purpose |
|--------|---------|
| @pymthouse/builder-sdk | PmtHouseClient, usage helpers, manifest parsers, token helpers |
| @pymthouse/builder-sdk/signer/webhook | Identity webhook for -remoteSignerWebhookUrl |
| @pymthouse/builder-sdk/config | isPymthouseConfigured, readPymthouseEnv (Edge/middleware-safe) |
| @pymthouse/builder-sdk/tokens | Signer session TTL, JWT shape helpers, parseSignerSessionExchange |
| @pymthouse/builder-sdk/format | Wei formatting for Usage API |
| @pymthouse/builder-sdk/env | createPmtHouseClientFromEnv, getPymthouseBaseUrl (server-only) |
| @pymthouse/builder-sdk/device | RFC 8628 pollDeviceToken |
| @pymthouse/builder-sdk/device-initiate | Option B device login validation (Edge-safe) |
| @pymthouse/builder-sdk/verify | RFC 9068 verifyJwt |
Usage API: duplicate byUser rows
When getUsage({ groupBy: "user" }) returns multiple byUser rows with the same
externalUserId, sum them with summarizeUsageForExternalUser (or
aggregateUsageByExternalUserId on byUser alone):
import { summarizeUsageForExternalUser } from "@pymthouse/builder-sdk";
const usage = await client.getUsage({ groupBy: "user", startDate, endDate });
const summary = summarizeUsageForExternalUser(usage, externalUserId);
// summary.requestCount, summary.feeWei (wei string)Billing: plans, retail usage, signed-ticket ingest
Plans (apiVersion=2): listBillingProducts({ apiVersion: "2" }) returns BillingProduct[] with capability pricing and sync status. syncBillingProduct(planId) POSTs to OpenMeter.
Retail estimates: getUsage({ includeRetail: true, groupBy: "pipeline_model" }) adds endUserBillableUsdMicros / fiat rows when the active plan has retail rates.
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.
Routing: getSignerRouting() returns the remote DMZ URL, webhook URL, and migration hints (directDmz / deprecatedHostedFacade).
Allowances (OpenMeter): Trial and manual USD micros allowance use OpenMeter entitlements — not a Postgres wei ledger.
| Method | SDK | HTTP |
|--------|-----|------|
| Read balance | getUsageBalance(externalUserId) | GET .../usage/balance?externalUserId= |
| Read allowance detail | getUserAllowances(externalUserId) | GET .../users/{id}/allowances |
| Top-up grant | grantUserAllowance(externalUserId, { amountUsdMicros, source }) | POST .../users/{id}/allowances |
grantUserCredits / getUserCredits remain as deprecated aliases that call the allowances / balance endpoints. POST .../users/{id}/credits was removed from PymtHouse (the route may still re-export allowances temporarily).
Plan pricing helpers: markupPercentToRetailRateUsd, applyRetailRateToNetworkMicros (exported from the main entry).
Usage API: pipeline/model grouping
When getUsage({ groupBy: "pipeline_model", startDate, endDate, userId }) returns
byPipelineModel, use listUsageByPipelineModel for a stable-sorted copy. Pass
the optional gatewayRequestId filter to scope results to a single upstream
gateway request:
import { listUsageByPipelineModel } from "@pymthouse/builder-sdk";
const usage = await client.getUsage({
groupBy: "pipeline_model",
startDate,
endDate,
userId: internalUserId,
gatewayRequestId, // optional: filter to a single gateway request
});
const rows = listUsageByPipelineModel(usage);Documentation
Authoritative API behavior: PymtHouse docs/builder-api.md.
Server-only: createPmtHouseClientFromEnv / @pymthouse/builder-sdk/env
M2M credentials are confidential. The env entry point:
- Throws as soon as the module loads in a browser (detects
globalThis.window), so a mistaken client import fails immediately instead of silently bundling secrets. - Does not stop someone from putting
m2mClientSecretinnew PmtHouseClient({ ... })in client code—you still must not do that.
Next.js — build-time guard (optional): in a file that is only used from the server, add the official marker so the bundler errors instead of shipping the module to the client:
// e.g. lib/pymthouse-server.ts
import "server-only";
export {
createPmtHouseClientFromEnv,
getPymthouseBaseUrl,
} from "@pymthouse/builder-sdk/env";Import createPmtHouseClientFromEnv only from that wrapper (or from Route Handlers / Server Actions directly).
Next.js (monorepo) consumption
When the SDK lives as a sibling folder (e.g. ../node-pymt-sdk), enable experimental.externalDir in next.config and re-export from a small lib shim that points at ../../node-pymt-sdk (see the website app in this org). Published installs from npm use the package name directly without shims.
License
MIT
