mirror of
https://github.com/hanzoai/ui-customization.git
synced 2026-10-30 18:18:42 +00:00
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.
117 lines
4.7 KiB
Go
117 lines
4.7 KiB
Go
package server
|
|
|
|
import (
|
|
"os"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tools/hook"
|
|
)
|
|
|
|
// CollectionName is the single Base collection backing UiCustomization data.
|
|
// One row per org (keyed by `org`); the service scopes reads to the org in
|
|
// the verified capability. Backed by Base's encrypted SQLite (vault plugin
|
|
// KEK per org) when vault is registered; plain SQLite otherwise.
|
|
const CollectionName = "ui_customization"
|
|
|
|
// Field names mirror the Prisma/tRPC model the old uiCustomizationRouter read.
|
|
// `present` is the entitlement flag (the wire model of tRPC's `null`).
|
|
const (
|
|
fOrg = "org"
|
|
fPresent = "present"
|
|
fHostname = "hostname"
|
|
fDocumentationHref = "documentationHref"
|
|
fSupportHref = "supportHref"
|
|
fFeedbackHref = "feedbackHref"
|
|
fLogoLightModeHref = "logoLightModeHref"
|
|
fLogoDarkModeHref = "logoDarkModeHref"
|
|
fDefaultModelAdapter = "defaultModelAdapter"
|
|
fDefaultBaseUrlOpenAI = "defaultBaseUrlOpenAI"
|
|
fDefaultBaseUrlAnthropic = "defaultBaseUrlAnthropic"
|
|
fDefaultBaseUrlAzure = "defaultBaseUrlAzure"
|
|
fVisibleModules = "visibleModules" // JSON array of strings
|
|
)
|
|
|
|
// RegisterCollections ensures the ui_customization collection exists and seeds
|
|
// the default org row from env on first boot. Idempotent: re-running finds the
|
|
// existing collection/row and no-ops. Wired via OnBootstrap so it runs once at
|
|
// startup, before the ZAP listener accepts calls.
|
|
func RegisterCollections(app core.App, defaultOrg string) {
|
|
app.OnBootstrap().Bind(&hook.Handler[*core.BootstrapEvent]{
|
|
Id: "uiCustomizationCollections",
|
|
Func: func(e *core.BootstrapEvent) error {
|
|
// Run the rest of bootstrap first so the DB is initialized.
|
|
if err := e.Next(); err != nil {
|
|
return err
|
|
}
|
|
if err := EnsureCollection(app); err != nil {
|
|
return err
|
|
}
|
|
return SeedDefaultRow(app, defaultOrg)
|
|
},
|
|
})
|
|
}
|
|
|
|
// EnsureCollection creates the ui_customization collection if absent. Exported
|
|
// so callers that bootstrap the app themselves (tests, one-shot migrations)
|
|
// can provision it directly rather than via the OnBootstrap hook. Idempotent.
|
|
func EnsureCollection(app core.App) error {
|
|
if _, err := app.FindCollectionByNameOrId(CollectionName); err == nil {
|
|
return nil // already exists
|
|
}
|
|
|
|
col := core.NewBaseCollection(CollectionName)
|
|
col.Fields.Add(&core.TextField{Name: fOrg, Required: true, Max: 255})
|
|
col.Fields.Add(&core.BoolField{Name: fPresent})
|
|
for _, name := range []string{
|
|
fHostname, fDocumentationHref, fSupportHref, fFeedbackHref,
|
|
fLogoLightModeHref, fLogoDarkModeHref, fDefaultModelAdapter,
|
|
fDefaultBaseUrlOpenAI, fDefaultBaseUrlAnthropic, fDefaultBaseUrlAzure,
|
|
} {
|
|
col.Fields.Add(&core.TextField{Name: name, Max: 2048})
|
|
}
|
|
col.Fields.Add(&core.JSONField{Name: fVisibleModules, MaxSize: 65536})
|
|
col.Fields.Add(&core.AutodateField{Name: "created", OnCreate: true})
|
|
col.Fields.Add(&core.AutodateField{Name: "updated", OnCreate: true, OnUpdate: true})
|
|
|
|
// Unique index on org: one customization row per organization.
|
|
col.AddIndex("idx_ui_customization_org", true, fOrg, "")
|
|
|
|
return app.Save(col)
|
|
}
|
|
|
|
// SeedDefaultRow writes the org's customization from env, mirroring the values
|
|
// the tRPC `get` procedure read directly from env. Skipped if a row for the org
|
|
// already exists (config lives in the DB after first boot). Exported alongside
|
|
// EnsureCollection for direct provisioning.
|
|
func SeedDefaultRow(app core.App, org string) error {
|
|
col, err := app.FindCollectionByNameOrId(CollectionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := app.FindFirstRecordByFilter(col, "org = {:org}", map[string]any{"org": org}); err == nil {
|
|
return nil // already seeded
|
|
}
|
|
|
|
rec := core.NewRecord(col)
|
|
rec.Set(fOrg, org)
|
|
// present mirrors the self-host-ui-customization entitlement. The plan
|
|
// gate lived in console; here it's a stored flag seeded from env so the
|
|
// service is self-contained. Default false (not entitled) unless set.
|
|
rec.Set(fPresent, env("HANZO_UI_PRESENT") == "true")
|
|
rec.Set(fHostname, env("HANZO_UI_API_HOST"))
|
|
rec.Set(fDocumentationHref, env("HANZO_UI_DOCUMENTATION_HREF"))
|
|
rec.Set(fSupportHref, env("HANZO_UI_SUPPORT_HREF"))
|
|
rec.Set(fFeedbackHref, env("HANZO_UI_FEEDBACK_HREF"))
|
|
rec.Set(fLogoLightModeHref, env("HANZO_UI_LOGO_LIGHT_MODE_HREF"))
|
|
rec.Set(fLogoDarkModeHref, env("HANZO_UI_LOGO_DARK_MODE_HREF"))
|
|
rec.Set(fDefaultModelAdapter, env("HANZO_UI_DEFAULT_MODEL_ADAPTER"))
|
|
rec.Set(fDefaultBaseUrlOpenAI, env("HANZO_UI_DEFAULT_BASE_URL_OPENAI"))
|
|
rec.Set(fDefaultBaseUrlAnthropic, env("HANZO_UI_DEFAULT_BASE_URL_ANTHROPIC"))
|
|
rec.Set(fDefaultBaseUrlAzure, env("HANZO_UI_DEFAULT_BASE_URL_AZURE"))
|
|
rec.Set(fVisibleModules, visibleModulesFromEnv())
|
|
|
|
return app.Save(rec)
|
|
}
|
|
|
|
func env(k string) string { return os.Getenv(k) }
|