Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,31 @@ SIGNER_REMOTE_DISCOVERY=0
# ORCH_WEBHOOK_URL=
# LIVE_AI_CAP_REPORT_INTERVAL=

# --- openmeter-collector ---
# --- openmeter-collector / builder-api ---
# Single-org fallback when KONNECT_CREDENTIALS_URL is unset / tenant unbound.
# Konnect base: https://<region>.api.konghq.com/v3/openmeter
# Self-hosted base: https://<host>
OPENMETER_URL=https://us.api.konghq.com/v3/openmeter
OPENMETER_API_KEY=
OPENMETER_DEFAULT_PLAN_KEY=clearinghouse_default_ppu
# Trial grant on first session provision (0 disables). Enforced before signer JWT mint.
OPENMETER_TRIAL_FEATURE_KEY=billable_spend
OPENMETER_TRIAL_GRANT_USD_MICROS=0
OPENMETER_ENFORCE_ALLOWANCE=true
# builder-api delegates end-user JWT verification to identity-webhook (POST /authorize).
# Reuses WEBHOOK_SECRET above; defaults the URL to REMOTE_SIGNER_WEBHOOK_URL when unset.
# IDENTITY_WEBHOOK_URL=http://identity-webhook:8090

# Optional (collector defaults shown)
PRICE_ORACLE_URL=https://api.coinbase.com/v2/prices/ETH-USD/spot
PRICE_ORACLE_REFRESH=5m

