feat(cli): lazy-resolve operator_id via billing GET /api/v1/me - #3
Closed
CryptoFewka wants to merge 2 commits into
Closed
feat(cli): lazy-resolve operator_id via billing GET /api/v1/me#3CryptoFewka wants to merge 2 commits into
CryptoFewka wants to merge 2 commits into
Conversation
keysync was originally designed around a JWT-exchange flow: POST the ovi_live_* to optimum-auth, decode the resulting JWT for operator_id, present that JWT as the Bearer on every billing call, re-exchange before expiry, handle `scope_changed` mid-reconcile. Reviewing the design against keysync's actual workload — one client per operator, hourly cron, ~4 billing calls per reconcile — the exchange pattern's wins (stateless-verify-at-scale, OAuth2-shaped ecosystem fit, scoped capability tokens) don't apply. Meanwhile its costs are real: multi-issuer JWT verification in optimum-billing, operator-key dispatch in optimum-auth, scope_version bump on revoke to invalidate outstanding JWTs, ~150 lines of CLI plumbing here. Billing's `combinedAuth` already accepts `Authorization: Bearer ovi_live_…` directly: HMAC-hashes per request, looks up `internal.operator_api_keys`, stamps role=operator. Revoking the key at the dashboard takes effect on the next request (no 1h cache window). So the direct path is materially simpler AND has faster revocation. What changes here: - `AuthClient` collapses to a credential holder. No httpx, no tenacity, no jwt.decode, no cache window, no scope_changed retry. Just (api_key, operator_id) returned on every token() call. - `KEYSYNC_OPERATOR_ID` is back as a required env var. Operators see it next to the generated key in the dashboard. The JWT model derived it from the claim; without the JWT, customers paste it alongside the key. - `KEYSYNC_AUTH_URL` is gone — no optimum-auth involvement. - `billing_client._do` drops the ScopeChangedError branch. A 401 from billing is now terminal (key was revoked / unknown / scoped to another operator). - `cli._compute_plan` / `_apply_plan` lose their re-exchange closures. The bearer is static, threaded directly. - pyjwt dropped from runtime deps; httpx + tenacity stay (still used by beacon_client + billing_client). - README rewritten to describe the direct flow. Companion changes: optimum-auth#16 and optimum-billing#63 are now unused; both will be closed. Tests: 43/43 green (was 44; one scope_changed-specific test removed, others trivialised to the new shape). ruff + mypy --strict clean. _Written with Claude Code_
KEYSYNC_OPERATOR_ID is now optional. When omitted, the CLI calls GET /api/v1/me on first contact with billing and uses the operator_id from the response. Explicit env or flag wins, so existing configs keep working unchanged. The server already knew the operator_id from the ovi_* key via combinedAuth; the /me endpoint just echoes it back. One fewer correlated value for customers to paste, no second host to configure (auth.getoptimum.io is gateway-key-only and doesn't authenticate ovi_*). Also fixes doc rot: operator_id was described as a UUID in several places, but it's TEXT holding a stringified auto-increment integer (migration 000011_integer_operator_id). The example placeholder is now "42" instead of a UUID. Depends on getoptimum/optimum-billing#71 (GET /api/v1/me) being deployed.
Contributor
Author
|
Closing alongside getoptimum/optimum-billing#71. Decision is to land The keysync-side wiring will be similar to what's here ( Branch left up ( Written with Claude Code |
CryptoFewka
added a commit
that referenced
this pull request
Jun 25, 2026
#1 desired_state._read_indices: reject a bool/float/negative/string validator_index instead of letting int() silently coerce it to a wrong index (the JSON path is the no-beacon input we steer customers to). #2 api_client.batch_register_keys: wrap int(added/skipped) so a null or non-numeric count surfaces as ApiError, not a raw exception escaping the public boundary. #3/#4 chain_id cross-check: BeaconClient.get_chain_id() reads /eth/v1/config/deposit_contract; the CLI verifies it matches the configured chain_id before computing a plan. Catches a testnet beacon paired with the mainnet default, and a wrong beacon_url (which would otherwise resolve an empty set and look like a mass exit). Keeps the convenience defaults. #5 partial-progress: batch_register_keys takes an on_progress callback invoked per committed chunk, so a mid-register failure logs what actually landed instead of 0. Adds tests for each. 107 passed, ruff and mypy clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
KEYSYNC_OPERATOR_IDbecomes optional. When omitted, the CLI callsGET /api/v1/meon first contact with billing and uses the returnedoperator_idfor subsequent URL paths. Explicit env or flag still wins, so existing customer configs keep working.BillingClient.get_me()reuses the existing tenacity/401-mapping_call. An ops-staff credential (emptyoperator_idin the response) is treated as a terminalBillingErrorwith an actionable message.operator_idwas called an "operator UUID" in the README and example values were full UUID strings. It's actuallyTEXTholding a stringified auto-increment integer (peroptimum-billingmigration000011_integer_operator_id). Example placeholders now use"42".pyproject.tomldescription rewritten — the old text still referenced the long-dead JWT-exchange model.Dependency
Requires
GET /api/v1/meto be deployed on whatever billing hostKEYSYNC_BILLING_URLpoints at. See getoptimum/optimum-billing#71. CI is unaffected — respx stubs/mein the test suite.Test plan
tests/test_billing_client.py—get_mehappy path + bearer forwarding, empty-operator →BillingError, 401 →BillingErrortests/test_config.py— missingoperator_idnow accepted (was rejected); explicit value still preservedtests/test_cli_lazy_me.py— end-to-end CliRunner:keysync showwithout--operator-idcalls/meonce then the validator list endpoint with the resolved id; explicit--operator-idskips/meentirely (verified by stubbing/mewith 500 — never called)ruff checkclean,mypy src/cleanunset KEYSYNC_OPERATOR_ID && keysync showshould log "operator_id not configured; resolving via billing /me" and then the JSONWritten with Claude Code