4 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 55aa586615 a product states which of its own routes a publishable key reaches
What such a key may reach was a list of addresses kept in the rule that
reads it. The list held Base's own routes and nothing else, so bootnode's
twelve reads — which it registers precisely to be reachable that way, and
guards with a read-only refusal beside each of its eleven writes —
answered 403 wherever the plugin was on.

A list of one package's addresses kept in another goes stale the moment
that package mounts a route, and it goes stale silently. So the answer
moves to the route: apis.Public declares one where it is registered, and
the rule holds no addresses at all. Bootnode declares its twelve reads;
Base declares its records, rest, files and health reads. The rule keeps
one address of its own, the public submit path, because whether it is
public is the collection's statement and not the route's.

A route needs to know its whole address for that, so the mux records it
as it mounts: Route.Pattern is the method, the group prefixes and the
path, the same string a request reports having matched.

Second layer, restored: bootnode read ReadOnly off the identity headers,
which the org plugin writes identically for every key it resolves — so a
publishable key arrived as a person and no read-only refusal could fire.
The headers say who; the credential says what kind.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 06:55:07 -07:00
zeekayandHanzo Dev 89adc2a014 one reader answers what credential a request carries
A rule that decides what a credential may reach and a door that resolves
it agree only if they read the same bytes. Three readers each accepted a
different subset of the spellings: the rule read `Bearer ` case-sensitively
from one header, the auth door matched the scheme without regard to case
across three headers and accepted a bare token, bootnode read X-API-Key
first. Past the /v1/iam and /v1/idv mounts — which forward headers and
query verbatim with the key doors unbound — the rule is the only thing
standing, so a spelling it did not read was a key the issuer authenticated.

apis.Credential is that one reader: the Bearer scheme matched without
regard to case and separated by spaces or tabs, a bare token, the four
headers in order, then the `key` query parameter. A value naming another
scheme presents no token, so a Basic blob is not compared against key
prefixes. The query is split on `;` as well as `&`, because this answer
decides what gets forwarded and the far side may split either way.

bootnode classifies by the credential rather than by which header carried
it — the same token was a JWT under one header and unsupported under
another — and a shape it does not recognise is IAM's to judge.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 06:47:32 -07:00
zeekay 054d65663a they are Bases, one per org — the word tenant is gone
Base's own registry was already org-native in every method — OrgDB(orgID),
HasOrg(orgID), orgDir(orgID) — and only the type names said tenant. So this is
the vocabulary catching up with the code, not a change of behaviour.

	TenantRegistry -> Bases          app.Bases().OrgDB(org)
	TenantConfig   -> BasesConfig
	MultiTenant    -> PerOrg         it enables per-org databases; say that
	MULTI_TENANT   -> MULTI_BASE
	core/tenant.go -> core/bases.go

Tenant was the wrong noun twice over: it is the multi-tenancy word, and this
product's unit is the thing a customer creates per org — a Base. Identity here
is org, user, project, and nothing else.

The UI never said it; this was Go-side only. 64 packages green.

One test was collateral and is fixed rather than renamed: TestToHTTPHeader
asserts a pure snake_case-to-Header-Case transform, so rewriting its EXPECTED
value while its input stayed "tenant_id" made it assert something false. The case
is dropped — org_id already covers that transform.
2026-08-10 18:23:40 -07:00
zeekayandGitHub 7dcecb01c9 feat(bootnode): Go port foundation as a Base plugin (5 modules end-to-end) (#15)
Why: consolidate the Python bootnode backend (bootnode/api/, ~100 .py files)
onto Hanzo Base, leveraging Base's IAM client and per-org/per-user tenant
infrastructure instead of reimplementing auth, sessions, and multi-tenancy.
This is the structural foundation + the 5 most-important modules, not the whole
port.

What landed
- plugins/bootnode/: blockchain developer platform mounted under /v1.
  Five modules ported end-to-end:
    1. auth   — multi-network OAuth2 callback (lux/pars/zoo/hanzo share the
                lux-web3 IAM app; client id derived from redirect_uri) + bn_
                project API keys (salted SHA-256, raw key shown once, verified
                in constant time). Accepts IAM JWTs and pk-/sk-/hk- keys by
                reusing github.com/hanzoai/base/iam — no IAM logic duplicated.
    2. team   — org/member CRUD scoped to the caller's project; invited emails
                resolved against IAM (active) or held pending with an invite
                token.
    3. networks — applies bootno.de/v1 Network CRs (white-label brand, tier,
                region, validator fleet). Replaces the Python's kubectl + raw
                nginx-Ingress templating with a declarative CR for the
                bootno.de operator to reconcile.
    4. nodes  — applies bootno.de/v1 NodeFleet CRs (CRD-driven cloud path;
                the Python docker provider was a local-dev concern).
    5. keys   — applies bootno.de/v1 KMSSecret CRs by KMS path. NO plaintext
                key material ever touches this service; the request and
                response carry none, and a guard rejects any private-key field.
- plugins/bootnode/kube/: dependency-free Kubernetes REST client (net/http
  server-side apply). No client-go, no CGO. In-cluster SA or KUBE_APISERVER.
- plugins/commerce/: typed Hanzo Commerce (Square billing) client behind a
  Client interface. bootnode depends on the interface; commerce never depends
  on bootnode/iam.
- 11 SQLAlchemy models -> Base collections (models/collections.go). No `users`
  collection: IAM owns identity; bootnode references IAM user ids as text.
  OrgCluster is the canonical org->k8s-cluster mapping.
- examples/base/main.go: platform plugin now runs PrincipalIsolation="sqlite"
  (per-org + per-user encrypted SQLite); bootnode registered (BOOTNODE_ENABLED).

Modules pending (15, tracked in PR body): chat, zap, billing-http, bundler,
fleets, gas, infra, launch, lux, mpc, nfts, observability, rpc, tokens,
transfers, wallets, webhooks-http. (chains is also ported as a bonus 6th.)

Tests (20 functions, all green incl -race)
- auth: token classification, key gen/hash/verify (tamper + wrong-salt),
  redirect->clientId derivation.
- kube: server-side-apply shape (PATCH + apply-patch+yaml + fieldManager),
  error propagation, 404 get/delete idempotency.
- commerce: disabled no-op, get-or-create (create + existing), usage error
  propagation, immediate cancel.
- workers: HMAC-signed delivery, non-2xx-is-failure, unreachable-is-failure.
- bootnode: full end-to-end against a fake IAM + fake apiserver — /me 401 then
  200, project + bn_ key, team invite/list, Network + NodeFleet CR apply/get,
  KMSSecret plaintext-rejection + apply + status, public chains. Plus
  fail-fast on the insecure default salt against production IAM.

Verified: `go build ./...` exits 0; `go test -race ./plugins/bootnode/...
./plugins/commerce/...` passes; live binary serves /v1/chains (200) and gates
/v1/auth/me, /v1/networks (401); per-org SQLite isolation logged active; all
10 _bootnode_ collections created on boot.

Co-authored-by: zeekay <[email protected]>
2026-06-18 17:16:15 -07:00