# --- konnect-credentials (+ collector multi-tenant ingest + builder-api) ---
# M2M secret for bind/credentials APIs and internal lookups.
PLATFORM_API_SECRET=dev-platform-secret-change-me
# AES key material: 64-char hex, 32-byte base64, or passphrase (SHA-256).
CREDENTIALS_ENCRYPTION_KEY=dev-credentials-encryption-change-me
# Compose sets KONNECT_CREDENTIALS_URL=http://konnect-credentials:8091 for collector/builder-api.
# Leave unset outside Compose to use OPENMETER_URL + OPENMETER_API_KEY only.
# KONNECT_CREDENTIALS_URL=
# KONNECT_IDENTITY_BASE=https://global.api.konghq.com/v2
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ node_modules/
# Editor / OS
.vscode/
.DS_Store
auth0-provisioner/provision/.env.livepeer
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ Docker Compose stack for the clearinghouse runtime:
| Service | Role | Docs |
| --- | --- | --- |
| **identity-webhook** (`identity-webhook`) | Resolves end-user credentials (API keys and/or OAuth/OIDC JWTs) to `auth_id` for go-livepeer's `/authorize` hook. Self-contained: implements the go-livepeer webhook wire protocol in-repo, verifying JWTs with `jose`. | [jose](https://github.com/panva/jose) |
| **konnect-credentials** (`konnect-credentials`) | Binds a per-tenant Konnect org and issues Provisioner / Usage SPATs (Ingest SPAT retained for the collector). Tenants call Konnect Metering & Billing directly — no API mirror. | [konnect-credentials/README.md](konnect-credentials/README.md) |
| **Redpanda** (`kafka`) | Kafka-compatible event bus. The signer publishes gateway events; the collector consumes them. | [Redpanda docs](https://docs.redpanda.com/) |
| **go-livepeer remote signer** (`remote-signer`) | Signs Livepeer payment tickets and emits `create_signed_ticket` events to Kafka. | [go-livepeer](https://github.com/livepeer/go-livepeer) |
| **OpenMeter collector** (`openmeter-collector`) | Benthos pipeline: filters Kafka events, converts fees to USD micros, POSTs CloudEvents to OpenMeter ingest. | [OpenMeter collector](https://openmeter.io/docs/collectors) |
| **Konnect / OpenMeter** (external) | Hosted metering and billing API. Set `OPENMETER_URL` to your OpenMeter API base; the collector appends the events path. | [Konnect OpenMeter](https://docs.konghq.com/konnect/openmeter/), [self-hosted OpenMeter](https://openmeter.io/docs/deploy/kubernetes) |
| **OpenMeter collector** (`openmeter-collector`) | Benthos pipeline: filters Kafka events, converts fees to USD micros, POSTs CloudEvents to OpenMeter ingest (per-tenant Ingest SPAT when bound). | [OpenMeter collector](https://openmeter.io/docs/collectors) |
| **Konnect / OpenMeter** (external) | Hosted metering and billing API. One org per platform tenant for SPAT isolation. | [Konnect OpenMeter](https://docs.konghq.com/konnect/openmeter/), [self-hosted OpenMeter](https://openmeter.io/docs/deploy/kubernetes) |

Data flow:

Expand Down Expand Up @@ -216,6 +217,19 @@ The collector emits `billable_usd_micros` as an interim passthrough equal to
markup rules (network × pipeline/model multiplier) are not applied yet — until then
`billable_usd_micros == network_fee_usd_micros`.

### Per-tenant Konnect SPATs

For multi-tenant isolation, bind one Konnect org per `client_id` and issue SPATs
via [`konnect-credentials`](konnect-credentials/README.md):

1. `POST /v1/tenants/{clientId}/konnect/bind` — BYO org admin PAT
2. `POST /v1/tenants/{clientId}/konnect/credentials` — Provisioner + Usage SPATs (once)
3. `POST /v1/tenants/{clientId}/konnect/catalog` — meters / features / default plan
4. Collector looks up Ingest SPAT at `GET /v1/internal/tenants/{clientId}/ingest`

Single-org local stacks can still set `OPENMETER_URL` + `OPENMETER_API_KEY` and leave
tenants unbound; the collector falls back to those env vars when the lookup misses.

### Identity contract (collector)

Three layers — each owns a different piece of the identity story:
Expand Down
112 changes: 112 additions & 0 deletions auth0-provisioner/provision/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Auth0 client scaffolding (CLI-login bash)

A thin, idempotent bash scaffolder for the clearinghouse Auth0 objects — the resource
server (API), the **public/M2M client pair**, and their client grants — driven entirely by
your **`auth0 login` session**. It is the Auth0 analog of the OpenMeter
[`provision/bootstrap.sh`](../../openmeter-collector/provision): the data lives in
[`apps.json`](apps.json); the script is thin and re-runnable.

Unlike the Go [`auth0ctl`](../README.md) tool, this needs **no Management API client
id/secret** — every call rides your CLI session via `auth0 api` (the authenticated
Management API v2 passthrough), the same way the OpenMeter script uses `kongctl api`.

## Prerequisites

- The [Auth0 CLI](https://github.com/auth0/auth0-cli) and `jq` on `PATH`.
- An authenticated session against the target tenant:

```bash
auth0 login # interactive, one-time
auth0 tenants use <tenant> # if you have more than one
```

## Usage

```bash
cd auth0-provisioner/provision
./bootstrap.sh
```

The script is safe to re-run: it matches the API by `identifier` and clients by `name`
(or optional `public.client_id` / `m2m.client_id` in `apps.json`), reusing existing
ids and updating grant scopes in place — never duplicating. If provisioning an app
fails partway through, only objects created during that run are deleted (rollback);
pre-existing clients and grants are left intact. It writes the resulting ids and M2M
secret to `.env.livepeer` (gitignored).

## What it provisions

From [`apps.json`](apps.json):

| Kind | Identity | Notes |
| --- | --- | --- |
| Resource server | `livepeer-clearinghouse` | API; `identifier` = audience; RS256; `allow_offline_access`; full scope set. |
| Tenant settings | — | `default_audience` + `device_flow` (RFC 8628). Best-effort; skipped with a warning if the session lacks `update:tenant_settings`. |
| Public client | `<App> Public` | `native`, `token_endpoint_auth_method: none`, grants `device_code` + `refresh_token`. |
| M2M client | `<App> M2M` | `non_interactive`, `client_secret_post`, grant `client_credentials`. |
| Management M2M | `Clearinghouse Builder Management` | Auth0 Management API (`create:users`, `read:users`, `update:users`) for Builder API user provisioning. |
| Client grants | per client | Public + M2M each granted their configured scopes against the audience. |

## How it maps to the `auth0` CLI

Each step is a Management API v2 call through the CLI passthrough:

```bash
auth0 api get "resource-servers?per_page=100"
auth0 api post "resource-servers" --data '{ "identifier": "...", "scopes": [ ... ] }'
auth0 api get "clients?per_page=100&include_fields=true&fields=client_id,name"
auth0 api post "clients" --data '{ "name": "Demo App Public", "app_type": "native", ... }'
auth0 api get "client-grants?client_id=...&audience=..."
auth0 api post "client-grants" --data '{ "client_id": "...", "audience": "...", "scope": [ ... ] }'
```

## Verify the device flow (RFC 8628)

### curl

```bash
set -a; source .env.livepeer; set +a
PUB=$DEMO_APP_AUTH0_PUBLIC_CLIENT_ID
curl -s -X POST "${AUTH0_ISSUER}oauth/device/code" \
-d "client_id=$PUB" -d "audience=$DEMO_APP_AUTH0_AUDIENCE" \
-d "scope=openid sign:job offline_access"
# open verification_uri_complete, approve, then poll /oauth/token with the device_code grant.
```

### Python (livepeer-gateway)

From the [livepeer-gateway](https://github.com/livepeer/livepeer-gateway) repo (device code → cached bearer → optional `write_frames`):

```bash
uv run examples/device_login.py \
--issuer https://pymthouse.us.auth0.com \
--client-id "$DEMO_APP_AUTH0_PUBLIC_CLIENT_ID" \
--audience "$DEMO_APP_AUTH0_AUDIENCE" \
--run-frames --signer http://localhost:8081
```

Requires the clearinghouse stack (`identity-webhook`, `remote-signer`, `openmeter-collector`). Pass `--billing-url` and `--client-id` (public Auth0 client) so device login exchanges the Auth0 user token via RFC 8693 `POST …/api/v1/apps/{clientId}/oidc/token` — that upserts the OpenMeter customer (`{clientId}:{sub}`) and returns a minted signer JWT with `signer_url` / `discovery_url`.

## Limitations

- Client lookup pages up to 100 clients and matches by name (the clients endpoint has no
name filter); for very large tenants, extend the pagination loop.
- Scaffolds and updates only — it never deletes clients/grants removed from `apps.json`.
- Relies on the CLI session's permissions; reading the M2M secret needs the session to
hold `read:client_keys` (the default interactive `auth0 login` does).

## Builder API follow-up

The Go **Builder API** in `openmeter-collector` provisions Auth0 **end-users**. Re-run `./bootstrap.sh` to
ensure the **Clearinghouse Builder Management** M2M client is created and
`AUTH0_MGMT_CLIENT_ID` / `AUTH0_MGMT_CLIENT_SECRET` are written to `.env.livepeer`
(alongside the Demo App M2M used for signer-token mint).

You still need a **Credentials Exchange Action** that copies `external_user_id` and `client_id`
into minted access tokens:

```bash
./bootstrap-credentials-exchange-action.sh # idempotent; requires auth0 login
```

See [openmeter-collector/builder-api/README.md](../../openmeter-collector/builder-api/README.md) for claim details and [identity-webhook/.env.example](../../identity-webhook/.env.example) for OIDC verifier env.
34 changes: 34 additions & 0 deletions auth0-provisioner/provision/apps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"resourceServer": {
"name": "Livepeer Clearinghouse API",
"identifier": "livepeer-clearinghouse",
"signing_alg": "RS256",
"scopes": [
{ "value": "sign:job", "description": "Sign payment tickets for the Livepeer remote signer" },
{ "value": "sign:mint_user_token", "description": "Mint short-lived user signer JWTs (M2M only)" },
{ "value": "users:read", "description": "Read provisioned application users" },
{ "value": "users:write", "description": "Create, update, and deactivate application users" },
{ "value": "users:token", "description": "Issue per-user access tokens (enables per-user billing)" },
{ "value": "device:approve", "description": "RFC 8693 token exchange to bind device codes (M2M only)" },
{ "value": "admin", "description": "Administrative access to provider surfaces" }
]
},
"apps": [
{
"name": "Demo App",
"audience": "livepeer-clearinghouse",
"public": {
"grant_scopes": ["sign:job"],
"callbacks": [],
"initiate_login_uri": ""
},
"m2m": {
"grant_scopes": ["users:write", "users:token", "device:approve", "sign:job", "sign:mint_user_token"]
}
}
],
"managementClient": {
"name": "Clearinghouse Builder Management",
"grant_scopes": ["create:users", "read:users", "update:users"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Idempotent: create/deploy/bind the Credentials Exchange Action for signer JWT claims.
# Requires: auth0 login session (same as bootstrap.sh).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ACTION_NAME="Clearinghouse Signer Claims"
ACTION_CODE_FILE="${ACTION_CODE_FILE:-$SCRIPT_DIR/credentials-exchange-action.js}"
TRIGGER="credentials-exchange"

die() { printf 'error: %s\n' "$*" >&2; exit 1; }
info() { printf '%s\n' "$*" >&2; }

[ -f "$ACTION_CODE_FILE" ] || die "action code not found: $ACTION_CODE_FILE"

AUTH0_BIN="${AUTH0_BIN:-auth0}"
command -v "$AUTH0_BIN" >/dev/null 2>&1 || AUTH0_BIN="$SCRIPT_DIR/auth0"
[ -x "$AUTH0_BIN" ] || die "auth0 CLI required (run bootstrap.sh or install auth0 CLI)"

ACTION_ID="$("$AUTH0_BIN" actions list --json 2>/dev/null | jq -r --arg n "$ACTION_NAME" '.[] | select(.name == $n) | .id' | head -n1)"

if [ -z "$ACTION_ID" ]; then
info "creating action \"$ACTION_NAME\" ..."
ACTION_ID="$("$AUTH0_BIN" actions create -n "$ACTION_NAME" -t "$TRIGGER" -c "$(cat "$ACTION_CODE_FILE")" -r node22 --json | jq -r '.id')"
[ -n "$ACTION_ID" ] || die "failed to create action"
info "created action $ACTION_ID"
else
info "action \"$ACTION_NAME\" exists ($ACTION_ID)"
fi

info "deploying action ..."
"$AUTH0_BIN" actions deploy "$ACTION_ID" >/dev/null

BIND_BODY="$(jq -nc --arg id "$ACTION_ID" '{bindings:[{ref:{type:"action_id",value:$id}}]}')"
printf '%s' "$BIND_BODY" | "$AUTH0_BIN" api patch "actions/triggers/$TRIGGER/bindings" >/dev/null
info "bound action to $TRIGGER trigger"
Loading