Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e78da8ddec | ||
|
|
dbe5dad2f8 | ||
|
|
d77e554b7e | ||
|
|
e075f7a438 | ||
|
|
cea54a96b0 | ||
|
|
ae8c0cea98 | ||
|
|
14bda07881 | ||
|
|
4de40e2396 | ||
|
|
7f1e0d61df | ||
|
|
18738d56b2 | ||
|
|
942feca4d7 | ||
|
|
ad7828553e | ||
|
|
08a6ef73e7 | ||
|
|
4e17df928d | ||
|
|
223fc137f3 | ||
|
|
8ca298873c | ||
|
|
b90e629308 | ||
|
|
7876d86241 | ||
|
|
91970431f3 | ||
|
|
50add91a84 | ||
|
|
ea1a699d9e | ||
|
|
10ad830e10 | ||
|
|
58bb1843a4 | ||
|
|
bb2600bd9b | ||
|
|
5c6beb850b | ||
|
|
1e1e017ee4 | ||
|
|
991ec0e788 | ||
|
|
8b59797b59 | ||
|
|
e67bd8aa6d | ||
|
|
ee879c028b | ||
|
|
e0680a7030 | ||
|
|
952a5112b3 | ||
|
|
d60e476ea8 | ||
|
|
66574461da |
@@ -0,0 +1,41 @@
|
||||
# VCS
|
||||
.git/
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# CI / repo metadata not needed inside the build
|
||||
.github/
|
||||
|
||||
# Docs (image runs the binary; readers visit GitHub)
|
||||
*.md
|
||||
LICENSE
|
||||
SECURITY.md
|
||||
|
||||
# Already-built binary at repo root (matches .gitignore)
|
||||
/cloud
|
||||
|
||||
# Environment files (never bake secrets into images)
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# IDE / editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Tests stay out of the runtime image
|
||||
*_test.go
|
||||
|
||||
# Local build outputs
|
||||
/dist/
|
||||
/build/
|
||||
/bin/
|
||||
|
||||
# The Dockerfile itself doesn't need to be in the context it builds
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
@@ -0,0 +1,87 @@
|
||||
name: release
|
||||
|
||||
# Builds and pushes ghcr.io/hanzoai/cloud:<tag> when a git tag matching v*
|
||||
# is pushed. Also publishes :sha-<short> and :latest for the default branch.
|
||||
#
|
||||
# Notes for maintainers:
|
||||
# - The cloud binary depends on private upstream modules
|
||||
# (hanzoai/iam, hanzoai/commerce, hanzoai/gateway, etc). The default
|
||||
# GITHUB_TOKEN issued by Actions does NOT have read access across
|
||||
# private repos in other orgs. If that's the case here, configure an
|
||||
# org-level secret HANZO_GH_RO_TOKEN with read:packages + repo scope
|
||||
# covering hanzoai/* and luxfi/*, and replace the GOPRIVATE step's
|
||||
# token reference.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure private module access for Go (build args)
|
||||
id: gomod-token
|
||||
run: |
|
||||
# Prefer an org-level token if configured (covers cross-org
|
||||
# private deps); fall back to the default workflow token.
|
||||
if [ -n "${{ secrets.HANZO_GH_RO_TOKEN }}" ]; then
|
||||
echo "token=${{ secrets.HANZO_GH_RO_TOKEN }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "token=${{ secrets.GITHUB_TOKEN }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix=sha-,format=short
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
secrets: |
|
||||
"gh_token=${{ steps.gomod-token.outputs.token }}"
|
||||
# If the Dockerfile needs the token at build-time for `go mod
|
||||
# download` of private modules, set the BuildKit secret above
|
||||
# and reference it in the Dockerfile with:
|
||||
# RUN --mount=type=secret,id=gh_token \
|
||||
# git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/" && \
|
||||
# GOPRIVATE='github.com/hanzoai/*,github.com/luxfi/*' go mod download
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Built binary (matches Dockerfile output path)
|
||||
/cloud
|
||||
|
||||
# Local build directories
|
||||
/dist/
|
||||
/build/
|
||||
/bin/
|
||||
|
||||
# Go test + coverage artifacts
|
||||
*.test
|
||||
*.out
|
||||
coverage.txt
|
||||
coverage.html
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDE / editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
+20
-1
@@ -1,7 +1,26 @@
|
||||
FROM golang:1.26-alpine AS build
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
RUN apk add --no-cache ca-certificates tzdata git
|
||||
RUN addgroup -g 65532 -S nonroot && adduser -u 65532 -S nonroot -G nonroot
|
||||
WORKDIR /src
|
||||
# Private cross-org subsystem modules (hanzoai/*, luxfi/*, zap-proto/*) are
|
||||
# fetched via authenticated git. GOSUMDB=off + GOPROXY=direct tolerate
|
||||
# force-re-tagged luxfi/hanzoai modules (committed go.sum is source of truth);
|
||||
# gh_token is the shared docker-build.yml BuildKit secret (no-op when absent).
|
||||
ENV GOPRIVATE=github.com/hanzoai/*,github.com/luxfi/*,github.com/zap-proto/* \
|
||||
GOSUMDB=off \
|
||||
GOPROXY=direct \
|
||||
GOFLAGS=-mod=mod
|
||||
COPY go.mod go.sum ./
|
||||
# go mod download, self-healing past upstream force-re-tag poisoning: if a
|
||||
# luxfi/hanzoai module's tag was force-moved/deleted after go.sum was recorded,
|
||||
# the committed sum mismatches the fresh origin fetch. With GOSUMDB=off the
|
||||
# regenerated sum is trustworthy for our own private modules. Root-cause fix is
|
||||
# stopping force-re-tags at the source; this keeps the image buildable meanwhile.
|
||||
RUN --mount=type=secret,id=gh_token \
|
||||
if [ -s /run/secrets/gh_token ]; then \
|
||||
git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/"; \
|
||||
fi && \
|
||||
(go mod download || (echo ">> go.sum poisoned by upstream force-re-tag; regenerating from origin" && rm -f go.sum && go mod download))
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /cloud ./cmd/cloud
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# hanzoai/cloud — developer ergonomics for the unified Hanzo Cloud binary (HIP-0106).
|
||||
# Targets are intentionally minimal; deploy artifacts (compose, helm) live in deploy/ and helm/.
|
||||
|
||||
GO ?= go
|
||||
BIN ?= cloud
|
||||
PKG ?= ./cmd/cloud
|
||||
DOCKER_IMAGE ?= ghcr.io/hanzoai/cloud
|
||||
DOCKER_TAG ?= dev
|
||||
LDFLAGS ?= -s -w
|
||||
|
||||
.PHONY: help build run smoke test vet tidy docker docker-push clean
|
||||
|
||||
help: ## Show this help.
|
||||
@awk 'BEGIN{FS=":.*##";printf "\nUsage: make <target>\n\nTargets:\n"} /^[a-zA-Z_-]+:.*##/{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
build: ## Build the unified cloud binary into ./bin/cloud.
|
||||
@mkdir -p bin
|
||||
$(GO) build -ldflags="$(LDFLAGS)" -o bin/$(BIN) $(PKG)
|
||||
|
||||
run: build ## Run with iam,base,kms,gateway,o11y enabled (matches README quickstart).
|
||||
./bin/$(BIN) --enable=iam,base,kms,gateway,o11y --brand=hanzo --domain=api.hanzo.ai
|
||||
|
||||
smoke: ## Build and run cmd/cloud-smoke (mount-time integration check).
|
||||
$(GO) run ./cmd/cloud-smoke
|
||||
|
||||
test: ## Run unit + integration tests.
|
||||
$(GO) test ./...
|
||||
|
||||
vet: ## go vet across the module.
|
||||
$(GO) vet ./...
|
||||
|
||||
tidy: ## go mod tidy + verify go.sum.
|
||||
$(GO) mod tidy
|
||||
$(GO) mod verify
|
||||
|
||||
docker: ## Build the Docker image (uses repo Dockerfile, scratch final stage).
|
||||
docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||
|
||||
docker-push: docker ## Push the Docker image to ghcr.io. Requires docker login.
|
||||
docker push $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
|
||||
clean: ## Remove built artifacts.
|
||||
rm -rf bin
|
||||
@@ -63,6 +63,9 @@ type disabledCommerce struct{}
|
||||
func (disabledCommerce) GetTenantConfig(_ context.Context, _ string) (*types.TenantConfig, error) {
|
||||
return nil, &disabledErr{"commerce"}
|
||||
}
|
||||
func (disabledCommerce) CheckEntitlement(_ context.Context, _, _ string) (*types.LicenseEntitlement, error) {
|
||||
return nil, &disabledErr{"commerce"}
|
||||
}
|
||||
|
||||
type disabledAI struct{}
|
||||
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// Package gojahost runs a Hanzo Node service's goja bundle (a self-contained,
|
||||
// ESM-free JS file exposing globalThis.handle(req)) inside the unified cloud
|
||||
// binary, per HIP-0106.
|
||||
//
|
||||
// It is the SHARED glue used by clients/plansvc and clients/pricingsvc to host
|
||||
// @hanzo/plans and @hanzo/pricing in-process via dop251/goja — the same engine
|
||||
// base/plugins/gojavm uses. We do not import base's gojavm Runtime directly
|
||||
// because that loader is manifest-driven (extension.json + a single exported
|
||||
// `fn` over JSON-over-the-wire payloads); our services instead inject a catalog
|
||||
// of JSON globals at VM init and call a richer handle({route,params,...}) entry.
|
||||
// The VM-pool + compile-once + per-runtime ensureLoaded discipline here mirrors
|
||||
// gojavm/runtime.go exactly so behavior and the pool semantics are identical.
|
||||
//
|
||||
// Module boundary: the JS bundle + catalog data live in the service repos
|
||||
// (hanzoai/plans, hanzoai/pricing) and are passed in by the caller. This
|
||||
// package carries zero service logic — only the engine plumbing.
|
||||
package gojahost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// defaultPoolSize mirrors gojavm's default. Override via CLOUD_GOJAHOST_POOL_SIZE.
|
||||
const defaultPoolSize = 8
|
||||
|
||||
// Request is the dispatch envelope handed to globalThis.handle in JS.
|
||||
type Request struct {
|
||||
Route string `json:"route"`
|
||||
Params map[string]string `json:"params,omitempty"`
|
||||
Query map[string]string `json:"query,omitempty"`
|
||||
Tenant string `json:"tenant,omitempty"`
|
||||
}
|
||||
|
||||
// Response is what globalThis.handle returns: an HTTP status + an opaque body
|
||||
// that the host serializes straight to JSON.
|
||||
type Response struct {
|
||||
Status int `json:"status"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
}
|
||||
|
||||
// Host is a compiled service bundle plus a pool of goja runtimes that have had
|
||||
// the bundle + the injected globals evaluated. Safe for concurrent use.
|
||||
type Host struct {
|
||||
name string
|
||||
program *goja.Program
|
||||
globals map[string]any
|
||||
pool []*slot
|
||||
factory func() *goja.Runtime
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
type slot struct {
|
||||
mu sync.Mutex
|
||||
busy bool
|
||||
vm *goja.Runtime
|
||||
loaded bool
|
||||
hasFunc bool
|
||||
}
|
||||
|
||||
// Config configures a Host.
|
||||
type Config struct {
|
||||
// Name identifies the service for error messages ("plans", "pricing").
|
||||
Name string
|
||||
// Bundle is the goja bundle source (goja/bundle.js from the service repo).
|
||||
Bundle []byte
|
||||
// Globals are injected onto each runtime before the bundle runs, e.g.
|
||||
// {"__PLANS_DATA__": <catalog>}. Values are converted via goja.ToValue.
|
||||
// Pointers to the same Go value are shared read-only across runtimes; the
|
||||
// bundles never mutate injected globals.
|
||||
Globals map[string]any
|
||||
}
|
||||
|
||||
// New compiles the bundle and pre-warms the runtime pool. The bundle is
|
||||
// compiled once (goja.Program is safe to share across runtimes); each pool
|
||||
// runtime evaluates it lazily on first use.
|
||||
func New(cfg Config) (*Host, error) {
|
||||
if cfg.Name == "" {
|
||||
return nil, errors.New("gojahost: Config.Name required")
|
||||
}
|
||||
if len(cfg.Bundle) == 0 {
|
||||
return nil, fmt.Errorf("gojahost[%s]: empty bundle", cfg.Name)
|
||||
}
|
||||
prog, err := goja.Compile(cfg.Name+"/bundle.js", string(cfg.Bundle), true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gojahost[%s]: compile: %w", cfg.Name, err)
|
||||
}
|
||||
|
||||
size := defaultPoolSize
|
||||
if v := os.Getenv("CLOUD_GOJAHOST_POOL_SIZE"); v != "" {
|
||||
if n, e := strconv.Atoi(v); e == nil {
|
||||
size = n
|
||||
}
|
||||
}
|
||||
if size < 1 {
|
||||
size = 1
|
||||
}
|
||||
|
||||
h := &Host{
|
||||
name: cfg.Name,
|
||||
program: prog,
|
||||
globals: cfg.Globals,
|
||||
factory: func() *goja.Runtime { return goja.New() },
|
||||
pool: make([]*slot, size),
|
||||
}
|
||||
for i := range h.pool {
|
||||
h.pool[i] = &slot{vm: h.factory()}
|
||||
}
|
||||
|
||||
// Eagerly load + validate one runtime so misconfiguration (bad bundle,
|
||||
// missing handle export) fails at Mount, not at first request.
|
||||
if err := h.withSlot(func(s *slot) error {
|
||||
if err := h.ensure(s); err != nil {
|
||||
return err
|
||||
}
|
||||
if !s.hasFunc {
|
||||
return fmt.Errorf("gojahost[%s]: bundle does not define globalThis.handle", cfg.Name)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// ensure evaluates the bundle on a runtime exactly once (installing the
|
||||
// injected globals first), then records whether globalThis.handle exists.
|
||||
func (h *Host) ensure(s *slot) error {
|
||||
if s.loaded {
|
||||
return nil
|
||||
}
|
||||
for k, v := range h.globals {
|
||||
if err := s.vm.Set(k, v); err != nil {
|
||||
return fmt.Errorf("gojahost[%s]: set global %s: %w", h.name, k, err)
|
||||
}
|
||||
}
|
||||
// Minimal node-ish shims the bundles may touch. The bundles are written
|
||||
// to avoid console, but defensively wire a no-op console so a stray
|
||||
// console.* never throws ReferenceError.
|
||||
installConsole(s.vm)
|
||||
|
||||
if _, err := s.vm.RunProgram(h.program); err != nil {
|
||||
return fmt.Errorf("gojahost[%s]: run bundle: %w", h.name, err)
|
||||
}
|
||||
_, s.hasFunc = goja.AssertFunction(s.vm.Get("handle"))
|
||||
s.loaded = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dispatch calls globalThis.handle(req) on a pooled runtime and returns the
|
||||
// JS-side {status, body}. ctx cancellation interrupts the call.
|
||||
func (h *Host) Dispatch(ctx context.Context, req Request) (*Response, error) {
|
||||
h.mu.Lock()
|
||||
if h.closed {
|
||||
h.mu.Unlock()
|
||||
return nil, fmt.Errorf("gojahost[%s]: closed", h.name)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *Response
|
||||
err := h.withSlot(func(s *slot) error {
|
||||
if err := h.ensure(s); err != nil {
|
||||
return err
|
||||
}
|
||||
fn, ok := goja.AssertFunction(s.vm.Get("handle"))
|
||||
if !ok {
|
||||
return fmt.Errorf("gojahost[%s]: globalThis.handle missing", h.name)
|
||||
}
|
||||
|
||||
// Watchdog: interrupt the VM if ctx cancels mid-call.
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.vm.Interrupt(ctx.Err())
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
arg := s.vm.ToValue(map[string]any{
|
||||
"route": req.Route,
|
||||
"params": toAnyMap(req.Params),
|
||||
"query": toAnyMap(req.Query),
|
||||
"tenant": req.Tenant,
|
||||
})
|
||||
out, callErr := fn(goja.Undefined(), arg)
|
||||
if callErr != nil {
|
||||
var iex *goja.InterruptedError
|
||||
if errors.As(callErr, &iex) {
|
||||
if ce := ctx.Err(); ce != nil {
|
||||
return ce
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("gojahost[%s]: handle(%s): %w", h.name, req.Route, callErr)
|
||||
}
|
||||
|
||||
// The JS returns {status:number, body:any}. Pull them out and
|
||||
// re-marshal body to canonical JSON bytes via the export view.
|
||||
exported := out.Export()
|
||||
m, ok := exported.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("gojahost[%s]: handle(%s) returned %T, want object", h.name, req.Route, exported)
|
||||
}
|
||||
status := 200
|
||||
if sv, ok := m["status"]; ok {
|
||||
if f, ok := sv.(int64); ok {
|
||||
status = int(f)
|
||||
} else if f, ok := sv.(float64); ok {
|
||||
status = int(f)
|
||||
}
|
||||
}
|
||||
bodyBytes, mErr := json.Marshal(m["body"])
|
||||
if mErr != nil {
|
||||
return fmt.Errorf("gojahost[%s]: marshal body: %w", h.name, mErr)
|
||||
}
|
||||
resp = &Response{Status: status, Body: bodyBytes}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Eval runs an arbitrary JS expression against a pooled runtime (bundle
|
||||
// already loaded) and returns the exported Go value. Used by callers that
|
||||
// want to invoke a non-route helper the bundle exposes (e.g. applyMarkup).
|
||||
func (h *Host) Eval(ctx context.Context, fnName string, jsonArg []byte) ([]byte, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []byte
|
||||
err := h.withSlot(func(s *slot) error {
|
||||
if err := h.ensure(s); err != nil {
|
||||
return err
|
||||
}
|
||||
fn, ok := goja.AssertFunction(s.vm.Get(fnName))
|
||||
if !ok {
|
||||
return fmt.Errorf("gojahost[%s]: globalThis.%s is not a function", h.name, fnName)
|
||||
}
|
||||
var arg any
|
||||
if len(jsonArg) > 0 {
|
||||
if err := json.Unmarshal(jsonArg, &arg); err != nil {
|
||||
return fmt.Errorf("gojahost[%s]: %s arg not JSON: %w", h.name, fnName, err)
|
||||
}
|
||||
}
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.vm.Interrupt(ctx.Err())
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
res, callErr := fn(goja.Undefined(), s.vm.ToValue(arg))
|
||||
if callErr != nil {
|
||||
var iex *goja.InterruptedError
|
||||
if errors.As(callErr, &iex) {
|
||||
if ce := ctx.Err(); ce != nil {
|
||||
return ce
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("gojahost[%s]: %s: %w", h.name, fnName, callErr)
|
||||
}
|
||||
b, mErr := json.Marshal(res.Export())
|
||||
if mErr != nil {
|
||||
return fmt.Errorf("gojahost[%s]: marshal %s result: %w", h.name, fnName, mErr)
|
||||
}
|
||||
out = b
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// SetGlobal updates an injected global and forces every pooled runtime to
|
||||
// re-evaluate the bundle on next use (so the new value takes effect). Used by
|
||||
// the pricing sync path to swap in freshly-synced data.
|
||||
func (h *Host) SetGlobal(key string, value any) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.globals == nil {
|
||||
h.globals = map[string]any{}
|
||||
}
|
||||
h.globals[key] = value
|
||||
for _, s := range h.pool {
|
||||
s.mu.Lock()
|
||||
s.loaded = false // re-run bundle with new globals on next ensure
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Close drops the pool.
|
||||
func (h *Host) Close() error {
|
||||
h.mu.Lock()
|
||||
h.closed = true
|
||||
h.pool = nil
|
||||
h.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// withSlot borrows a free pool slot; if all are busy it spins up a one-off
|
||||
// runtime (matching gojavm's saturation fallback).
|
||||
func (h *Host) withSlot(call func(*slot) error) error {
|
||||
h.mu.Lock()
|
||||
pool := h.pool
|
||||
h.mu.Unlock()
|
||||
|
||||
for _, s := range pool {
|
||||
s.mu.Lock()
|
||||
if s.busy {
|
||||
s.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
s.busy = true
|
||||
s.mu.Unlock()
|
||||
|
||||
err := call(s)
|
||||
|
||||
s.mu.Lock()
|
||||
s.busy = false
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
// Saturated: ephemeral runtime, fully loaded fresh.
|
||||
tmp := &slot{vm: h.factory()}
|
||||
return call(tmp)
|
||||
}
|
||||
|
||||
func toAnyMap(m map[string]string) map[string]any {
|
||||
if m == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
out := make(map[string]any, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// installConsole wires a no-op console.{log,info,warn,error,debug} so guest
|
||||
// code that logs does not throw. goja has no console by default.
|
||||
func installConsole(vm *goja.Runtime) {
|
||||
if !goja.IsUndefined(vm.Get("console")) {
|
||||
return
|
||||
}
|
||||
noop := func(goja.FunctionCall) goja.Value { return goja.Undefined() }
|
||||
console := vm.NewObject()
|
||||
for _, m := range []string{"log", "info", "warn", "error", "debug", "trace"} {
|
||||
_ = console.Set(m, noop)
|
||||
}
|
||||
_ = vm.Set("console", console)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package gojahost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const echoBundle = `
|
||||
(function(){
|
||||
globalThis.handle = function(req){
|
||||
if (req.route === 'boom') throw new Error('kaboom');
|
||||
if (req.route === 'notfound') return { status: 404, body: { error: 'nope' } };
|
||||
return { status: 200, body: { route: req.route, tenant: req.tenant, params: req.params, data: globalThis.__X__ } };
|
||||
};
|
||||
globalThis.dbl = function(n){ return n * 2; };
|
||||
})();
|
||||
`
|
||||
|
||||
func newTestHost(t *testing.T) *Host {
|
||||
t.Helper()
|
||||
h, err := New(Config{
|
||||
Name: "test",
|
||||
Bundle: []byte(echoBundle),
|
||||
Globals: map[string]any{"__X__": map[string]any{"k": "v"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func TestDispatch_OK(t *testing.T) {
|
||||
h := newTestHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), Request{Route: "ping", Tenant: "acme", Params: map[string]string{"id": "7"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
if resp.Status != 200 {
|
||||
t.Fatalf("status = %d, want 200", resp.Status)
|
||||
}
|
||||
var body struct {
|
||||
Route string `json:"route"`
|
||||
Tenant string `json:"tenant"`
|
||||
Params map[string]string `json:"params"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body, &body); err != nil {
|
||||
t.Fatalf("unmarshal %q: %v", resp.Body, err)
|
||||
}
|
||||
if body.Route != "ping" || body.Tenant != "acme" || body.Params["id"] != "7" || body.Data["k"] != "v" {
|
||||
t.Fatalf("unexpected body: %+v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatch_Status404(t *testing.T) {
|
||||
h := newTestHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), Request{Route: "notfound"})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
if resp.Status != 404 {
|
||||
t.Fatalf("status = %d, want 404", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatch_JSThrowIsError(t *testing.T) {
|
||||
h := newTestHost(t)
|
||||
defer h.Close()
|
||||
if _, err := h.Dispatch(context.Background(), Request{Route: "boom"}); err == nil {
|
||||
t.Fatal("expected error from thrown JS exception")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_RejectsBundleWithoutHandle(t *testing.T) {
|
||||
_, err := New(Config{Name: "x", Bundle: []byte(`globalThis.notHandle = 1;`)})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when bundle has no globalThis.handle")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEval_Helper(t *testing.T) {
|
||||
h := newTestHost(t)
|
||||
defer h.Close()
|
||||
out, err := h.Eval(context.Background(), "dbl", []byte(`21`))
|
||||
if err != nil {
|
||||
t.Fatalf("Eval: %v", err)
|
||||
}
|
||||
if string(out) != "42" {
|
||||
t.Fatalf("Eval dbl(21) = %s, want 42", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGlobal_TakesEffect(t *testing.T) {
|
||||
h := newTestHost(t)
|
||||
defer h.Close()
|
||||
h.SetGlobal("__X__", map[string]any{"k": "updated"})
|
||||
resp, err := h.Dispatch(context.Background(), Request{Route: "ping"})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
var body struct {
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(resp.Body, &body)
|
||||
if body.Data["k"] != "updated" {
|
||||
t.Fatalf("global not updated: %+v", body.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatch_ContextCancel(t *testing.T) {
|
||||
h, err := New(Config{
|
||||
Name: "loop",
|
||||
Bundle: []byte(`globalThis.handle = function(){ var x=0; while(true){ x=(x+1)|0; Math.sin(x); } };`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
defer h.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
if _, err := h.Dispatch(ctx, Request{Route: "x"}); err == nil {
|
||||
t.Fatal("expected ctx error from infinite loop")
|
||||
}
|
||||
if time.Since(start) > 2*time.Second {
|
||||
t.Fatalf("interrupt too slow: %v", time.Since(start))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Package plansvc mounts the @hanzo/plans catalog into the unified cloud
|
||||
// binary under /v1/plans/*, per HIP-0106.
|
||||
//
|
||||
// STRATEGY: wrap, don't rewrite. @hanzo/plans is a Node data package (JSON
|
||||
// catalog + entitlements.mjs transforms). We do NOT reimplement the entitlement
|
||||
// vocabulary in Go and we do NOT copy the catalog into cloud. Instead:
|
||||
//
|
||||
// - github.com/hanzoai/plans (the service repo's Go embed module) ships
|
||||
// goja/bundle.js — the ESM-free port of entitlements.mjs + the /v1/plans
|
||||
// route table — plus the embedded *.json catalog (plans.Data()).
|
||||
// - This wrapper loads that bundle into a goja runtime (clients/gojahost),
|
||||
// injects the catalog as globalThis.__PLANS_DATA__, and registers thin zip
|
||||
// handlers that call globalThis.handle({route, params, tenant}). The
|
||||
// entitlement transforms (fromLegacy/toLicenseFeatures/resolvePlan) run in
|
||||
// goja — real JS, not a Go reimplementation.
|
||||
//
|
||||
// The plans data is read-only public-catalog content; there are no secrets
|
||||
// here. The licensing SIGNER/fingerprint that consumes toLicenseFeatures stays
|
||||
// in hanzoai/licensing. This wrapper is pure glue.
|
||||
//
|
||||
// IAM gating + X-Org-Id tenant scope: every /v1/plans route reads the
|
||||
// gateway-minted identity off the zip.Ctx (c.Org()) and threads it into the
|
||||
// bundle as the tenant, so a reseller org (tenant_id != "hanzo") sees its own
|
||||
// catalog overrides. The plan catalog is readable by any authenticated caller;
|
||||
// no admin scope is required for reads.
|
||||
package plansvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/cloud/clients/gojahost"
|
||||
hplans "github.com/hanzoai/plans"
|
||||
"github.com/hanzoai/zip"
|
||||
)
|
||||
|
||||
// host is the process-global goja host for the plans bundle. nil before Mount.
|
||||
var host *gojahost.Host
|
||||
|
||||
// Mount registers the /v1/plans/* surface on app per HIP-0106.
|
||||
func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
if app == nil {
|
||||
return fmt.Errorf("plansvc.Mount: nil zip.App")
|
||||
}
|
||||
logger := deps.Logger
|
||||
if logger == nil {
|
||||
return fmt.Errorf("plansvc.Mount: nil deps.Logger")
|
||||
}
|
||||
logger = logger.New("subsystem", "plans")
|
||||
|
||||
bundle, err := hplans.Bundle()
|
||||
if err != nil {
|
||||
return fmt.Errorf("plansvc.Mount: load bundle: %w", err)
|
||||
}
|
||||
data, err := hplans.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("plansvc.Mount: load catalog: %w", err)
|
||||
}
|
||||
|
||||
h, err := gojahost.New(gojahost.Config{
|
||||
Name: "plans",
|
||||
Bundle: bundle,
|
||||
Globals: map[string]any{"__PLANS_DATA__": data},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("plansvc.Mount: goja host: %w", err)
|
||||
}
|
||||
host = h
|
||||
|
||||
// Native health endpoint — always answers, no JS, no auth.
|
||||
app.Get("/v1/plans/health", func(c *zip.Ctx) error {
|
||||
return c.JSON(http.StatusOK, map[string]any{"status": "ok", "service": "plans"})
|
||||
})
|
||||
|
||||
// Fixed-route handlers. Each maps a path to a bundle route name.
|
||||
// gateway-minted identity (c.Org()) becomes the tenant for catalog scoping.
|
||||
type binding struct{ path, route string }
|
||||
fixed := []binding{
|
||||
{"/v1/plans", "plans"},
|
||||
{"/v1/plans/subscriptions", "subscriptions"},
|
||||
{"/v1/plans/cloud", "cloud"},
|
||||
{"/v1/plans/blockchain", "blockchain"},
|
||||
{"/v1/plans/dns", "dns"},
|
||||
{"/v1/plans/gpu", "gpu"},
|
||||
{"/v1/plans/regions", "regions"},
|
||||
{"/v1/plans/storage", "storage"},
|
||||
{"/v1/plans/tools", "tools"},
|
||||
{"/v1/plans/policy", "policy"},
|
||||
{"/v1/plans/schema", "schema"},
|
||||
{"/v1/plans/vocab", "vocab"},
|
||||
}
|
||||
for _, b := range fixed {
|
||||
route := b.route
|
||||
app.Get(b.path, func(c *zip.Ctx) error {
|
||||
return dispatch(c, route, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Parameterized: resolve + entitlements take a plan id.
|
||||
app.Get("/v1/plans/resolve/:id", func(c *zip.Ctx) error {
|
||||
return dispatch(c, "resolve", map[string]string{"id": c.Param("id")})
|
||||
})
|
||||
app.Get("/v1/plans/entitlements/:id", func(c *zip.Ctx) error {
|
||||
return dispatch(c, "entitlements", map[string]string{"id": c.Param("id")})
|
||||
})
|
||||
|
||||
logger.Info("plans mounted",
|
||||
"prefix", "/v1/plans",
|
||||
"routes", len(fixed)+2,
|
||||
"brand", deps.Brand,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatch runs one bundle route on the shared goja host and writes the
|
||||
// {status, body} back as JSON. The tenant is the gateway-minted org (X-Org-Id
|
||||
// per HIP-0026) so reseller catalogs resolve correctly.
|
||||
func dispatch(c *zip.Ctx, route string, params map[string]string) error {
|
||||
if host == nil {
|
||||
return c.JSON(http.StatusServiceUnavailable, map[string]any{
|
||||
"error": "plans not initialised",
|
||||
})
|
||||
}
|
||||
tenant := c.Org()
|
||||
if tenant == "" {
|
||||
tenant = "hanzo"
|
||||
}
|
||||
resp, err := host.Dispatch(c.Context(), gojahost.Request{
|
||||
Route: route,
|
||||
Params: params,
|
||||
Tenant: tenant,
|
||||
})
|
||||
if err != nil {
|
||||
c.Log().Error("plans dispatch failed", "route", route, "err", err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"error": "plans dispatch failed",
|
||||
})
|
||||
}
|
||||
return c.Bytes(resp.Status, withContentType(c, resp.Body))
|
||||
}
|
||||
|
||||
// withContentType sets application/json and returns the bytes unchanged.
|
||||
func withContentType(c *zip.Ctx, b []byte) []byte {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
return b
|
||||
}
|
||||
|
||||
func init() {
|
||||
cloud.Register("plans", 111, func(app any, deps cloud.Deps) error {
|
||||
a, ok := app.(*zip.App)
|
||||
if !ok {
|
||||
return fmt.Errorf("plansvc.Mount: app is %T, want *zip.App", app)
|
||||
}
|
||||
return Mount(a, deps)
|
||||
})
|
||||
}
|
||||
|
||||
// Shutdown drops the goja host. Idempotent.
|
||||
func Shutdown(context.Context) error {
|
||||
if host == nil {
|
||||
return nil
|
||||
}
|
||||
err := host.Close()
|
||||
host = nil
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package plansvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/cloud/clients/gojahost"
|
||||
hplans "github.com/hanzoai/plans"
|
||||
)
|
||||
|
||||
// newHost loads the REAL @hanzo/plans goja bundle + embedded catalog, so this
|
||||
// test exercises the actual entitlements.mjs port running in goja.
|
||||
func newHost(t *testing.T) *gojahost.Host {
|
||||
t.Helper()
|
||||
bundle, err := hplans.Bundle()
|
||||
if err != nil {
|
||||
t.Fatalf("Bundle: %v", err)
|
||||
}
|
||||
data, err := hplans.Data()
|
||||
if err != nil {
|
||||
t.Fatalf("Data: %v", err)
|
||||
}
|
||||
h, err := gojahost.New(gojahost.Config{
|
||||
Name: "plans",
|
||||
Bundle: bundle,
|
||||
Globals: map[string]any{"__PLANS_DATA__": data},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("gojahost.New: %v", err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func TestPlans_Vocab(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "vocab", Tenant: "hanzo"})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
var body struct {
|
||||
Namespaces []string `json:"namespaces"`
|
||||
Keys map[string]any `json:"keys"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body, &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(body.Namespaces) != 9 {
|
||||
t.Fatalf("namespaces = %d, want 9", len(body.Namespaces))
|
||||
}
|
||||
if len(body.Keys) < 40 {
|
||||
t.Fatalf("entitlement keys = %d, want >=40", len(body.Keys))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlans_ResolveProducesLicenseFeatures(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "resolve", Tenant: "hanzo", Params: map[string]string{"id": "pro"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
if resp.Status != 200 {
|
||||
t.Fatalf("status = %d, want 200 (body=%s)", resp.Status, resp.Body)
|
||||
}
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Entitlements map[string]any `json:"entitlements"`
|
||||
LicenseFeatures []string `json:"license_features"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body, &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if body.ID != "pro" || body.TenantID != "hanzo" {
|
||||
t.Fatalf("id/tenant = %q/%q", body.ID, body.TenantID)
|
||||
}
|
||||
if len(body.Entitlements) == 0 {
|
||||
t.Fatal("expected non-empty entitlements for pro")
|
||||
}
|
||||
if body.LicenseFeatures == nil {
|
||||
t.Fatal("expected license_features array (the engine gate input)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlans_Resolve404(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "resolve", Tenant: "hanzo", Params: map[string]string{"id": "does-not-exist"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
if resp.Status != 404 {
|
||||
t.Fatalf("status = %d, want 404", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlans_TenantScopingFallsBackToHanzo(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
// A reseller with no overrides sees the hanzo default catalog.
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "subscriptions", Tenant: "acme-reseller"})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
var body struct {
|
||||
Plans []map[string]any `json:"plans"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body, &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(body.Plans) == 0 {
|
||||
t.Fatal("reseller should fall back to hanzo default catalog (non-empty)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package pricingsvc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
hplans "github.com/hanzoai/plans"
|
||||
)
|
||||
|
||||
// loadPlansCatalog returns the @hanzo/plans catalog the pricing bundle reads
|
||||
// for its subscription/blockchain/policy/tools/gpu endpoints. Sourced from the
|
||||
// plans embed module so cloud has ONE copy of the plan catalog feeding both
|
||||
// /v1/plans/* (plansvc) and /v1/pricing/{subscriptions,blockchain,…} (here).
|
||||
func loadPlansCatalog() (map[string]any, error) {
|
||||
return hplans.Data()
|
||||
}
|
||||
|
||||
// parseFloatEnv reads a float env var with a default (markup knobs).
|
||||
func parseFloatEnv(key string, dflt float64) float64 {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return dflt
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// Package pricingsvc mounts the @hanzo/pricing service into the unified cloud
|
||||
// binary under /v1/pricing/* (+ the /v1/models, /v1/gpu, /v1/tools aliases),
|
||||
// per HIP-0106.
|
||||
//
|
||||
// HONEST GOJA STATUS: @hanzo/pricing is an EXPRESS app. Express needs Node's
|
||||
// http/net stack and CANNOT run in goja. So the Express *transport* is dropped
|
||||
// and replaced by native zip routes; the pricing *handlers* (pure transforms
|
||||
// over data/pricing.json + the @hanzo/plans catalog) run in goja via the
|
||||
// goja/bundle.js shipped by github.com/hanzoai/pricing. The sync.mjs MARKUP
|
||||
// logic (toMTok/roundPrice/processOpenRouterModel/…) also runs in goja through
|
||||
// the bundle's applyMarkup(); the only thing that does NOT run in goja is the
|
||||
// live network fetch (OpenRouter/HuggingFace — no fetch/AbortController in
|
||||
// goja), which this wrapper performs with Go's net/http and then feeds the raw
|
||||
// JSON into applyMarkup. See SyncEnabled.
|
||||
//
|
||||
// Module boundary: pricing source + markup logic live in hanzoai/pricing. This
|
||||
// wrapper is glue. No pricing data or markup math is reimplemented in Go.
|
||||
//
|
||||
// IAM gating + X-Org-Id: read endpoints are open to any authenticated caller
|
||||
// (the public pricing catalog). The sync trigger is admin-only (c.IsAdmin()).
|
||||
package pricingsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/cloud/clients/gojahost"
|
||||
hpricing "github.com/hanzoai/pricing"
|
||||
"github.com/hanzoai/zip"
|
||||
)
|
||||
|
||||
var host *gojahost.Host
|
||||
|
||||
// Mount registers the pricing surface on app per HIP-0106.
|
||||
func Mount(app *zip.App, deps cloud.Deps) error {
|
||||
if app == nil {
|
||||
return fmt.Errorf("pricingsvc.Mount: nil zip.App")
|
||||
}
|
||||
logger := deps.Logger
|
||||
if logger == nil {
|
||||
return fmt.Errorf("pricingsvc.Mount: nil deps.Logger")
|
||||
}
|
||||
logger = logger.New("subsystem", "pricing")
|
||||
|
||||
bundle, err := hpricing.Bundle()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pricingsvc.Mount: load bundle: %w", err)
|
||||
}
|
||||
pricingData, err := hpricing.Pricing()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pricingsvc.Mount: load pricing.json: %w", err)
|
||||
}
|
||||
plansExtra, err := hpricing.PlansExtra()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pricingsvc.Mount: load plans-extra: %w", err)
|
||||
}
|
||||
// The pricing bundle also reads the @hanzo/plans catalog for the
|
||||
// subscription/blockchain/policy/tools/gpu endpoints. We pull that from the
|
||||
// plans embed module so both subsystems share ONE source of truth.
|
||||
plansData, err := loadPlansCatalog()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pricingsvc.Mount: load plans catalog: %w", err)
|
||||
}
|
||||
|
||||
h, err := gojahost.New(gojahost.Config{
|
||||
Name: "pricing",
|
||||
Bundle: bundle,
|
||||
Globals: map[string]any{
|
||||
"__PRICING_DATA__": pricingData,
|
||||
"__PLANS_EXTRA__": plansExtra,
|
||||
"__PLANS_DATA__": plansData,
|
||||
"__MARKUP__": map[string]any{
|
||||
"thirdParty": parseFloatEnv("THIRD_PARTY_MARKUP", 1.0),
|
||||
"computeMonthly": parseFloatEnv("COMPUTE_MARKUP_MONTHLY", 1.0),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pricingsvc.Mount: goja host: %w", err)
|
||||
}
|
||||
host = h
|
||||
|
||||
app.Get("/v1/pricing/health", func(c *zip.Ctx) error {
|
||||
return c.JSON(http.StatusOK, map[string]any{"status": "ok", "service": "pricing"})
|
||||
})
|
||||
|
||||
// /v1/pricing/* read surface (mirrors server.mjs handler-for-handler).
|
||||
type binding struct{ path, route string }
|
||||
fixed := []binding{
|
||||
{"/v1/pricing", "pricing"},
|
||||
{"/v1/pricing/models", "models"},
|
||||
{"/v1/pricing/summary", "summary"},
|
||||
{"/v1/pricing/free", "free"},
|
||||
{"/v1/pricing/featured", "featured"},
|
||||
{"/v1/pricing/compute", "compute"},
|
||||
{"/v1/pricing/compute/presets", "compute/presets"},
|
||||
{"/v1/pricing/cloud", "cloud"},
|
||||
{"/v1/pricing/cloud/plans", "cloud/plans"},
|
||||
{"/v1/pricing/cloud/regions", "cloud/regions"},
|
||||
{"/v1/pricing/cloud/storage", "cloud/storage"},
|
||||
{"/v1/pricing/providers", "providers"},
|
||||
{"/v1/pricing/subscriptions", "subscriptions"},
|
||||
{"/v1/pricing/blockchain", "blockchain"},
|
||||
{"/v1/pricing/iam", "iam"},
|
||||
{"/v1/pricing/base", "base"},
|
||||
{"/v1/pricing/paas", "paas"},
|
||||
{"/v1/pricing/policy", "policy"},
|
||||
{"/v1/pricing/tools", "tools"},
|
||||
{"/v1/pricing/gpu", "gpu"},
|
||||
}
|
||||
for _, b := range fixed {
|
||||
route := b.route
|
||||
app.Get(b.path, func(c *zip.Ctx) error { return dispatch(c, route, nil) })
|
||||
}
|
||||
|
||||
// Single model lookup.
|
||||
app.Get("/v1/pricing/model/:name", func(c *zip.Ctx) error {
|
||||
return dispatch(c, "model", map[string]string{"name": c.Param("name")})
|
||||
})
|
||||
|
||||
// Convenience aliases (the cleaner top-level surface from server.mjs).
|
||||
app.Get("/v1/models", func(c *zip.Ctx) error { return dispatch(c, "__models_alias", nil) })
|
||||
app.Get("/v1/pricing-policy", func(c *zip.Ctx) error { return dispatch(c, "policy", nil) })
|
||||
// NOTE: /v1/plans, /v1/tools, /v1/gpu, /v1/cloud, /v1/subscriptions, /v1/iam
|
||||
// are owned by plansvc / other subsystems at the top level to avoid route
|
||||
// collisions; pricing serves them under its /v1/pricing/* prefix.
|
||||
|
||||
// Live sync trigger — admin only. Network fetch in Go, markup in goja.
|
||||
app.Post("/v1/pricing/sync", func(c *zip.Ctx) error {
|
||||
if !c.IsAdmin() {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]any{"error": "admin required"})
|
||||
}
|
||||
updated, err := RunSync(c.Context())
|
||||
if err != nil {
|
||||
c.Log().Error("pricing sync failed", "err", err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]any{"error": "sync failed", "message": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{"status": "ok", "updated": updated})
|
||||
})
|
||||
|
||||
logger.Info("pricing mounted",
|
||||
"prefix", "/v1/pricing",
|
||||
"routes", len(fixed)+4,
|
||||
"express", false,
|
||||
"goja", true,
|
||||
"brand", deps.Brand,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dispatch(c *zip.Ctx, route string, params map[string]string) error {
|
||||
if host == nil {
|
||||
return c.JSON(http.StatusServiceUnavailable, map[string]any{"error": "pricing not initialised"})
|
||||
}
|
||||
tenant := c.Org()
|
||||
if tenant == "" {
|
||||
tenant = "hanzo"
|
||||
}
|
||||
resp, err := host.Dispatch(c.Context(), gojahost.Request{Route: route, Params: params, Tenant: tenant})
|
||||
if err != nil {
|
||||
c.Log().Error("pricing dispatch failed", "route", route, "err", err)
|
||||
return c.JSON(http.StatusInternalServerError, map[string]any{"error": "pricing dispatch failed"})
|
||||
}
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
return c.Bytes(resp.Status, resp.Body)
|
||||
}
|
||||
|
||||
// RunSync performs the live third-party model sync: fetch upstream listings
|
||||
// (network — Go's net/http, since goja has no fetch), run the markup transform
|
||||
// in goja via the bundle's applyMarkup(), and swap the shaped third-party
|
||||
// section into the served catalog. Returns an ISO timestamp.
|
||||
//
|
||||
// This is the HONEST split: network IO in Go, markup math in JS. Only the
|
||||
// dynamic third-party section is refreshed here; the Zen catalog + cloud/DO
|
||||
// pricing in sync.mjs need the zen-gateway + DO credentials and stay on the
|
||||
// standalone sync path for now.
|
||||
func RunSync(ctx context.Context) (string, error) {
|
||||
if host == nil {
|
||||
return "", fmt.Errorf("pricing not initialised")
|
||||
}
|
||||
raw := map[string]any{}
|
||||
|
||||
// OpenRouter — public, no auth.
|
||||
if or, err := fetchJSON(ctx, "https://openrouter.ai/api/v1/models"); err == nil {
|
||||
if m, ok := or.(map[string]any); ok {
|
||||
raw["openrouter"] = m["data"]
|
||||
}
|
||||
}
|
||||
// (HuggingFace router needs a token; left to the standalone path unless
|
||||
// HF_TOKEN is wired. We still call applyMarkup with whatever we fetched so
|
||||
// the markup logic runs in goja.)
|
||||
|
||||
rawJSON, _ := json.Marshal(raw)
|
||||
shapedJSON, err := host.Eval(ctx, "applyMarkup", rawJSON)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("applyMarkup: %w", err)
|
||||
}
|
||||
var shaped map[string]any
|
||||
if err := json.Unmarshal(shapedJSON, &shaped); err != nil {
|
||||
return "", fmt.Errorf("decode shaped: %w", err)
|
||||
}
|
||||
// Merge the freshly-shaped third-party section into the served catalog and
|
||||
// re-inject so subsequent reads see it.
|
||||
cur, _ := hpricing.Pricing()
|
||||
if m, ok := cur.(map[string]any); ok {
|
||||
if v, ok := shaped["thirdPartyModels"]; ok {
|
||||
m["thirdPartyModels"] = v
|
||||
}
|
||||
if v, ok := shaped["providers"]; ok {
|
||||
m["providers"] = v
|
||||
}
|
||||
if v, ok := shaped["freeModels"]; ok {
|
||||
m["freeModels"] = v
|
||||
}
|
||||
ts := time.Now().UTC().Format(time.RFC3339)
|
||||
m["updated"] = ts
|
||||
host.SetGlobal("__PRICING_DATA__", m)
|
||||
return ts, nil
|
||||
}
|
||||
return time.Now().UTC().Format(time.RFC3339), nil
|
||||
}
|
||||
|
||||
func fetchJSON(ctx context.Context, url string) (any, error) {
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(cctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s: status %d", url, resp.StatusCode)
|
||||
}
|
||||
b, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
cloud.Register("pricing", 112, func(app any, deps cloud.Deps) error {
|
||||
a, ok := app.(*zip.App)
|
||||
if !ok {
|
||||
return fmt.Errorf("pricingsvc.Mount: app is %T, want *zip.App", app)
|
||||
}
|
||||
return Mount(a, deps)
|
||||
})
|
||||
}
|
||||
|
||||
// Shutdown drops the goja host. Idempotent.
|
||||
func Shutdown(context.Context) error {
|
||||
if host == nil {
|
||||
return nil
|
||||
}
|
||||
err := host.Close()
|
||||
host = nil
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package pricingsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/cloud/clients/gojahost"
|
||||
hplans "github.com/hanzoai/plans"
|
||||
hpricing "github.com/hanzoai/pricing"
|
||||
)
|
||||
|
||||
// newHost loads the REAL @hanzo/pricing goja bundle + embedded catalogs, so
|
||||
// this exercises the actual server.mjs handler port + sync.mjs markup port
|
||||
// running in goja (Express dropped).
|
||||
func newHost(t *testing.T) *gojahost.Host {
|
||||
t.Helper()
|
||||
bundle, err := hpricing.Bundle()
|
||||
if err != nil {
|
||||
t.Fatalf("Bundle: %v", err)
|
||||
}
|
||||
pricingData, err := hpricing.Pricing()
|
||||
if err != nil {
|
||||
t.Fatalf("Pricing: %v", err)
|
||||
}
|
||||
plansExtra, err := hpricing.PlansExtra()
|
||||
if err != nil {
|
||||
t.Fatalf("PlansExtra: %v", err)
|
||||
}
|
||||
plansData, err := hplans.Data()
|
||||
if err != nil {
|
||||
t.Fatalf("plans.Data: %v", err)
|
||||
}
|
||||
h, err := gojahost.New(gojahost.Config{
|
||||
Name: "pricing",
|
||||
Bundle: bundle,
|
||||
Globals: map[string]any{
|
||||
"__PRICING_DATA__": pricingData,
|
||||
"__PLANS_EXTRA__": plansExtra,
|
||||
"__PLANS_DATA__": plansData,
|
||||
"__MARKUP__": map[string]any{"thirdParty": 1.0, "computeMonthly": 1.0},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("gojahost.New: %v", err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func TestPricing_Summary(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "summary"})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
var body struct {
|
||||
TotalModels int `json:"totalModels"`
|
||||
ZenModels int `json:"zenModels"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body, &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if body.TotalModels <= 0 || body.ZenModels <= 0 {
|
||||
t.Fatalf("expected non-zero model counts, got total=%d zen=%d", body.TotalModels, body.ZenModels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPricing_PublicStripsInternal(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "pricing"})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
var body struct {
|
||||
Cloud map[string]json.RawMessage `json:"cloud"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body, &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if _, leaked := body.Cloud["_internal"]; leaked {
|
||||
t.Fatal("cloud._internal (provider costs / routing) leaked into public /v1/pricing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPricing_Model404(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "model", Params: map[string]string{"name": "no-such-model"}})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
if resp.Status != 404 {
|
||||
t.Fatalf("status = %d, want 404", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPricing_SubscriptionsFromPlans(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
resp, err := h.Dispatch(context.Background(), gojahost.Request{Route: "subscriptions"})
|
||||
if err != nil {
|
||||
t.Fatalf("Dispatch: %v", err)
|
||||
}
|
||||
var body struct {
|
||||
Plans []map[string]any `json:"plans"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Body, &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(body.Plans) == 0 {
|
||||
t.Fatal("expected subscription plans from @hanzo/plans")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPricing_ApplyMarkupRunsInGoja proves the sync.mjs markup transform runs
|
||||
// in goja: feed a raw OpenRouter-shaped model, assert toMTok markup math.
|
||||
func TestPricing_ApplyMarkupRunsInGoja(t *testing.T) {
|
||||
h := newHost(t)
|
||||
defer h.Close()
|
||||
raw := `{"openrouter":[{"id":"openai/gpt-4o","name":"GPT-4o","context_length":128000,"pricing":{"prompt":"0.0000025","completion":"0.00001"}}],"huggingface":[{"id":"meta-llama/Llama-3.1-8B"}]}`
|
||||
out, err := h.Eval(context.Background(), "applyMarkup", []byte(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Eval applyMarkup: %v", err)
|
||||
}
|
||||
var shaped struct {
|
||||
ThirdPartyModels []struct {
|
||||
ID string `json:"id"`
|
||||
Pricing struct {
|
||||
Input float64 `json:"input"`
|
||||
Output float64 `json:"output"`
|
||||
} `json:"pricing"`
|
||||
} `json:"thirdPartyModels"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &shaped); err != nil {
|
||||
t.Fatalf("unmarshal shaped: %v", err)
|
||||
}
|
||||
if len(shaped.ThirdPartyModels) != 2 {
|
||||
t.Fatalf("expected 2 shaped models, got %d", len(shaped.ThirdPartyModels))
|
||||
}
|
||||
// toMTok: 0.0000025 * 1e6 * 1.0 = 2.5 ; 0.00001 * 1e6 = 10
|
||||
var gpt4o *struct {
|
||||
ID string `json:"id"`
|
||||
Pricing struct {
|
||||
Input float64 `json:"input"`
|
||||
Output float64 `json:"output"`
|
||||
} `json:"pricing"`
|
||||
}
|
||||
for i := range shaped.ThirdPartyModels {
|
||||
if shaped.ThirdPartyModels[i].ID == "openai/gpt-4o" {
|
||||
gpt4o = &shaped.ThirdPartyModels[i]
|
||||
}
|
||||
}
|
||||
if gpt4o == nil {
|
||||
t.Fatal("gpt-4o not in shaped output")
|
||||
}
|
||||
if gpt4o.Pricing.Input != 2.5 || gpt4o.Pricing.Output != 10 {
|
||||
t.Fatalf("markup math wrong: input=%v output=%v, want 2.5/10", gpt4o.Pricing.Input, gpt4o.Pricing.Output)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,9 @@ type rpcCommerce struct{ rpcEndpoint }
|
||||
func (c *rpcCommerce) GetTenantConfig(_ context.Context, _ string) (*types.TenantConfig, error) {
|
||||
return nil, c.errf("GetTenantConfig")
|
||||
}
|
||||
func (c *rpcCommerce) CheckEntitlement(_ context.Context, _, _ string) (*types.LicenseEntitlement, error) {
|
||||
return nil, c.errf("CheckEntitlement")
|
||||
}
|
||||
|
||||
type rpcAI struct{ rpcEndpoint }
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// cloud-smoke is a minimal smoke harness for the cloud orchestrator
|
||||
// HTTP+JSON path. It mounts a couple of in-process subsystems on a
|
||||
// zip.App, brings up the listener, and exposes the /v1/base/health
|
||||
// endpoint per the HIP-0106 reference contract. Used to verify the
|
||||
// jsonv2 wiring end-to-end without pulling in the full subsystem
|
||||
// matrix (which has unrelated build issues in cmd/cloud — see
|
||||
// gateway/zap_wire.go uint16 overflow + gateway import cycle, both
|
||||
// tracked separately).
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/zip"
|
||||
"github.com/hanzoai/zip/middleware"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := cloud.LoadConfig()
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = "/tmp/cloud-smoke"
|
||||
}
|
||||
if cfg.Brand == "" {
|
||||
cfg.Brand = "hanzo"
|
||||
}
|
||||
if cfg.Domain == "" {
|
||||
cfg.Domain = "api.hanzo.ai"
|
||||
}
|
||||
|
||||
deps := cloud.BuildDeps(cfg)
|
||||
|
||||
app := zip.New(zip.Config{Logger: deps.Logger})
|
||||
app.Use(middleware.Recover())
|
||||
app.Use(middleware.RequestID())
|
||||
app.Use(middleware.Logger(deps.Logger))
|
||||
|
||||
// HIP-0106 reference health endpoints. The brief's smoke target.
|
||||
app.Get("/v1/base/health", func(c *zip.Ctx) error {
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"service": "base",
|
||||
"status": "ok",
|
||||
})
|
||||
})
|
||||
app.Get("/v1/vfs/health", func(c *zip.Ctx) error {
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"service": "vfs",
|
||||
"status": "ok",
|
||||
})
|
||||
})
|
||||
|
||||
deps.Logger.Info("cloud-smoke listening",
|
||||
"http", cfg.ListenAddr,
|
||||
"brand", cfg.Brand,
|
||||
"domain", cfg.Domain,
|
||||
"json_variant", zip.JSONVariant,
|
||||
)
|
||||
if err := app.Listen(cfg.ListenAddr); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "listen: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+11
-53
@@ -4,6 +4,10 @@
|
||||
// which subsystems mount at startup. Same artifact powers
|
||||
// api.hanzo.ai, api.osage.cloud, api.lux.cloud, api.zoo.cloud, and
|
||||
// every other white-label resold cloud surface.
|
||||
//
|
||||
// The serve body lives in cloud.Serve (one place, shared with the `hanzo`
|
||||
// subcommand dispatcher); main() is just its full-surface entrypoint. The
|
||||
// subsystem set is defined once in the subsystems bundle.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -11,63 +15,17 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/zip"
|
||||
"github.com/hanzoai/zip/middleware"
|
||||
|
||||
// Subsystems — each ships func Mount(*zip.App, cloud.Deps) error and
|
||||
// registers itself via init() in cloud.Registry. Import paths reflect
|
||||
// where each subsystem's Mount lives.
|
||||
_ "github.com/hanzoai/ai" // order 150
|
||||
_ "github.com/hanzoai/amqp" // order 30
|
||||
_ "github.com/hanzoai/authz" // order 70
|
||||
_ "github.com/hanzoai/base" // order 60
|
||||
_ "github.com/hanzoai/commerce" // order 100
|
||||
_ "github.com/hanzoai/gateway" // order 80
|
||||
_ "github.com/hanzoai/iam/pkg/iam" // order 50 (Mount lives in pkg/iam submodule)
|
||||
_ "github.com/hanzoai/ingress" // order 90
|
||||
_ "github.com/hanzoai/kms" // order 10
|
||||
_ "github.com/hanzoai/mcp/go" // order 160 (Mount lives in go submodule)
|
||||
_ "github.com/hanzoai/o11y" // order 70 (mounts alongside authz)
|
||||
_ "github.com/hanzoai/vfs" // order 20
|
||||
// Every subsystem registers into cloud.Registry via init(); the set is
|
||||
// defined ONCE in the subsystems bundle (one source of truth, shared with
|
||||
// cmd/hanzo). Blank-importing it populates the registry cloud.Serve mounts.
|
||||
_ "github.com/hanzoai/cloud/subsystems"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := cloud.LoadConfig()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
deps := cloud.BuildDeps(cfg)
|
||||
|
||||
app := zip.New(zip.Config{Logger: deps.Logger})
|
||||
|
||||
// Canonical middleware pipeline. Order matters:
|
||||
// 1. Recover — panic → JSON 500
|
||||
// 2. RequestID — generate / propagate X-Request-Id
|
||||
// 3. Logger — request-line log via luxfi/log
|
||||
// 4. Telemetry — OTel span; depends on deps.O11y if enabled
|
||||
// 5. Auth — JWT validation; strips client identity, mints from JWT
|
||||
app.Use(middleware.Recover())
|
||||
app.Use(middleware.RequestID())
|
||||
app.Use(middleware.Logger(deps.Logger))
|
||||
// app.Use(middleware.Telemetry(deps.O11y)) // enable once o11y mounted
|
||||
// app.Use(middleware.Auth(deps.IAM)) // enable once iam mounted
|
||||
|
||||
// Per-deployment subsystem mount.
|
||||
if err := cloud.MountAll(app, cfg, deps); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mount: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
deps.Logger.Info("listening",
|
||||
"http", cfg.ListenAddr,
|
||||
"zap", cfg.ZAPListenAddr,
|
||||
"brand", cfg.Brand,
|
||||
"domain", cfg.Domain,
|
||||
)
|
||||
if err := app.Listen(cfg.ListenAddr); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "listen: %v\n", err)
|
||||
// nil ⇒ honor cfg.Enable from flags/env (empty = all subsystems).
|
||||
if err := cloud.Serve(nil); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cloud: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
// Real integration tests for the unified Hanzo Cloud binary (HIP-0106).
|
||||
// These exercise the actual orchestrator path — BuildDeps -> MountAll over the
|
||||
// init()-populated Registry -> serve via the real zip/fiber + jsonenc stack —
|
||||
// not a hand-rolled smoke harness. app.Fiber().Test drives requests in-process,
|
||||
// no listener or external services.
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
"github.com/hanzoai/zip"
|
||||
"github.com/hanzoai/zip/middleware"
|
||||
)
|
||||
|
||||
// every subsystem main.go imports must self-register via init() — this is the
|
||||
// proof the unified binary actually wires the whole matrix.
|
||||
var wantSubsystems = []string{
|
||||
"kms", "amqp", "metrics", "iam", "base", "authz", "o11y",
|
||||
"gateway", "licensing", "plans", "pricing", "ai", "mcp",
|
||||
}
|
||||
|
||||
func TestRegistryAssemblesSubsystems(t *testing.T) {
|
||||
got := map[string]bool{}
|
||||
for _, s := range cloud.Registry {
|
||||
got[s.Name] = true
|
||||
}
|
||||
for _, name := range wantSubsystems {
|
||||
if !got[name] {
|
||||
t.Errorf("subsystem %q not registered — main.go import or its init() missing", name)
|
||||
}
|
||||
}
|
||||
t.Logf("registry assembled %d subsystems", len(cloud.Registry))
|
||||
}
|
||||
|
||||
// newTestApp mirrors main()'s wiring: BuildDeps + the canonical middleware
|
||||
// pipeline + MountAll for the requested subsystems.
|
||||
func newTestApp(t *testing.T, enable ...string) *zip.App {
|
||||
t.Helper()
|
||||
cfg := &cloud.Config{
|
||||
Brand: "hanzo",
|
||||
Domain: "api.hanzo.ai",
|
||||
DataDir: t.TempDir(),
|
||||
Enable: enable,
|
||||
}
|
||||
deps := cloud.BuildDeps(cfg)
|
||||
app := zip.New(zip.Config{Logger: deps.Logger})
|
||||
app.Use(middleware.Recover())
|
||||
app.Use(middleware.RequestID())
|
||||
app.Use(middleware.Logger(deps.Logger))
|
||||
if err := cloud.MountAll(app, cfg, deps); err != nil {
|
||||
t.Fatalf("MountAll(%v): %v", enable, err)
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
// The self-contained subsystems mount in-process (per-tenant SQLite / in-mem,
|
||||
// HIP-0302) and serve a healthy /v1/<name>/health with no external deps.
|
||||
func TestMountAllAndServeHealth(t *testing.T) {
|
||||
healthy := []string{"base", "authz", "amqp", "metrics", "plans", "pricing"}
|
||||
app := newTestApp(t, healthy...)
|
||||
for _, name := range healthy {
|
||||
path := "/v1/" + name + "/health"
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
resp, err := app.Fiber().Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s: %v", path, err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("GET %s = %d, want 200", path, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subsystems whose deps are disabled (no in-process peer, no ZAP endpoint) must
|
||||
// mount and fail CLOSED — a 5xx from the disabled stub, never a panic or a
|
||||
// silent 200. This proves the BuildDeps three-mode contract end-to-end.
|
||||
func TestDepGatedSubsystemsFailClosed(t *testing.T) {
|
||||
for _, name := range []string{"ai", "o11y"} {
|
||||
app := newTestApp(t, name)
|
||||
path := "/v1/" + name + "/health"
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
resp, err := app.Fiber().Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s: %v", path, err)
|
||||
}
|
||||
if resp.StatusCode < 500 {
|
||||
t.Errorf("GET %s = %d, want >=500 (fail-closed; deps disabled)", path, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// Command hanzo is the unified Hanzo Go binary, dispatched by subcommand.
|
||||
//
|
||||
// hanzo list subcommands
|
||||
// hanzo --help list subcommands
|
||||
// hanzo <svc> [flags] serve exactly one subsystem (iam, kms, commerce,
|
||||
// gateway, ai, base, vfs, o11y, …)
|
||||
// hanzo cloud [flags] serve the full unified surface (all enabled
|
||||
// subsystems mounted into one zip.App / one listener)
|
||||
//
|
||||
// One binary. Many subsystems. The subcommand selects WHICH subsystem(s)
|
||||
// serve this process; the same artifact is every standalone service AND
|
||||
// the fused cloud control plane.
|
||||
//
|
||||
// Design — one mechanism, not many. Every Hanzo subsystem registers a
|
||||
// cloud.MountSpec{Name, Order, Mount} into cloud.Registry via init() at
|
||||
// package load (kms order 10, iam 50, gateway 80, commerce 100, …). A
|
||||
// subcommand is therefore just a *selection* over that registry:
|
||||
//
|
||||
// - `hanzo <svc>` ⇒ cloud.Serve([]string{svc}); MountAll mounts only it.
|
||||
// - `hanzo cloud` ⇒ cloud.Serve(nil); cfg.Enable per --enable (empty = all).
|
||||
//
|
||||
// Both paths run the identical compose root (BuildDeps → zip.App → health
|
||||
// contract → MountAll → graceful Listen) — that body lives once in cloud.Serve
|
||||
// and is shared with cmd/cloud. No subcommand duplicates boot logic.
|
||||
//
|
||||
// The single exception is `hanzo iam`. The registry's iam Mount (pkg/iam,
|
||||
// order 50) wraps the Beego handler under /v1/iam/* for the fused surface;
|
||||
// the FULL standalone IAM — login UI at /, all ~150 routes at root, LDAP +
|
||||
// RADIUS listeners — is iamserver.Run(), the body of the legacy iamd
|
||||
// main(). `hanzo iam` runs that, so the standalone identity provider is
|
||||
// byte-for-byte what iamd shipped. See the iam case in dispatch().
|
||||
//
|
||||
// THE BEEGO CRUX (and why this binary does not init-panic). iam imports
|
||||
// github.com/hanzoai/beego/v2; that fork carries process-global state
|
||||
// (web.BeeApp singleton, ORM model registry, logger registration). The
|
||||
// fear is that importing iam alongside the other subsystems collides at
|
||||
// package load regardless of subcommand. It does not, for two reasons
|
||||
// this codebase already established:
|
||||
//
|
||||
// 1. iam's ~150 route registrations and ORM table creation are NOT at
|
||||
// package init() — they live inside iamserver.Init() / routers.InitAPI()
|
||||
// / object.CreateTables(), which run only when iam actually serves.
|
||||
// Blank-importing the package is inert: no router, no ORM, no listener.
|
||||
// 2. There is exactly ONE Beego v2 import path in the graph
|
||||
// (hanzoai/beego/v2), so there is exactly one Beego global to
|
||||
// initialize, and it is initialized lazily by whichever path serves
|
||||
// iam. (visor, the other Beego service, pins the *v1* fork
|
||||
// github.com/beego/beego and is intentionally NOT linked here — two
|
||||
// Beego majors in one binary is the collision to avoid, so we don't.)
|
||||
//
|
||||
// The proof is mechanical: the existing cmd/cloud binary already links
|
||||
// this same graph (iam Beego v2 + kms + commerce + gateway + …) and builds
|
||||
// + boots. cmd/hanzo links the same set plus iamserver for the standalone
|
||||
// path; nothing new collides.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/hanzoai/cloud"
|
||||
|
||||
// iamserver is the body of the standalone iamd main() — full Beego
|
||||
// server (login UI, all routes, LDAP/RADIUS). `hanzo iam` calls Run().
|
||||
"github.com/hanzoai/iam/iamserver"
|
||||
|
||||
// Every subsystem registers into cloud.Registry via init(); the set is
|
||||
// defined ONCE in the subsystems bundle (shared with cmd/cloud), so the
|
||||
// dispatcher and the full-surface binary mount an identical set. Inert at
|
||||
// load — see THE BEEGO CRUX.
|
||||
_ "github.com/hanzoai/cloud/subsystems"
|
||||
)
|
||||
|
||||
// version is overridden at build time via -ldflags "-X main.version=...".
|
||||
var version = "dev"
|
||||
|
||||
// nonRegistrySubcommands are the dispatch targets that do NOT correspond to
|
||||
// a single cloud.Registry entry: the full fused surface, the standalone IAM
|
||||
// boot, and the datastore (a ClickHouse C++ fork with no Go serve target —
|
||||
// see the datastore case in dispatch()). Listed in --help alongside the
|
||||
// registry-backed subcommands.
|
||||
var nonRegistrySubcommands = map[string]string{
|
||||
"cloud": "serve the full unified surface (all enabled subsystems, one listener)",
|
||||
"iam": "serve standalone Hanzo IAM (full Beego server: login UI, OAuth2/OIDC, LDAP/RADIUS)",
|
||||
"datastore": "ClickHouse-fork analytics DB — not a Go serve target (see help text)",
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage(os.Stdout)
|
||||
return
|
||||
}
|
||||
sub := os.Args[1]
|
||||
switch sub {
|
||||
case "-h", "--help", "help":
|
||||
usage(os.Stdout)
|
||||
return
|
||||
case "version", "--version", "-v":
|
||||
fmt.Printf("hanzo %s\n", version)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset os.Args so the delegated service / cloud.LoadConfig sees its own
|
||||
// flags at argv[1:], not the subcommand token. e.g. `hanzo kms --listen=:9000`
|
||||
// → the kms serve path parses `--listen=:9000`.
|
||||
os.Args = append(os.Args[:1], os.Args[2:]...)
|
||||
|
||||
if err := dispatch(sub); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "hanzo %s: %v\n", sub, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch routes a subcommand to its serve entrypoint.
|
||||
func dispatch(sub string) error {
|
||||
switch sub {
|
||||
case "cloud":
|
||||
// Full fused surface: --enable governs the set (empty = all).
|
||||
return cloud.Serve(nil)
|
||||
|
||||
case "iam":
|
||||
// Standalone IAM = the body of iamd's main(). Full Beego server:
|
||||
// login UI at /, ~150 routes at root, LDAP + RADIUS listeners,
|
||||
// background sync loops. iamserver.Run() blocks until the process
|
||||
// is signalled. This is intentionally NOT the /v1/iam/*-wrapped
|
||||
// registry Mount — `hanzo iam` IS the identity provider, not a
|
||||
// route-prefixed subsystem inside the cloud surface.
|
||||
iamserver.Run()
|
||||
return nil
|
||||
|
||||
case "datastore":
|
||||
// Hanzo Datastore is a ClickHouse C++ fork. It has no Go
|
||||
// Serve()/Run() to dispatch to: the server is the ClickHouse
|
||||
// engine (built via CMake), and the only Go in the repo is
|
||||
// cmd/zap-bridge — a SEPARATE per-package Go module
|
||||
// (github.com/hanzoai/datastore/cmd/zap-bridge) built solely by
|
||||
// the datastore Dockerfile's zap-builder stage, not part of this
|
||||
// module graph. Folding it into `hanzo` would mean either cgo-
|
||||
// linking ClickHouse into every Hanzo binary (a non-starter) or
|
||||
// vendoring a second main module (violates one-binary). So
|
||||
// datastore stays its own artifact; `hanzo datastore` documents
|
||||
// that boundary instead of pretending to serve it.
|
||||
return fmt.Errorf(
|
||||
"datastore is a ClickHouse-fork analytics DB, not a Go serve target.\n" +
|
||||
" - server: the ClickHouse engine (CMake build) — run its own image ghcr.io/hanzoai/datastore\n" +
|
||||
" - zap-bridge: github.com/hanzoai/datastore/cmd/zap-bridge is a separate Go module,\n" +
|
||||
" built only by the datastore Dockerfile; it is not linked into hanzo.\n" +
|
||||
" use the standalone datastore deployment; `hanzo` composes the request-tier Go services")
|
||||
|
||||
default:
|
||||
// Registry-backed single-service mode: serve exactly `sub`.
|
||||
// Validate it is a known subsystem before booting anything.
|
||||
if !registryHas(sub) {
|
||||
usage(os.Stderr)
|
||||
return fmt.Errorf("unknown subcommand %q", sub)
|
||||
}
|
||||
return cloud.Serve([]string{sub})
|
||||
}
|
||||
}
|
||||
|
||||
// registryHas reports whether name is a registered subsystem.
|
||||
func registryHas(name string) bool {
|
||||
for _, spec := range cloud.Registry {
|
||||
if spec.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// usage prints the subcommand list: the non-registry targets (cloud, iam,
|
||||
// datastore) plus every subsystem registered into cloud.Registry, sorted.
|
||||
func usage(w *os.File) {
|
||||
fmt.Fprintf(w, "hanzo %s — the unified Hanzo Go binary\n\n", version)
|
||||
fmt.Fprintf(w, "Usage:\n hanzo <subcommand> [flags]\n\n")
|
||||
fmt.Fprintf(w, "Service subcommands:\n")
|
||||
|
||||
// Collect: registry names ∪ non-registry names, dedup, sort.
|
||||
seen := map[string]string{}
|
||||
for name, desc := range nonRegistrySubcommands {
|
||||
seen[name] = desc
|
||||
}
|
||||
for _, spec := range cloud.Registry {
|
||||
if _, ok := seen[spec.Name]; !ok {
|
||||
seen[spec.Name] = fmt.Sprintf("serve the %s subsystem standalone", spec.Name)
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(seen))
|
||||
for name := range seen {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(w, " %-12s %s\n", name, seen[name])
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\nMeta:\n")
|
||||
fmt.Fprintf(w, " %-12s %s\n", "help", "show this message")
|
||||
fmt.Fprintf(w, " %-12s %s\n", "version", "print version and exit")
|
||||
fmt.Fprintf(w, "\nFlags are per-subcommand (e.g. `hanzo cloud --enable=iam,kms --brand=hanzo`,\n")
|
||||
fmt.Fprintf(w, "`hanzo kms --listen=:8443`). Run a subcommand to see its config via env/flags.\n")
|
||||
}
|
||||
@@ -20,7 +20,7 @@ type Config struct {
|
||||
// Domain is the deployment's primary public domain.
|
||||
Domain string
|
||||
|
||||
// IAMIssuer is the JWKS issuer for JWT validation (usually iam.hanzo.id).
|
||||
// IAMIssuer is the JWKS issuer for JWT validation (usually iam.hanzo.ai).
|
||||
IAMIssuer string
|
||||
|
||||
// KMSMasterKeyRef points at the KMS master key for per-tenant DEK derivation.
|
||||
@@ -74,7 +74,7 @@ func LoadConfig() *Config {
|
||||
AdminListenAddr: getenv("CLOUD_ADMIN_LISTEN", ":8081"),
|
||||
Brand: getenv("CLOUD_BRAND", "hanzo"),
|
||||
Domain: getenv("CLOUD_DOMAIN", "api.hanzo.ai"),
|
||||
IAMIssuer: getenv("CLOUD_IAM_ISSUER", "https://iam.hanzo.id"),
|
||||
IAMIssuer: getenv("CLOUD_IAM_ISSUER", "https://iam.hanzo.ai"),
|
||||
KMSMasterKeyRef: getenv("CLOUD_KMS_MASTER_KEY_REF", ""),
|
||||
DataDir: getenv("CLOUD_DATA_DIR", "/var/lib/cloud"),
|
||||
PaymentsZAPAddr: getenv("CLOUD_PAYMENTS_ZAP_ADDR", ""),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Deploying hanzoai/cloud
|
||||
|
||||
Reference deployment manifests for the unified Hanzo Cloud binary
|
||||
(HIP-0106). Each manifest demonstrates a different deployment topology.
|
||||
|
||||
| Manifest | Topology | Use case |
|
||||
|----------|----------|----------|
|
||||
| `compose.yml` | single-node Docker | VPS, dev, demo |
|
||||
|
||||
Coming soon: `kustomize/` and `helm/` for k8s deploys (see luxfi/operator
|
||||
for the canonical CRD-driven shape).
|
||||
|
||||
## Quick start (Docker Compose)
|
||||
|
||||
```bash
|
||||
cp deploy/compose.env.example deploy/compose.env
|
||||
# edit HANZO_BRAND / HANZO_DOMAIN / HANZO_IAM_ISSUER as needed
|
||||
docker compose -f deploy/compose.yml --env-file deploy/compose.env up -d
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
Required:
|
||||
- `HANZO_IAM_ISSUER` — OIDC issuer URL. Without it the IAM subsystem
|
||||
refuses to mount and the binary exits.
|
||||
|
||||
Optional (defaults shown):
|
||||
- `HANZO_BRAND=hanzo`
|
||||
- `HANZO_DOMAIN=api.hanzo.local`
|
||||
- `HANZO_ENABLE=iam,base,kms,gateway,o11y`
|
||||
- `HANZO_DATA_DIR=/var/lib/cloud`
|
||||
|
||||
The full subsystem list is in `cmd/cloud/main.go`. Per HIP-0106
|
||||
payments and vault NEVER co-resident — leave them off this binary
|
||||
unless you understand the PCI scope implications.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Tag of ghcr.io/hanzoai/cloud to pull (defaults to :latest)
|
||||
HANZO_CLOUD_TAG=latest
|
||||
|
||||
# White-label brand for this deployment
|
||||
HANZO_BRAND=hanzo
|
||||
|
||||
# Public domain this binary serves (used to scope URLs in responses)
|
||||
HANZO_DOMAIN=api.hanzo.ai
|
||||
|
||||
# OIDC issuer your IAM trusts (required — no default in the binary)
|
||||
HANZO_IAM_ISSUER=https://iam.hanzo.id
|
||||
|
||||
# Subsystem mount list. Empty = mount everything imported in cmd/cloud/main.go.
|
||||
HANZO_ENABLE=iam,base,kms,gateway,o11y
|
||||
|
||||
# Port mappings (override if host has conflicts)
|
||||
HANZO_HTTP_PORT=8080
|
||||
HANZO_METRICS_PORT=9090
|
||||
HANZO_ZAP_PORT=9653
|
||||
@@ -0,0 +1,48 @@
|
||||
# Single-node VPS deployment of the unified hanzoai/cloud binary (HIP-0106).
|
||||
#
|
||||
# Mounts the iam + base + kms + gateway + o11y subsystems in-process.
|
||||
# Payments / Vault stay out-of-process per HIP-0106 PCI scope; configure
|
||||
# their ZAP endpoints via env if you need them.
|
||||
#
|
||||
# Usage:
|
||||
# cp deploy/compose.env.example deploy/compose.env
|
||||
# # edit HANZO_BRAND, HANZO_DOMAIN, HANZO_IAM_ISSUER
|
||||
# docker compose -f deploy/compose.yml --env-file deploy/compose.env up -d
|
||||
#
|
||||
# Health check:
|
||||
# curl http://localhost:8080/health
|
||||
|
||||
services:
|
||||
cloud:
|
||||
image: ghcr.io/hanzoai/cloud:${HANZO_CLOUD_TAG:-latest}
|
||||
container_name: hanzo-cloud
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HANZO_HTTP_PORT:-8080}:8080"
|
||||
- "${HANZO_METRICS_PORT:-9090}:9090"
|
||||
- "${HANZO_ZAP_PORT:-9653}:9653"
|
||||
environment:
|
||||
HANZO_BRAND: ${HANZO_BRAND:-hanzo}
|
||||
HANZO_DOMAIN: ${HANZO_DOMAIN:-api.hanzo.local}
|
||||
HANZO_DATA_DIR: /var/lib/cloud
|
||||
HANZO_IAM_ISSUER: ${HANZO_IAM_ISSUER:-https://iam.hanzo.id}
|
||||
# Subsystem enable list (empty = all). Match what your tenant needs.
|
||||
HANZO_ENABLE: ${HANZO_ENABLE:-iam,base,kms,gateway,o11y}
|
||||
command:
|
||||
- "--brand=${HANZO_BRAND:-hanzo}"
|
||||
- "--domain=${HANZO_DOMAIN:-api.hanzo.local}"
|
||||
- "--data-dir=/var/lib/cloud"
|
||||
- "--iam-issuer=${HANZO_IAM_ISSUER:-https://iam.hanzo.id}"
|
||||
- "--enable=${HANZO_ENABLE:-iam,base,kms,gateway,o11y}"
|
||||
volumes:
|
||||
- cloud-data:/var/lib/cloud
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O - http://localhost:8080/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
cloud-data:
|
||||
driver: local
|
||||
@@ -86,6 +86,7 @@ type User = types.User
|
||||
type Org = types.Org
|
||||
type DBHandle = types.DBHandle
|
||||
type TenantConfig = types.TenantConfig
|
||||
type LicenseEntitlement = types.LicenseEntitlement
|
||||
type ChatRequest = types.ChatRequest
|
||||
type ChatResponse = types.ChatResponse
|
||||
type Counter = types.Counter
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
module github.com/hanzoai/cloud
|
||||
|
||||
go 1.26.3
|
||||
go 1.26.4
|
||||
|
||||
// Dependencies will be added as subsystems are mounted per HIP-0106.
|
||||
|
||||
require (
|
||||
github.com/hanzoai/zip v0.1.0
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
||||
github.com/hanzoai/gateway v2.9.7+incompatible
|
||||
github.com/hanzoai/iam v1.19.4
|
||||
github.com/hanzoai/plans v1.2.0
|
||||
github.com/hanzoai/pricing v1.3.0
|
||||
github.com/hanzoai/zip v0.2.0
|
||||
github.com/luxfi/log v1.4.3
|
||||
)
|
||||
|
||||
@@ -13,25 +18,18 @@ require (
|
||||
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/kms v1.25.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.8.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
cloud.google.com/go/pubsub v1.50.1 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.3.0 // indirect
|
||||
cloud.google.com/go/storage v1.59.2 // indirect
|
||||
cloud.google.com/go/trace v1.11.7 // indirect
|
||||
contrib.go.opencensus.io/exporter/aws v0.0.0-20230502192102-15967c811cec // indirect
|
||||
contrib.go.opencensus.io/exporter/jaeger v0.2.1 // indirect
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.7.0 // indirect
|
||||
contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
|
||||
contrib.go.opencensus.io/exporter/stackdriver v0.13.14 // indirect
|
||||
contrib.go.opencensus.io/exporter/zipkin v0.1.2 // indirect
|
||||
cloud.google.com/go/iam v1.7.0 // indirect
|
||||
cloud.google.com/go/kms v1.27.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.9.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.25.0 // indirect
|
||||
cloud.google.com/go/pubsub v1.50.2 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.5.1 // indirect
|
||||
cloud.google.com/go/storage v1.61.3 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/ALTree/bigfloat v0.2.0 // indirect
|
||||
github.com/AfterShip/clickhouse-sql-parser v0.4.16 // indirect
|
||||
@@ -39,7 +37,7 @@ require (
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1 // indirect
|
||||
@@ -49,16 +47,13 @@ require (
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.71.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 // indirect
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
|
||||
github.com/DataDog/opencensus-go-exporter-datadog v0.0.0-20220622145613-731d59e8b567 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/IBM/sarama v1.46.3 // indirect
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
|
||||
github.com/Machiel/slugify v1.0.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260311194731-d5b7577c683d // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20240116134246-a8cbe886bab0 // indirect
|
||||
@@ -84,39 +79,34 @@ require (
|
||||
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
|
||||
github.com/aliyun/credentials-go v1.4.7 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/smithy-go v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/aymerick/raymond v2.0.2+incompatible // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.260 // indirect
|
||||
github.com/beego/beego/v2 v2.3.8 // indirect
|
||||
github.com/beevik/etree v1.6.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
@@ -134,7 +124,6 @@ require (
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
@@ -146,16 +135,15 @@ require (
|
||||
github.com/consensys/gnark-crypto v0.20.1 // indirect
|
||||
github.com/corazawaf/coraza/v3 v3.3.3 // indirect
|
||||
github.com/corazawaf/libinjection-go v0.2.2 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/corpix/uarand v0.2.0 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dghubble/sling v1.4.2 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
|
||||
@@ -172,13 +160,11 @@ require (
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect
|
||||
@@ -198,11 +184,9 @@ require (
|
||||
github.com/go-git/go-git/v5 v5.16.4 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-lark/lark v1.15.1 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.6 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -231,17 +215,15 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/webauthn v0.10.2 // indirect
|
||||
github.com/go-webauthn/x v0.1.9 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gojek/heimdall/v7 v7.0.3 // indirect
|
||||
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
@@ -249,7 +231,6 @@ require (
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/mock v1.7.0-rc.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
@@ -260,37 +241,43 @@ require (
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/wire v0.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
github.com/gregdel/pushover v1.3.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/gtank/merlin v0.1.1 // indirect
|
||||
github.com/gtank/ristretto255 v0.2.0 // indirect
|
||||
github.com/hanzoai/authzstore v0.1.1 // indirect
|
||||
github.com/hanzoai/beego/v2 v2.3.9 // indirect
|
||||
github.com/hanzoai/builder v0.3.13 // indirect
|
||||
github.com/hanzoai/common v0.67.7 // indirect
|
||||
github.com/hanzoai/datastore-go/v2 v2.45.0 // indirect
|
||||
github.com/hanzoai/dbx v1.16.0 // indirect
|
||||
github.com/hanzoai/go-sms-sender v0.25.2 // indirect
|
||||
github.com/hanzoai/gomail/v2 v2.3.2 // indirect
|
||||
github.com/hanzoai/iam v1.18.0 // indirect
|
||||
github.com/hanzoai/exporter-toolkit v0.15.2 // indirect
|
||||
github.com/hanzoai/goauthorizenet v0.0.0-20180920213706-626992b83568 // indirect
|
||||
github.com/hanzoai/gochimp3 v0.0.0-20241127054040-6051f77e24f1 // indirect
|
||||
github.com/hanzoai/iamsdk/v2 v2.1.0 // indirect
|
||||
github.com/hanzoai/idv v1.0.0 // indirect
|
||||
github.com/hanzoai/kms/sdk/go v1.0.0 // indirect
|
||||
github.com/hanzoai/kv-go/v9 v9.18.0 // indirect
|
||||
github.com/hanzoai/ldapserver v1.2.1 // indirect
|
||||
github.com/hanzoai/notify2 v1.6.3 // indirect
|
||||
github.com/hanzoai/orm v0.5.1 // indirect
|
||||
github.com/hanzoai/orm v0.5.2 // indirect
|
||||
github.com/hanzoai/oss v1.8.5 // indirect
|
||||
github.com/hanzoai/pubsub-go v1.0.0 // indirect
|
||||
github.com/hanzoai/search-go v0.36.0 // indirect
|
||||
github.com/hanzoai/sendgrid-go v3.4.2-0.20180724185151-733a05184a8d+incompatible // indirect
|
||||
github.com/hanzoai/sigv4 v0.4.2 // indirect
|
||||
github.com/hanzoai/storage-go v1.0.0 // indirect
|
||||
github.com/hanzoai/tasks v1.40.0 // indirect
|
||||
github.com/hanzoai/xorm v1.1.6 // indirect
|
||||
@@ -312,6 +299,7 @@ require (
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.4 // indirect
|
||||
github.com/hashicorp/vault/api v1.14.0 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/hsluoyz/modsecurity-go v0.0.7 // indirect
|
||||
github.com/huandu/go-sqlbuilder v1.35.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
@@ -334,14 +322,13 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/knadh/koanf v1.5.0 // indirect
|
||||
github.com/knadh/koanf/v2 v2.3.2 // indirect
|
||||
github.com/kpacha/opencensus-influxdb v0.0.0-20180520162117-1b490a38de4c // indirect
|
||||
github.com/krakend/binder v0.0.0-20250826131726-e91a8a754ef8 // indirect
|
||||
github.com/krakend/bloomfilter/v2 v2.1.0 // indirect
|
||||
github.com/krakend/flatmap v1.2.0 // indirect
|
||||
@@ -390,28 +377,43 @@ require (
|
||||
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/luraproject/lura/v2 v2.12.1 // indirect
|
||||
github.com/luxfi/accel v1.0.7 // indirect
|
||||
github.com/luxfi/accel v1.1.9 // indirect
|
||||
github.com/luxfi/address v1.0.1 // indirect
|
||||
github.com/luxfi/age v1.5.0 // indirect
|
||||
github.com/luxfi/cache v1.2.1 // indirect
|
||||
github.com/luxfi/compress v0.0.5 // indirect
|
||||
github.com/luxfi/concurrent v0.0.3 // indirect
|
||||
github.com/luxfi/consensus v1.22.85 // indirect
|
||||
github.com/luxfi/consensus v1.25.0 // indirect
|
||||
github.com/luxfi/constants v1.5.8 // indirect
|
||||
github.com/luxfi/container v0.0.4 // indirect
|
||||
github.com/luxfi/crypto v1.19.0 // indirect
|
||||
github.com/luxfi/corona v0.7.6 // indirect
|
||||
github.com/luxfi/crypto v1.19.17 // indirect
|
||||
github.com/luxfi/crypto/ipa v1.2.4 // indirect
|
||||
github.com/luxfi/database v1.18.1 // indirect
|
||||
github.com/luxfi/fhe v1.7.9 // indirect
|
||||
github.com/luxfi/ids v1.2.9 // indirect
|
||||
github.com/luxfi/kms v1.5.2 // indirect
|
||||
github.com/luxfi/lattice/v7 v7.0.0 // indirect
|
||||
github.com/luxfi/math v1.2.4 // indirect
|
||||
github.com/luxfi/database v1.18.3 // indirect
|
||||
github.com/luxfi/fhe v1.8.2 // indirect
|
||||
github.com/luxfi/formatting v1.0.1 // indirect
|
||||
github.com/luxfi/geth v1.16.100 // indirect
|
||||
github.com/luxfi/go-bip32 v1.0.2 // indirect
|
||||
github.com/luxfi/go-bip39 v1.1.2 // indirect
|
||||
github.com/luxfi/ids v1.2.15 // indirect
|
||||
github.com/luxfi/keys v1.1.0 // indirect
|
||||
github.com/luxfi/kms v1.11.6 // indirect
|
||||
github.com/luxfi/lattice/v7 v7.1.4 // indirect
|
||||
github.com/luxfi/lens v0.1.4 // indirect
|
||||
github.com/luxfi/math v1.4.1 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/mdns v0.1.0 // indirect
|
||||
github.com/luxfi/metric v1.5.1 // indirect
|
||||
github.com/luxfi/mdns v0.1.1 // indirect
|
||||
github.com/luxfi/metric v1.5.8 // indirect
|
||||
github.com/luxfi/mock v0.1.1 // indirect
|
||||
github.com/luxfi/ringtail v0.2.0 // indirect
|
||||
github.com/luxfi/zap v0.3.1 // indirect
|
||||
github.com/luxfi/zapdb v1.9.0 // indirect
|
||||
github.com/luxfi/pq v1.0.3 // indirect
|
||||
github.com/luxfi/proto v1.0.0 // indirect
|
||||
github.com/luxfi/pulsar v1.1.1 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/threshold v1.9.4 // indirect
|
||||
github.com/luxfi/tls v1.0.3 // indirect
|
||||
github.com/luxfi/vm v1.2.0 // indirect
|
||||
github.com/luxfi/zap v0.7.2 // indirect
|
||||
github.com/luxfi/zapdb v1.10.0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20241126214340-bdc92f694516 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/markbates/going v1.0.0 // indirect
|
||||
@@ -426,6 +428,7 @@ require (
|
||||
github.com/microsoft/go-mssqldb v1.9.5 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/mileusna/viber v1.0.1 // indirect
|
||||
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.100 // indirect
|
||||
@@ -438,28 +441,20 @@ require (
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/montanaflynn/stats v0.8.2 // indirect
|
||||
github.com/montanaflynn/stats v0.9.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/nats-io/nats.go v1.50.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/netlify/netlify-go v0.1.11 // indirect
|
||||
github.com/nyaruka/phonenumbers v1.2.2 // indirect
|
||||
github.com/oklog/run v1.2.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/open-feature/go-sdk v1.17.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect
|
||||
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 // indirect
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||
github.com/pariz/gountries v0.1.6 // indirect
|
||||
github.com/paulmach/orb v0.13.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
@@ -468,21 +463,12 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/plaid/plaid-go/v15 v15.3.0 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/prometheus/alertmanager v0.31.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.15.1 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/prometheus v0.310.0 // indirect
|
||||
github.com/prometheus/sigv4 v0.4.1 // indirect
|
||||
github.com/prometheus/statsd_exporter v0.26.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
@@ -493,12 +479,11 @@ require (
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.17.2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/resend/resend-go/v3 v3.1.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/rs/zerolog v1.35.0 // indirect
|
||||
github.com/russellhaering/gosaml2 v0.11.0 // indirect
|
||||
github.com/russellhaering/goxmldsig v1.6.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
@@ -528,6 +513,7 @@ require (
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/square/square-go-sdk/v3 v3.0.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
@@ -540,8 +526,6 @@ require (
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
|
||||
github.com/tealeg/xlsx v1.0.5 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.744 // indirect
|
||||
github.com/thanhpk/randstr v1.0.4 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
@@ -557,10 +541,7 @@ require (
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/tmthrgd/go-memset v0.0.0-20190904060434-6fb7a21f88f1 // indirect
|
||||
github.com/tmthrgd/go-popcount v0.0.0-20190904054823-afb1ace8b04f // indirect
|
||||
github.com/twilio/twilio-go v1.13.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
github.com/ucloud/ucloud-sdk-go v0.22.5 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/unrolled/secure v1.15.0 // indirect
|
||||
github.com/uptrace/bun v1.2.9 // indirect
|
||||
@@ -572,7 +553,6 @@ require (
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
@@ -581,6 +561,7 @@ require (
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zap-proto/go v1.1.0 // indirect
|
||||
github.com/zap-proto/http v0.0.0-20260506200741-fd6047874433 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.mau.fi/util v0.8.3 // indirect
|
||||
@@ -588,52 +569,25 @@ require (
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/collector/client v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/component v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configretry v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumererror v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/exporterhelper v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/extension v1.50.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/xextension v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.145.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/xpdata v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0 // indirect
|
||||
go.opentelemetry.io/collector/processor v1.51.0 // indirect
|
||||
go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685 // indirect
|
||||
go.opentelemetry.io/contrib/config v0.10.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.15.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
@@ -650,37 +604,35 @@ require (
|
||||
gocloud.dev/secrets/hashivault v0.39.0 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/image v0.38.0 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.267.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
gonum.org/v1/gonum v0.17.0 // indirect
|
||||
google.golang.org/api v0.275.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.62.0 // indirect
|
||||
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20191017102106-1550ee647df0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/telebot.v3 v3.3.8 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apimachinery v0.35.0 // indirect
|
||||
k8s.io/client-go v0.35.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8 // indirect
|
||||
maunium.net/go/mautrix v0.22.1 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.50.0 // indirect
|
||||
modernc.org/sqlite v1.51.0 // indirect
|
||||
rsc.io/binaryregexp v0.2.0 // indirect
|
||||
)
|
||||
|
||||
@@ -692,27 +644,28 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hanzoai/ai v1.785.0
|
||||
github.com/hanzoai/amqp v0.2.0
|
||||
github.com/hanzoai/authz v1.10.0
|
||||
github.com/hanzoai/base v1.3.1
|
||||
github.com/hanzoai/commerce v1.37.0
|
||||
github.com/hanzoai/gateway v0.2.0
|
||||
github.com/hanzoai/iam/pkg/iam v1.18.0
|
||||
github.com/hanzoai/authz v1.10.1
|
||||
github.com/hanzoai/base v1.3.2
|
||||
github.com/hanzoai/commerce v1.42.27
|
||||
github.com/hanzoai/iam/pkg/iam v1.18.4
|
||||
github.com/hanzoai/ingress v1.8.0
|
||||
github.com/hanzoai/kms v0.159.0
|
||||
github.com/hanzoai/kms v0.159.1
|
||||
github.com/hanzoai/licensing v0.1.0
|
||||
github.com/hanzoai/mcp/go v0.1.0
|
||||
github.com/hanzoai/o11y v0.1.0
|
||||
github.com/hanzoai/vfs v0.1.0
|
||||
github.com/hanzoai/metrics v0.4.0
|
||||
github.com/hanzoai/o11y v1.3.7
|
||||
github.com/hanzoai/vfs v0.4.0
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.70.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
)
|
||||
|
||||
@@ -728,28 +681,11 @@ replace github.com/mailgun/minheap => github.com/containous/minheap v0.0.0-20190
|
||||
|
||||
replace github.com/vulcand/oxy/v2 => github.com/traefik/oxy/v2 v2.0.0-20260126093803-fb11d60e0fdf
|
||||
|
||||
replace github.com/hanzoai/ai => ../ai
|
||||
// Pin to last real go-sqlite3 release; v2.0.3+incompatible is a phantom (no module).
|
||||
replace github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.16
|
||||
|
||||
replace github.com/hanzoai/amqp => ../amqp
|
||||
replace github.com/prometheus/alertmanager => github.com/hanzoai/alertmanager v0.28.2
|
||||
|
||||
replace github.com/hanzoai/authz => ../authz
|
||||
replace github.com/krakend/krakend-otel => github.com/hanzoai/krakend-otel v0.13.1
|
||||
|
||||
replace github.com/hanzoai/base => ../base
|
||||
|
||||
replace github.com/hanzoai/commerce => ../commerce
|
||||
|
||||
replace github.com/hanzoai/gateway => ../gateway
|
||||
|
||||
replace github.com/hanzoai/iam => ../iam
|
||||
|
||||
replace github.com/hanzoai/ingress => ../ingress
|
||||
|
||||
replace github.com/hanzoai/kms => ../kms
|
||||
|
||||
replace github.com/hanzoai/o11y => ../o11y
|
||||
|
||||
replace github.com/hanzoai/vfs => ../vfs
|
||||
|
||||
replace github.com/hanzoai/iam/pkg/iam => ../iam/pkg/iam
|
||||
|
||||
replace github.com/hanzoai/mcp/go => ../mcp/go
|
||||
exclude github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
*.tgz
|
||||
README.md.tmpl
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: v2
|
||||
name: cloud
|
||||
description: Hanzo Cloud — unified Go binary mounting iam/base/kms/gateway/o11y/commerce/ai (HIP-0106).
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
home: https://hanzo.ai
|
||||
sources:
|
||||
- https://github.com/hanzoai/cloud
|
||||
maintainers:
|
||||
- name: Hanzo
|
||||
url: https://hanzo.ai
|
||||
keywords:
|
||||
- hanzo
|
||||
- ai
|
||||
- cloud
|
||||
- unified-binary
|
||||
- hip-0106
|
||||
@@ -0,0 +1,30 @@
|
||||
# helm/cloud
|
||||
|
||||
Minimal Helm chart for the unified `hanzoai/cloud` binary (HIP-0106).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
helm install cloud ./helm/cloud \
|
||||
--set hanzo.iamIssuer=https://iam.hanzo.id \
|
||||
--set hanzo.brand=hanzo \
|
||||
--set hanzo.domain=api.hanzo.ai \
|
||||
--set image.tag=v0.1.0
|
||||
```
|
||||
|
||||
## Required values
|
||||
|
||||
- `hanzo.iamIssuer` — OIDC issuer URL. Without this the IAM subsystem
|
||||
refuses to mount and the pod CrashLoopBackOff's.
|
||||
|
||||
## What this chart is NOT
|
||||
|
||||
This is a single-Deployment / single-Service / single-ConfigMap chart.
|
||||
For multi-tenant / multi-CRD operator-driven topology, install
|
||||
`luxfi/operator` and use the `Service` CRD instead — that's the
|
||||
canonical k8s shape per HIP-0106 + HIP-0014.
|
||||
|
||||
For PCI workloads (payments, vault) — those subsystems are NEVER
|
||||
co-resident with the rest of cloud. Run them as separate Deployments
|
||||
backed by their own charts; configure their ZAP RPC endpoints into
|
||||
this chart's values.
|
||||
@@ -0,0 +1,32 @@
|
||||
{{/* Standard helpers */}}
|
||||
{{- define "cloud.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloud.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloud.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
|
||||
app.kubernetes.io/name: {{ include "cloud.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloud.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "cloud.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,65 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cloud.fullname" . }}
|
||||
labels: {{- include "cloud.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels: {{- include "cloud.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels: {{- include "cloud.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: cloud
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- "--brand={{ .Values.hanzo.brand }}"
|
||||
- "--domain={{ .Values.hanzo.domain }}"
|
||||
- "--data-dir={{ .Values.hanzo.dataDir }}"
|
||||
- "--iam-issuer={{ .Values.hanzo.iamIssuer }}"
|
||||
- "--enable={{ .Values.hanzo.enable }}"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
- name: metrics
|
||||
containerPort: 9090
|
||||
protocol: TCP
|
||||
- name: zap
|
||||
containerPort: 9653
|
||||
protocol: TCP
|
||||
livenessProbe: {{- toYaml .Values.probes.liveness | nindent 12 }}
|
||||
readinessProbe: {{- toYaml .Values.probes.readiness | nindent 12 }}
|
||||
securityContext: {{- toYaml .Values.securityContext | nindent 12 }}
|
||||
resources: {{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: {{ .Values.hanzo.dataDir }}
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: data
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "cloud.fullname" . }}-data
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
{{- if .Values.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "cloud.fullname" . }}-data
|
||||
labels: {{- include "cloud.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes: {{- toYaml .Values.persistence.accessModes | nindent 4 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- if .Values.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.persistence.storageClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cloud.fullname" . }}
|
||||
labels: {{- include "cloud.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.httpPort }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
- name: metrics
|
||||
port: {{ .Values.service.metricsPort }}
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
- name: zap
|
||||
port: {{ .Values.service.zapPort }}
|
||||
targetPort: zap
|
||||
protocol: TCP
|
||||
selector: {{- include "cloud.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,75 @@
|
||||
# hanzoai/cloud — Helm values
|
||||
#
|
||||
# Minimal chart: Deployment + Service + ConfigMap.
|
||||
# Reference shape only — for richer ops use luxfi/operator and the
|
||||
# Service CRD (operator wraps this shape with reconciliation).
|
||||
|
||||
image:
|
||||
repository: ghcr.io/hanzoai/cloud
|
||||
tag: "" # defaults to .Chart.AppVersion when empty
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
# Brand / domain / enabled subsystems mirror the binary's CLI flags
|
||||
# (cmd/cloud/main.go).
|
||||
hanzo:
|
||||
brand: hanzo
|
||||
domain: api.hanzo.ai
|
||||
dataDir: /var/lib/cloud
|
||||
# OIDC issuer — REQUIRED. Without this the IAM mount refuses and the
|
||||
# process exits at start.
|
||||
iamIssuer: https://iam.hanzo.id
|
||||
# Comma-separated subsystem list (empty = all imported in cmd/cloud).
|
||||
# Payments + Vault are NEVER co-resident per HIP-0106 PCI isolation;
|
||||
# leave them off unless you fully understand the implications.
|
||||
enable: "iam,base,kms,gateway,o11y"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
httpPort: 8080
|
||||
metricsPort: 9090
|
||||
zapPort: 9653
|
||||
|
||||
persistence:
|
||||
# Per-tenant SQLite (HIP-0302) lives under dataDir. Set to an
|
||||
# appropriate PVC for production; emptyDir is fine for dev.
|
||||
enabled: false
|
||||
size: 20Gi
|
||||
storageClass: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
resources: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
probes:
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
# smoke-runtime.sh — boot the cloud binary and probe the mounted HTTP surface.
|
||||
#
|
||||
# Complements cmd/cloud-smoke (which exercises the in-process mount path).
|
||||
# This script exercises the REAL ./cmd/cloud binary end-to-end: build, boot
|
||||
# with the "default safe" --enable list, curl the endpoints that should return
|
||||
# 200/401 per the HIP-0106 contract, kill the process, exit non-zero if any
|
||||
# endpoint regresses.
|
||||
#
|
||||
# Intended for: CI smoke gating + local "does this clone actually serve?".
|
||||
#
|
||||
# Usage:
|
||||
# scripts/smoke-runtime.sh # build, boot, probe, teardown
|
||||
# PORT=8090 scripts/smoke-runtime.sh # alternate port
|
||||
# KEEP_RUNNING=1 scripts/smoke-runtime.sh # leave the binary up after probes
|
||||
# BIN=./bin/cloud scripts/smoke-runtime.sh # reuse an already-built binary
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${PORT:-8088}"
|
||||
LISTEN="${LISTEN:-127.0.0.1:${PORT}}"
|
||||
BIN="${BIN:-./bin/cloud}"
|
||||
DATA_DIR="${DATA_DIR:-$(mktemp -d -t cloud-smoke.XXXXXX)}"
|
||||
LOG_FILE="${LOG_FILE:-${DATA_DIR}/cloud-smoke.log}"
|
||||
KEEP_RUNNING="${KEEP_RUNNING:-0}"
|
||||
BOOT_TIMEOUT="${BOOT_TIMEOUT:-30}"
|
||||
|
||||
# IAM env vars are required by the kms subsystem even when iam is disabled,
|
||||
# since kms validates inbound JWTs against the live hanzo.id IAM.
|
||||
export IAM_URL="${IAM_URL:-https://hanzo.id}"
|
||||
export IAM_ISSUER="${IAM_ISSUER:-https://hanzo.id}"
|
||||
export IAM_AUDIENCE="${IAM_AUDIENCE:-kms}"
|
||||
export IAM_KEYS_URL="${IAM_KEYS_URL:-https://hanzo.id/v1/iam/.well-known/jwks}"
|
||||
|
||||
# Default safe enable list — omits iam (panics on boot until hanzoai/iam ships
|
||||
# the v1.19.2 fix for the orphan RunAuthzCommand route) and ingress/commerce
|
||||
# (not yet wired into the default smoke set).
|
||||
ENABLE="${ENABLE:-base,kms,gateway,o11y,mcp,ai,licensing,plans,pricing,amqp,metrics,vfs,authz}"
|
||||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||
log() { printf '[smoke-runtime] %s\n' "$*" >&2; }
|
||||
|
||||
cleanup() {
|
||||
local rc=$?
|
||||
if [[ "${KEEP_RUNNING}" != "1" ]]; then
|
||||
if [[ -n "${CLOUD_PID:-}" ]] && kill -0 "${CLOUD_PID}" 2>/dev/null; then
|
||||
log "stopping cloud (pid=${CLOUD_PID})"
|
||||
kill "${CLOUD_PID}" 2>/dev/null || true
|
||||
wait "${CLOUD_PID}" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -d "${DATA_DIR}" && "${DATA_DIR}" == /tmp/cloud-smoke.* ]]; then
|
||||
rm -rf "${DATA_DIR}"
|
||||
fi
|
||||
else
|
||||
log "KEEP_RUNNING=1: cloud (pid=${CLOUD_PID:-?}) left up; logs at ${LOG_FILE}"
|
||||
fi
|
||||
exit "${rc}"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
if [[ ! -x "${BIN}" ]]; then
|
||||
log "building ${BIN}"
|
||||
mkdir -p "$(dirname "${BIN}")"
|
||||
go build -ldflags='-s -w' -o "${BIN}" ./cmd/cloud
|
||||
fi
|
||||
|
||||
log "data dir: ${DATA_DIR}"
|
||||
log "log file: ${LOG_FILE}"
|
||||
log "starting: ${BIN} --brand=smoke --data-dir=${DATA_DIR} --enable=${ENABLE} --listen=${LISTEN}"
|
||||
|
||||
"${BIN}" \
|
||||
--brand=smoke \
|
||||
--domain=cloud.smoke.local \
|
||||
--data-dir="${DATA_DIR}" \
|
||||
--enable="${ENABLE}" \
|
||||
--listen="${LISTEN}" \
|
||||
> "${LOG_FILE}" 2>&1 &
|
||||
CLOUD_PID=$!
|
||||
|
||||
# Wait for the listener to come up. We poll /healthz; if it doesn't bind
|
||||
# within BOOT_TIMEOUT, dump the log tail and exit non-zero.
|
||||
log "waiting for ${LISTEN} (timeout ${BOOT_TIMEOUT}s)…"
|
||||
for i in $(seq 1 "${BOOT_TIMEOUT}"); do
|
||||
if curl -fsS -o /dev/null "http://${LISTEN}/healthz" 2>/dev/null; then
|
||||
log "listener up after ${i}s"
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "${CLOUD_PID}" 2>/dev/null; then
|
||||
red "cloud exited before listener came up. Last log lines:"
|
||||
tail -40 "${LOG_FILE}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! curl -fsS -o /dev/null "http://${LISTEN}/healthz" 2>/dev/null; then
|
||||
red "timed out waiting for /healthz on ${LISTEN}. Last log lines:"
|
||||
tail -40 "${LOG_FILE}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Probe matrix. Format: "<expected_status> <path>".
|
||||
# Values match the verified runtime contract per the HIP-0106 runbook:
|
||||
# /healthz 200 — process health probe
|
||||
# /v1/models 200 — catalog endpoint (no auth)
|
||||
# /v1/plans 200 — plansvc catalog (goja-hosted)
|
||||
# /v1/pricing 200 — pricingsvc catalog (goja-hosted)
|
||||
# /v1/base/collections 401 — base subsystem alive, auth-gated
|
||||
PROBES=(
|
||||
"200 /healthz"
|
||||
"200 /v1/models"
|
||||
"200 /v1/plans"
|
||||
"200 /v1/pricing"
|
||||
"401 /v1/base/collections"
|
||||
)
|
||||
|
||||
failed=0
|
||||
for probe in "${PROBES[@]}"; do
|
||||
expected="${probe%% *}"
|
||||
path="${probe#* }"
|
||||
actual="$(curl -sS -o /dev/null -w '%{http_code}' "http://${LISTEN}${path}" || echo 'ERR')"
|
||||
if [[ "${actual}" == "${expected}" ]]; then
|
||||
green " OK ${path} -> ${actual}"
|
||||
else
|
||||
red " FAIL ${path} -> expected=${expected} actual=${actual}"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${failed}" -gt 0 ]]; then
|
||||
red "${failed} probe(s) failed. Tail of cloud log:"
|
||||
tail -60 "${LOG_FILE}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
green "all ${#PROBES[@]} probes passed."
|
||||
@@ -0,0 +1,93 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/hanzoai/zip"
|
||||
"github.com/hanzoai/zip/middleware"
|
||||
)
|
||||
|
||||
// Serve boots the canonical compose root and mounts the selected subsystems.
|
||||
//
|
||||
// This is the ONE place the cloud-server body lives. cmd/cloud (the full fused
|
||||
// surface) and every `hanzo <svc>` subcommand share it; no boot logic is
|
||||
// duplicated per entrypoint.
|
||||
//
|
||||
// enable==nil ⇒ honor cfg.Enable from flags/env (cloud mode; empty = all).
|
||||
// enable!=nil ⇒ force exactly that set (single-service mode), overriding
|
||||
// --enable so `hanzo kms` is unambiguous.
|
||||
//
|
||||
// Serve registers the HIP-0106 liveness contract (GET /v1/<name>/health for
|
||||
// every enabled subsystem) before MountAll, runs the canonical middleware
|
||||
// pipeline (Recover → RequestID → Logger), and shuts down gracefully on
|
||||
// SIGINT/SIGTERM.
|
||||
func Serve(enable []string) error {
|
||||
cfg := LoadConfig()
|
||||
if enable != nil {
|
||||
cfg.Enable = enable
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
|
||||
deps := BuildDeps(cfg)
|
||||
|
||||
app := zip.New(zip.Config{Logger: deps.Logger})
|
||||
|
||||
// Canonical middleware pipeline. Order matters:
|
||||
// 1. Recover — panic → JSON 500
|
||||
// 2. RequestID — generate / propagate X-Request-Id
|
||||
// 3. Logger — request-line log
|
||||
// Telemetry/Auth stay gateway-owned in Phase 1 (enable once mounted).
|
||||
app.Use(middleware.Recover())
|
||||
app.Use(middleware.RequestID())
|
||||
app.Use(middleware.Logger(deps.Logger))
|
||||
|
||||
// HIP-0106 liveness contract: every enabled subsystem answers
|
||||
// GET /v1/<name>/health uniformly, registered at the compose root before
|
||||
// MountAll so it precedes subsystem /v1/<n>/* wildcards.
|
||||
for _, spec := range Registry {
|
||||
if !cfg.Enabled(spec.Name) {
|
||||
continue
|
||||
}
|
||||
name := spec.Name
|
||||
app.Get("/v1/"+name+"/health", func(c *zip.Ctx) error {
|
||||
return c.JSON(200, map[string]string{"service": name, "status": "ok"})
|
||||
})
|
||||
}
|
||||
|
||||
if err := MountAll(app, cfg, deps); err != nil {
|
||||
return fmt.Errorf("mount: %w", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
listenErr := make(chan error, 1)
|
||||
go func() {
|
||||
deps.Logger.Info("listening",
|
||||
"http", cfg.ListenAddr,
|
||||
"zap", cfg.ZAPListenAddr,
|
||||
"enabled", cfg.Enable,
|
||||
"brand", cfg.Brand,
|
||||
"domain", cfg.Domain,
|
||||
)
|
||||
listenErr <- app.Listen(cfg.ListenAddr)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
deps.Logger.Info("shutdown requested")
|
||||
case err := <-listenErr:
|
||||
return fmt.Errorf("listen: %w", err)
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
return app.ShutdownWithContext(shutdownCtx)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package subsystems is the single source of truth for which Hanzo cloud
|
||||
// subsystems are linked into a binary.
|
||||
//
|
||||
// Blank-importing this package pulls every subsystem into the build graph;
|
||||
// each one registers a cloud.MountSpec into cloud.Registry from its own
|
||||
// init() (gated by //go:build cloud in the subsystem package, so the
|
||||
// registrations fire only when built with -tags cloud).
|
||||
//
|
||||
// Both entrypoints — cmd/cloud (the full fused surface) and cmd/hanzo (the
|
||||
// subcommand dispatcher) — blank-import THIS package and nothing else. The
|
||||
// subsystem set is therefore defined ONCE, here; adding or removing a
|
||||
// subsystem is a one-line change in one file, never duplicated per binary.
|
||||
//
|
||||
// (This package must NOT live in the root `cloud` package: the subsystems
|
||||
// import `cloud` for Deps + Register, so a root-package bundle would form an
|
||||
// import cycle. As a sibling subpackage it composes them without one.)
|
||||
package subsystems
|
||||
|
||||
import (
|
||||
_ "github.com/hanzoai/ai" // order 150
|
||||
_ "github.com/hanzoai/amqp" // order 30
|
||||
_ "github.com/hanzoai/authz" // order 70
|
||||
_ "github.com/hanzoai/base" // order 60
|
||||
_ "github.com/hanzoai/commerce" // order 100
|
||||
_ "github.com/hanzoai/gateway" // order 80
|
||||
_ "github.com/hanzoai/iam/pkg/iam" // order 50 (Mount lives in the pkg/iam submodule)
|
||||
_ "github.com/hanzoai/ingress" // order 90
|
||||
_ "github.com/hanzoai/kms" // order 10 (thin wrapper over the canonical luxfi/kms)
|
||||
_ "github.com/hanzoai/licensing" // order 110 (after iam + commerce)
|
||||
_ "github.com/hanzoai/mcp/go" // order 160 (Mount lives in the go submodule)
|
||||
_ "github.com/hanzoai/metrics" // order 40
|
||||
_ "github.com/hanzoai/o11y" // order 70
|
||||
_ "github.com/hanzoai/vfs" // order 20
|
||||
|
||||
// Node-service subsystems hosted in-process via base+goja (HIP-0106);
|
||||
// the JS + catalog data live in hanzoai/plans, hanzoai/pricing.
|
||||
_ "github.com/hanzoai/cloud/clients/plansvc" // order 111 — /v1/plans/*
|
||||
_ "github.com/hanzoai/cloud/clients/pricingsvc" // order 112 — /v1/pricing/*
|
||||
)
|
||||
@@ -46,6 +46,42 @@ type TenantConfig struct {
|
||||
Brand string
|
||||
}
|
||||
|
||||
// LicenseEntitlement is commerce's answer to "does this org/user hold an
|
||||
// active entitlement for licensed product X, and what does its plan grant?".
|
||||
//
|
||||
// It is the inter-subsystem transport for the entitlement-flow that gates
|
||||
// licensing token issuance (commerce → licensing → engine). The licensing
|
||||
// subsystem copies Features verbatim into the signed token's `features`
|
||||
// list so the proprietary engine's offline release gate (hasFeatures)
|
||||
// enforces exactly the plan the buyer paid for.
|
||||
//
|
||||
// Features is the FLAT capability list produced from the canonical
|
||||
// entitlement vocabulary by the data plane's toLicenseFeatures contract
|
||||
// (@hanzo/plans entitlements.mjs): licensing.engine_features verbatim,
|
||||
// plus derived capability tokens (e.g. "ai.premium", "training",
|
||||
// "tools.<name>"), plus scoping tokens ("licensing.app:<id>",
|
||||
// "licensing.product:<id>"). Numeric quotas (tokens_per_min, seats,
|
||||
// max_vms, …) ride out of band and are NOT encoded here.
|
||||
type LicenseEntitlement struct {
|
||||
// ProductID is the licensed product the entitlement was checked for
|
||||
// (e.g. "engine", "engine-rocm", a plugin id).
|
||||
ProductID string
|
||||
// Active reports whether the entitlement is currently valid (paid,
|
||||
// not lapsed/cancelled). Licensing refuses to mint when false.
|
||||
Active bool
|
||||
// Plan is the resolved plan/tier id (e.g. "developer", "pro", "max",
|
||||
// "enterprise"). Surfaced for logging/audit; not load-bearing for the
|
||||
// release gate.
|
||||
Plan string
|
||||
// Features is the flat license-feature list per the toLicenseFeatures
|
||||
// vocab contract — copied verbatim into License.Features at issue.
|
||||
Features []string
|
||||
// ExpiresUnix bounds the entitlement (unix seconds, 0 = no bound). The
|
||||
// issued token's exp is clamped to it so a token never outlives the
|
||||
// entitlement.
|
||||
ExpiresUnix int64
|
||||
}
|
||||
|
||||
// ChatRequest mirrors the AI subsystem's chat-completion request.
|
||||
type ChatRequest struct {
|
||||
Model string
|
||||
@@ -116,6 +152,14 @@ type BaseClient interface {
|
||||
// CommerceClient is the inter-subsystem interface to Commerce.
|
||||
type CommerceClient interface {
|
||||
GetTenantConfig(ctx context.Context, orgID string) (*TenantConfig, error)
|
||||
// CheckEntitlement reports whether org `orgID` holds an active
|
||||
// entitlement for licensed product `productID`, and returns the plan's
|
||||
// flat license-features per the toLicenseFeatures vocab contract. Used
|
||||
// by the licensing subsystem to gate + scope token issuance. orgID is
|
||||
// the tenant the buyer acts as (X-Org-Id); when callers only have a
|
||||
// user subject they pass it through here and commerce resolves the
|
||||
// owning org.
|
||||
CheckEntitlement(ctx context.Context, orgID, productID string) (*LicenseEntitlement, error)
|
||||
}
|
||||
|
||||
// AIClient is the inter-subsystem interface to AI.
|
||||
|
||||
Reference in New Issue
Block a user