Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copy to .env and fill in values:
# cp .env.example .env
#
# clearinghouse-bootstrap loads .env automatically from the current directory.

# --- Auth0 (required unless --skip-auth0) ---
AUTH0_DOMAIN=your-org.us.auth0.com
AUTH0_MGMT_CLIENT_ID=your_auth0_management_m2m_client_id
AUTH0_MGMT_CLIENT_SECRET=your_auth0_management_m2m_client_secret

APP_NAME=Livepeer Clearinghouse
AUTH0_AUDIENCE=livepeer-clearinghouse

# --- Konnect Metering & Billing (required unless --skip-openmeter) ---
# Docs: https://developer.konghq.com/api/konnect/metering-and-billing/v3/
OPENMETER_URL=https://us.api.konghq.com/v3/openmeter
OPENMETER_API_KEY=kpat_your_konnect_personal_access_token
# OPENMETER_TRIAL_FEATURE_KEY=network_spend

# --- Outputs (optional) ---
# BOOTSTRAP_OUTPUT=.env.livepeer
# SDK_CONFIG_OUTPUT=sdk-config.json

# --- Config paths (optional) ---
# METERS_CONFIG_PATH=config/meters.json
# PRICING_CONFIG_PATH=config/pricing.json
25 changes: 25 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build cross-platform binaries
run: make cross
- name: Create release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: dist/*
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build output (binary at repo root)
/clearinghouse-bootstrap
/clearinghouse-bootstrap.exe
dist/

# Environment / secrets
.env
.env.livepeer
sdk-config.json

# Go
/vendor/
node_modules/
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
BIN := clearinghouse-bootstrap
CMD := ./cmd/clearinghouse-bootstrap
GOFLAGS ?=
PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64

.PHONY: build test clean cross

build:
go build $(GOFLAGS) -o $(BIN) $(CMD)

test:
go test ./... -count=1

clean:
rm -f $(BIN) dist/*

cross:
@mkdir -p dist
@for platform in $(PLATFORMS); do \
os=$${platform%/*}; \
arch=$${platform#*/}; \
ext=""; \
if [ "$$os" = "windows" ]; then ext=".exe"; fi; \
echo "Building $$os/$$arch..."; \
GOOS=$$os GOARCH=$$arch go build $(GOFLAGS) -o dist/$(BIN)-$$os-$$arch$$ext $(CMD); \
done
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# clearinghouse

