mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
core.App.Logger() now returns logger.Logger (the new shim interface) instead of *slog.Logger, so every consumer talks to the same backend-agnostic surface and stops pinning itself to log/slog. The raw *slog.Logger is still reachable as App.SlogLogger() for the in-app BatchHandler reload hook and the two tests that type-assert the underlying handler — that's the only sanctioned escape hatch. Plugins and network code that previously held *slog.Logger fields move to luxlog.Logger (kms, vault, zap-transport, base-network/transport_zap, network/archive_writer, tools/tasks/client). luxfi/zap's Logger field was a pure transit dependency; we drop it from base call sites since zaplib has its own default. Remaining "log/slog" imports — 11 files — are confined to the adapter/handler bridge (tools/logger and core/log_printer/base/app) and to two tests that exercise the slog.Handler contract directly. Those are the irreducible boundary where slog is the language of the interface.
349 lines
8.9 KiB
Go
349 lines
8.9 KiB
Go
package zap
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
luxlog "github.com/luxfi/log"
|
|
zaplib "github.com/luxfi/zap"
|
|
)
|
|
|
|
// ZAP message types for Base operations.
|
|
const (
|
|
MsgTypeCollections uint16 = 100
|
|
MsgTypeRecords uint16 = 101
|
|
MsgTypeAuth uint16 = 102
|
|
MsgTypeRealtime uint16 = 103
|
|
)
|
|
|
|
// ZAP field offsets (matching orm/db/zap.go protocol).
|
|
const (
|
|
fieldPath = 4 // Text: request path
|
|
fieldBody = 12 // Bytes: JSON body
|
|
respStatus = 0 // Uint32: HTTP-style status
|
|
respBody = 4 // Bytes: response JSON
|
|
)
|
|
|
|
// handler wraps a Base app to handle ZAP messages.
|
|
type handler struct {
|
|
app core.App
|
|
logger luxlog.Logger
|
|
node *zaplib.Node
|
|
|
|
// Realtime subscriptions: peer -> set of collection IDs/names
|
|
subsMu sync.RWMutex
|
|
subs map[string]map[string]struct{}
|
|
}
|
|
|
|
func newHandler(app core.App, logger luxlog.Logger) *handler {
|
|
return &handler{
|
|
app: app,
|
|
logger: logger,
|
|
subs: make(map[string]map[string]struct{}),
|
|
}
|
|
}
|
|
|
|
// setNode sets the ZAP node reference for push notifications.
|
|
func (h *handler) setNode(node *zaplib.Node) {
|
|
h.node = node
|
|
}
|
|
|
|
// handleCollections handles collection CRUD via ZAP.
|
|
func (h *handler) handleCollections(ctx context.Context, from string, msg *zaplib.Message) (*zaplib.Message, error) {
|
|
root := msg.Root()
|
|
path := root.Text(fieldPath)
|
|
body := root.Bytes(fieldBody)
|
|
|
|
h.logger.Debug("zap: collections", "path", path, "from", from, "bodyLen", len(body))
|
|
|
|
var req map[string]interface{}
|
|
if len(body) > 0 {
|
|
json.Unmarshal(body, &req)
|
|
}
|
|
|
|
var result interface{}
|
|
var err error
|
|
|
|
switch path {
|
|
case "/list":
|
|
collections, findErr := h.app.FindAllCollections()
|
|
if findErr != nil {
|
|
return h.errorResponse(500, findErr.Error())
|
|
}
|
|
result = collections
|
|
|
|
case "/get":
|
|
name, _ := req["name"].(string)
|
|
if name == "" {
|
|
return h.errorResponse(400, "collection name required")
|
|
}
|
|
col, findErr := h.app.FindCollectionByNameOrId(name)
|
|
if findErr != nil {
|
|
return h.errorResponse(404, "collection not found")
|
|
}
|
|
result = col
|
|
|
|
default:
|
|
return h.errorResponse(400, fmt.Sprintf("unknown collections path: %s", path))
|
|
}
|
|
|
|
if err != nil {
|
|
return h.errorResponse(500, err.Error())
|
|
}
|
|
return h.jsonResponse(200, result)
|
|
}
|
|
|
|
// handleRecords handles record CRUD via ZAP.
|
|
func (h *handler) handleRecords(ctx context.Context, from string, msg *zaplib.Message) (*zaplib.Message, error) {
|
|
root := msg.Root()
|
|
path := root.Text(fieldPath)
|
|
body := root.Bytes(fieldBody)
|
|
|
|
h.logger.Debug("zap: records", "path", path, "from", from)
|
|
|
|
var req map[string]interface{}
|
|
if len(body) > 0 {
|
|
json.Unmarshal(body, &req)
|
|
}
|
|
|
|
collection, _ := req["collection"].(string)
|
|
if collection == "" {
|
|
return h.errorResponse(400, "collection required")
|
|
}
|
|
|
|
col, err := h.app.FindCollectionByNameOrId(collection)
|
|
if err != nil {
|
|
return h.errorResponse(404, "collection not found")
|
|
}
|
|
|
|
switch path {
|
|
case "/list":
|
|
records, findErr := h.app.FindAllRecords(col)
|
|
if findErr != nil {
|
|
return h.errorResponse(500, findErr.Error())
|
|
}
|
|
return h.jsonResponse(200, records)
|
|
|
|
case "/get":
|
|
id, _ := req["id"].(string)
|
|
if id == "" {
|
|
return h.errorResponse(400, "record id required")
|
|
}
|
|
record, findErr := h.app.FindRecordById(col, id)
|
|
if findErr != nil {
|
|
return h.errorResponse(404, "record not found")
|
|
}
|
|
return h.jsonResponse(200, record)
|
|
|
|
case "/create":
|
|
record := core.NewRecord(col)
|
|
data, _ := req["data"].(map[string]interface{})
|
|
for k, v := range data {
|
|
record.Set(k, v)
|
|
}
|
|
if saveErr := h.app.Save(record); saveErr != nil {
|
|
return h.errorResponse(400, saveErr.Error())
|
|
}
|
|
return h.jsonResponse(200, record)
|
|
|
|
case "/update":
|
|
id, _ := req["id"].(string)
|
|
record, findErr := h.app.FindRecordById(col, id)
|
|
if findErr != nil {
|
|
return h.errorResponse(404, "record not found")
|
|
}
|
|
data, _ := req["data"].(map[string]interface{})
|
|
for k, v := range data {
|
|
record.Set(k, v)
|
|
}
|
|
if saveErr := h.app.Save(record); saveErr != nil {
|
|
return h.errorResponse(400, saveErr.Error())
|
|
}
|
|
return h.jsonResponse(200, record)
|
|
|
|
case "/delete":
|
|
id, _ := req["id"].(string)
|
|
record, findErr := h.app.FindRecordById(col, id)
|
|
if findErr != nil {
|
|
return h.errorResponse(404, "record not found")
|
|
}
|
|
if delErr := h.app.Delete(record); delErr != nil {
|
|
return h.errorResponse(500, delErr.Error())
|
|
}
|
|
return h.jsonResponse(200, map[string]string{"deleted": id})
|
|
|
|
default:
|
|
return h.errorResponse(400, fmt.Sprintf("unknown records path: %s", path))
|
|
}
|
|
}
|
|
|
|
// handleAuth handles auth operations via ZAP.
|
|
//
|
|
// The legacy /identity local-password path is retired — Hanzo IAM is
|
|
// the only credential issuer. The ZAP transport mirrors the HTTP
|
|
// surface and returns 410 Gone for that path so callers stop retrying;
|
|
// see apis.externalAuthGuard for the HTTP equivalent.
|
|
func (h *handler) handleAuth(ctx context.Context, from string, msg *zaplib.Message) (*zaplib.Message, error) {
|
|
root := msg.Root()
|
|
path := root.Text(fieldPath)
|
|
|
|
h.logger.Debug("zap: auth", "path", path, "from", from)
|
|
|
|
switch path {
|
|
case "/identity":
|
|
return h.errorResponse(410, "local-password auth is retired — use the IAM PKCE flow")
|
|
|
|
default:
|
|
return h.errorResponse(400, fmt.Sprintf("unknown auth path: %s", path))
|
|
}
|
|
}
|
|
|
|
// handleRealtime handles realtime subscription management via ZAP.
|
|
func (h *handler) handleRealtime(ctx context.Context, from string, msg *zaplib.Message) (*zaplib.Message, error) {
|
|
root := msg.Root()
|
|
path := root.Text(fieldPath)
|
|
body := root.Bytes(fieldBody)
|
|
|
|
h.logger.Debug("zap: realtime", "path", path, "from", from)
|
|
|
|
var req map[string]interface{}
|
|
if len(body) > 0 {
|
|
json.Unmarshal(body, &req)
|
|
}
|
|
|
|
switch path {
|
|
case "/subscribe":
|
|
collections, _ := req["collections"].([]interface{})
|
|
if len(collections) == 0 {
|
|
return h.errorResponse(400, "collections array required")
|
|
}
|
|
|
|
h.subsMu.Lock()
|
|
if h.subs[from] == nil {
|
|
h.subs[from] = make(map[string]struct{})
|
|
}
|
|
subscribed := make([]string, 0, len(collections))
|
|
for _, c := range collections {
|
|
if name, ok := c.(string); ok && name != "" {
|
|
h.subs[from][name] = struct{}{}
|
|
subscribed = append(subscribed, name)
|
|
}
|
|
}
|
|
h.subsMu.Unlock()
|
|
|
|
h.logger.Info("zap: subscribed", "peer", from, "collections", subscribed)
|
|
return h.jsonResponse(200, map[string]interface{}{
|
|
"subscribed": subscribed,
|
|
})
|
|
|
|
case "/unsubscribe":
|
|
collections, _ := req["collections"].([]interface{})
|
|
|
|
h.subsMu.Lock()
|
|
if len(collections) == 0 {
|
|
// Unsubscribe from all
|
|
delete(h.subs, from)
|
|
} else {
|
|
if peerSubs := h.subs[from]; peerSubs != nil {
|
|
for _, c := range collections {
|
|
if name, ok := c.(string); ok {
|
|
delete(peerSubs, name)
|
|
}
|
|
}
|
|
if len(peerSubs) == 0 {
|
|
delete(h.subs, from)
|
|
}
|
|
}
|
|
}
|
|
h.subsMu.Unlock()
|
|
|
|
return h.jsonResponse(200, map[string]string{"status": "unsubscribed"})
|
|
|
|
case "/status":
|
|
h.subsMu.RLock()
|
|
peerSubs := h.subs[from]
|
|
cols := make([]string, 0, len(peerSubs))
|
|
for c := range peerSubs {
|
|
cols = append(cols, c)
|
|
}
|
|
h.subsMu.RUnlock()
|
|
|
|
return h.jsonResponse(200, map[string]interface{}{
|
|
"subscriptions": cols,
|
|
})
|
|
|
|
default:
|
|
return h.errorResponse(400, fmt.Sprintf("unknown realtime path: %s", path))
|
|
}
|
|
}
|
|
|
|
// broadcastEvent pushes a record change event to all subscribed peers.
|
|
func (h *handler) broadcastEvent(collection string, action string, record *core.Record) {
|
|
if h.node == nil {
|
|
return
|
|
}
|
|
|
|
h.subsMu.RLock()
|
|
defer h.subsMu.RUnlock()
|
|
|
|
payload, _ := json.Marshal(map[string]interface{}{
|
|
"action": action,
|
|
"collection": collection,
|
|
"record": record,
|
|
})
|
|
|
|
for peer, collections := range h.subs {
|
|
if _, ok := collections[collection]; !ok {
|
|
continue
|
|
}
|
|
|
|
msg, err := h.buildResponse(200, payload)
|
|
if err != nil {
|
|
h.logger.Warn("zap: failed to build broadcast", "error", err)
|
|
continue
|
|
}
|
|
|
|
if err := h.node.Send(context.Background(), peer, msg); err != nil {
|
|
h.logger.Debug("zap: send to peer failed, removing subscription",
|
|
"peer", peer, "error", err)
|
|
// Remove dead peer (defer unlock already held as RLock, so schedule cleanup)
|
|
go func(p string) {
|
|
h.subsMu.Lock()
|
|
delete(h.subs, p)
|
|
h.subsMu.Unlock()
|
|
}(peer)
|
|
}
|
|
}
|
|
}
|
|
|
|
// jsonResponse builds a ZAP response message with JSON body.
|
|
func (h *handler) jsonResponse(status uint32, data interface{}) (*zaplib.Message, error) {
|
|
body, _ := json.Marshal(data)
|
|
return h.buildResponse(status, body)
|
|
}
|
|
|
|
// errorResponse builds a ZAP error response.
|
|
func (h *handler) errorResponse(status uint32, message string) (*zaplib.Message, error) {
|
|
body, _ := json.Marshal(map[string]string{"error": message})
|
|
return h.buildResponse(status, body)
|
|
}
|
|
|
|
// buildResponse constructs a ZAP response message.
|
|
func (h *handler) buildResponse(status uint32, body []byte) (*zaplib.Message, error) {
|
|
b := zaplib.NewBuilder(len(body) + 128)
|
|
obj := b.StartObject(20)
|
|
obj.SetUint32(respStatus, status)
|
|
obj.SetBytes(respBody, body)
|
|
obj.FinishAsRoot()
|
|
data := b.Finish()
|
|
|
|
msg, err := zaplib.Parse(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zap: build response: %w", err)
|
|
}
|
|
return msg, nil
|
|
}
|