Files
zeekay 11c160189c ui-customization: first Hanzo Base-native Go service binary
The architectural reference for console's tRPC->ZAP migration. Replaces the
in-process tRPC UiCustomization router with a standalone Go binary built on
Hanzo Base (embedded encrypted SQLite + plugins) exposing a typed ZAP
capability-RPC interface. 11+ services follow this pattern.

- proto/ui-customization.zap: canonical schema (zap-spec dialect), source of
  truth, compiled to Go via zapgen (make zap-gen, idempotent).
- server/: ZAP RPC handler at msgType 200. Three decoupled wire layers
  (transport / envelope / typed payload); capability carried as opaque bytes.
  Single auth chokepoint (Kind + PermSessionRead bitmask); signature verify
  gated on a wired issuer registry (TODO -> SPEC.md 2.3). Get + GetModules
  read from the ui_customization Base collection. Promise-pipelining via an
  await/resolve table.
- main.go: base.New() + vault (per-org encrypted SQLite) + ZAP router.
  HTTP :8090 (Base health/metrics), ZAP :9999.
- cmd/probe/: out-of-process smoke probe.

Tests: 4/4 pass (Get, GetModules, permission-denied, pipelining proof showing
the dependent call ships before the first answer resolves). Pure-Go
(CGO_ENABLED=0); zap-gen + build reproducible. No Prisma/Postgres/Mongo/tRPC.
2026-06-15 12:31:11 -07:00

149 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Command ui-customization is the first Hanzo Base-native Go service binary:
// a typed ZAP capability-RPC backend for UI customization, built on Hanzo Base
// (embedded SQLite + plugins). It replaces console's in-process tRPC router.
//
// Architecture (the reference pattern 11+ more services follow):
//
// base.New() → Base app: embedded SQLite, hooks, migrations
// ├── vault (optional) → per-org encrypted SQLite shard (KEK)
// ├── zap.MustRegister → generic ORM transport (msgType 100–103)
// └── server.Register(node) → THIS service's typed router (msgType 200)
// apis.NewRouter(app) → sidecar HTTP (health/metrics), NOT app data
// app.Start() → serves HTTP :8090 + ZAP :9999
//
// The .zap schema (proto/) is the source of truth; gen/ is its Go projection.
package main
import (
"context"
"crypto/rand"
"log"
"os"
"github.com/hanzoai/base"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/plugins/vault"
"github.com/hanzoai/base/tools/hook"
luxlog "github.com/luxfi/log"
zaplib "github.com/luxfi/zap"
zcap "github.com/zap-proto/go/cap"
"github.com/hanzoai/ui-customization/server"
)
func main() {
app := base.New()
var zapAddr string
app.RootCmd.PersistentFlags().StringVar(&zapAddr, "zap", envOr("ZAP_ADDR", "127.0.0.1:9999"),
"address for the typed ZAP capability-RPC listener")
var defaultOrg string
app.RootCmd.PersistentFlags().StringVar(&defaultOrg, "org", envOr("UI_CUSTOMIZATION_ORG", "default"),
"default organization scope when a capability carries no org binding")
var vaultDir string
app.RootCmd.PersistentFlags().StringVar(&vaultDir, "vaultDir", os.Getenv("VAULT_DIR"),
"directory for per-org encrypted SQLite shards (enables the vault plugin)")
app.RootCmd.ParseFlags(os.Args[1:])
// Optional: per-org encrypted SQLite backing via the vault plugin. Enabled
// only when --vaultDir is set so local dev stays single-file SQLite. The
// master KEK comes from KMS in production; a process-ephemeral key is used
// when VAULT_MASTER_KEY is unset (dev only — shards won't persist across
// restarts, which is correct for throwaway dev data).
if vaultDir != "" {
vault.MustRegister(app, vault.Config{
Enabled: true,
DataDir: vaultDir,
OrgID: defaultOrg,
MasterKey: masterKey(),
})
}
// Ensure the ui_customization collection + seed the default org row.
server.RegisterCollections(app, defaultOrg)
// Stand up the typed ZAP router alongside Base's serve lifecycle. We run a
// dedicated luxfi/zap node for the capability RPC (NoDiscovery: direct dial
// only — service discovery is the gateway's job, not mDNS here).
logger := luxlog.New("component", "ui-customization")
node := zaplib.NewNode(zaplib.NodeConfig{
NodeID: "ui-customization",
Port: portOf(zapAddr),
NoDiscovery: true,
})
// Verifier: bootstrap (ed25519, no issuer registry → Kind+Permissions
// enforced, signature step skipped). Wire IssuerKey to the IAM pubkey
// registry to enable full cryptographic verification.
srv := server.NewServer(app, logger, defaultOrg, zcap.Verifier{})
srv.Register(node)
app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
Id: "uiCustomizationZapNode",
Func: func(e *core.ServeEvent) error {
if err := e.Next(); err != nil {
return err
}
if err := node.Start(); err != nil {
return err
}
logger.Info("ui-customization ZAP router listening", "addr", zapAddr, "msgType", server.MsgTypeRouterBase)
return nil
},
})
app.OnTerminate().Bind(&hook.Handler[*core.TerminateEvent]{
Id: "uiCustomizationZapNodeStop",
Func: func(e *core.TerminateEvent) error {
node.Stop()
return e.Next()
},
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
// masterKey returns the 32-byte vault master KEK: from VAULT_MASTER_KEY (hex
// or raw 32 bytes) in production, else a process-ephemeral random key for dev.
func masterKey() []byte {
if v := os.Getenv("VAULT_MASTER_KEY"); len(v) >= 32 {
return []byte(v)[:32]
}
k := make([]byte, 32)
_, _ = rand.Read(k)
return k
}
func envOr(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
// portOf extracts the port from a host:port address, defaulting to 9999.
func portOf(addr string) int {
for i := len(addr) - 1; i >= 0; i-- {
if addr[i] == ':' {
p := 0
for _, c := range addr[i+1:] {
if c < '0' || c > '9' {
return 9999
}
p = p*10 + int(c-'0')
}
if p == 0 {
return 9999
}
return p
}
}
return 9999
}
var _ = context.Background // reserved for future graceful-shutdown wiring