A single, cross-platform Go CLI that provisions **Auth0** and
**OpenMeter/Konnect** for the Livepeer remote-signer platform and emits
`.env.livepeer` + `sdk-config.json` for
[`@pymthouse/builder-sdk`](https://github.com/pymthouse/builder-sdk).

## Quick start

```bash
make build
cp .env.example .env # fill in secrets — see file for Auth0 + Konnect vars
./clearinghouse-bootstrap

# Konnect only (no Auth0 creds in .env needed):
./clearinghouse-bootstrap --skip-auth0

# Auth0 only:
./clearinghouse-bootstrap --skip-openmeter
```

The CLI loads `.env` from the current directory automatically. See
`.env.example` for settings; run `./clearinghouse-bootstrap --help` for CLI flags.

## What it does

1. **Auth0** — creates a resource server (`livepeer-clearinghouse`, RS256, `sign:job`),
a public client (native, device_code + refresh_token), an M2M client
(client_credentials), and two client grants. Uses
[`go-auth0/v2`](https://github.com/auth0/go-auth0).

2. **OpenMeter/Konnect** — idempotently ensures meters
(`network_fee_usd_micros`, `billable_usd_micros`, `signed_ticket_count`),
features (`network_spend`, `billable_spend`), and the default pay-per-use
plan with a usage rate card. Uses the official
[`Kong/sdk-konnect-go`](https://github.com/Kong/sdk-konnect-go) SDK.

3. **Output** — writes `.env.livepeer` (Auth0 + Konnect runtime vars) and
`sdk-config.json` (structured config for Vercel platform deploy via
builder-sdk). Signer URLs in `sdk-config.json` are placeholders to update
after platform deploy.

## Configuration

Copy `.env.example` to `.env` and fill in your secrets. The CLI loads `.env`
automatically (override with `--env-file`).

If a required value is missing for the selected mode, the CLI exits with an
error before calling any APIs. See `.env.example` for variable names and
comments.

Use `--prune` to destructively remove Konnect catalog objects (meters,
features, plans) that are not defined in `config/meters.json` and
`config/pricing.json`, or whose meter dimensions no longer match config.
Prune runs before ensure/create. **This can delete production billing
catalog data** — use only when you intend to reconcile the tenant to config.

## Config files

Meter and pricing definitions live in `config/meters.json` and
`config/pricing.json`. These control which meters, features, and plans are
bootstrapped.

## Cross-compilation

```bash
make cross # builds linux/darwin/windows on amd64/arm64 into dist/
```

Tagged releases (`v*`) are built and published via GitHub Actions
(`.github/workflows/release.yml`).

## Follow-ups (out of scope for this PR)

- Per-customer provisioning (customers + subscriptions)
- Self-hosted OpenMeter adapter
- Benthos collector / Docker Compose stack
- Railway deploy scripts
171 changes: 171 additions & 0 deletions cmd/clearinghouse-bootstrap/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strings"

"github.com/livepeer/clearinghouse/internal/admin"
"github.com/livepeer/clearinghouse/internal/auth0"
"github.com/livepeer/clearinghouse/internal/config"
"github.com/livepeer/clearinghouse/internal/meters"
"github.com/livepeer/clearinghouse/internal/output"
"github.com/livepeer/clearinghouse/internal/pricing"
)

func main() {
args, envFile, envFileExplicit, help, err := config.PreprocessArgs(os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
config.PrintUsage(false)
os.Exit(1)
}
if help != config.HelpNone {
config.PrintUsage(help == config.HelpAll)
os.Exit(0)
}

if err := config.LoadEnvFile(envFile, envFileExplicit); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if envFileExplicit || fileExists(envFile) {
log.Printf("Loaded config from %s", envFile)
}

cfg, err := config.Parse(args)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
config.PrintUsage(false)
os.Exit(1)
}

ctx := context.Background()

var auth0Result *auth0.ProvisionResult
if !cfg.SkipAuth0 {
log.Println("Provisioning Auth0...")
auth0Result, err = auth0.Provision(ctx, auth0.ProvisionConfig{
Domain: cfg.Auth0Domain,
MgmtClientID: cfg.Auth0MgmtClientID,
MgmtClientSecret: cfg.Auth0MgmtClientSecret,
AppName: cfg.AppName,
APIAudience: cfg.APIAudience,
})
if err != nil {
log.Fatalf("Auth0 provisioning failed: %v", err)
}
log.Printf("Auth0 provisioned: API=%s PublicClient=%s M2M=%s",
auth0Result.APIIdentifier, auth0Result.PublicClientID, auth0Result.M2MClientID)
} else {
log.Println("Skipping Auth0 (--skip-auth0)")
}

if !cfg.SkipOpenMeter {
log.Println("Bootstrapping OpenMeter/Konnect catalog...")

meterCfg, err := meters.Load(cfg.MetersConfigPath)
if err != nil {
log.Fatalf("Failed to load meters config: %v", err)
}
pricingCfg, err := pricing.Load(cfg.PricingConfigPath)
if err != nil {
log.Fatalf("Failed to load pricing config: %v", err)
}

omAdmin := admin.CreateAdmin(cfg.OpenmeterURL, cfg.OpenmeterAPIKey)
result, err := admin.BootstrapCatalog(ctx, omAdmin, meterCfg, pricingCfg, cfg.TrialFeatureKey, admin.BootstrapOptions{
Prune: cfg.Prune,
})
if err != nil {
log.Fatalf("OpenMeter bootstrap failed: %v", err)
}
printBootstrapResult(result)
} else {
log.Println("Skipping OpenMeter (--skip-openmeter)")
}

envContent := output.BuildEnvFile(cfg, auth0Result)
if err := os.WriteFile(cfg.OutputPath, []byte(envContent), 0644); err != nil {
log.Fatalf("Failed to write %s: %v", cfg.OutputPath, err)
}
log.Printf("Wrote %s", cfg.OutputPath)

if auth0Result != nil {
sdkJSON, err := output.BuildSDKConfig(cfg, auth0Result)
if err != nil {
log.Fatalf("Failed to build sdk-config.json: %v", err)
}
if err := os.WriteFile(cfg.SDKConfigOutputPath, append(sdkJSON, '\n'), 0644); err != nil {
log.Fatalf("Failed to write %s: %v", cfg.SDKConfigOutputPath, err)
}
log.Printf("Wrote %s", cfg.SDKConfigOutputPath)
}

log.Println("Bootstrap complete.")
}

func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

func printBootstrapResult(r *admin.BootstrapResult) {
if r.Prune != nil {
for _, key := range r.Prune.DeletedPlans {
log.Printf(" pruned plan: %s", key)
}
for _, key := range r.Prune.DeletedFeatures {
log.Printf(" pruned feature: %s", key)
}
for _, key := range r.Prune.DeletedMeters {
log.Printf(" pruned meter: %s", key)
}
for _, w := range r.Prune.Warnings {
log.Printf(" prune warning: %s", w)
}
}
for _, m := range r.Meters {
action := "exists"
if m.Created {
action = "created"
}
msg := fmt.Sprintf(" meter %s: %s", m.Resource.Key, action)
if len(m.Warnings) > 0 {
msg += " [" + strings.Join(m.Warnings, "; ") + "]"
}
log.Println(msg)
}
for _, f := range r.Features {
action := "exists"
if f.Created {
action = "created"
}
msg := fmt.Sprintf(" feature %s: %s", f.Resource.Key, action)
if len(f.Warnings) > 0 {
msg += " [" + strings.Join(f.Warnings, "; ") + "]"
}
log.Println(msg)
}
if r.Plan != nil {
action := "exists"
if r.Plan.Created {
action = "created"
}
log.Printf(" plan %s: %s", r.Plan.Resource.Key, action)
}
if r.PlanSkippedReason != "" {
log.Printf(" plan skipped: %s", r.PlanSkippedReason)
}

summary := map[string]string{
"planKey": r.PlanKey,
"billableFeatureKey": r.BillableFeatureKey,
"trialIncludedMicros": r.TrialIncludedMicros,
}
b, _ := json.MarshalIndent(summary, " ", " ")
log.Printf(" pricing: %s", string(b))
}
34 changes: 34 additions & 0 deletions config/meters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"createSignedTicketEventType": "create_signed_ticket",
"signedTicketEventSource": "go-livepeer-remote-signer",
"defaultTrialFeatureKey": "network_spend",
"defaultBillableFeatureKey": "billable_spend",
"networkFeeUsdMicrosMeter": "network_fee_usd_micros",
"billableUsdMicrosMeter": "billable_usd_micros",
"signedTicketCountMeter": "signed_ticket_count",
"dimensions": {
"client_id": "$.client_id",
"external_user_id": "$.external_user_id",
"pipeline": "$.pipeline",
"model_id": "$.model_id"
},
"meters": {
"networkFeeUsdMicros": {
"openmeterDescription": "Livepeer signed-ticket network fee (USD micros) — SUM of signer computed_fee_usd_micros; grouped by client, user, pipeline, model",
"konnectName": "Network fee (USD micros)",
"konnectDescription": "Livepeer signed-ticket network fee (USD micros) — sum of signer computed_fee_usd_micros; grouped by client, user, pipeline, model",
"valueProperty": "$.network_fee_usd_micros"
},
"billableUsdMicros": {
"openmeterDescription": "Billable usage (USD micros) after pipeline/model markup — SUM of billable_usd_micros; grouped by client, user, pipeline, model",
"konnectName": "Billable usage (USD micros)",
"konnectDescription": "Post-markup billable amount in USD micros for pay-per-use billing",
"valueProperty": "$.billable_usd_micros"
},
"signedTicketCount": {
"openmeterDescription": "Signed ticket count per user",
"konnectName": "Signed ticket count",
"konnectDescription": "Signed ticket count per user"
}
}
}
11 changes: 11 additions & 0 deletions config/pricing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"defaultTrialIncludedUsdMicros": "5000000",
"defaultPlanKey": "clearinghouse_default_ppu",
"billableFeatureKey": "billable_spend",
"billableMeterKey": "billable_usd_micros",
"unitPriceUsdPerBillableMicro": "0.000001",
"markupRules": [
{ "pipeline": "*", "model_id": "*", "markupPercent": 0 },
{ "pipeline": "live-video-to-video", "model_id": "*", "markupPercent": 15 }
]
}
Loading