mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723
The one bundle that did everything becomes the manager bundle, which runs long and answers the seat's ten verbs and the three acting tools, and a tools bundle of its own that answers the four reading tools and writes nothing, so they answer while the manager fails to load or stops. Both import the module's one package for the layouts of the kept data and the log entries, the key form, the entries' names and the settings with their validity; the settings file and the account with its directory stay as they are. Builds: design 39 parts §1, §2, §3, §4, §5, §6, §7, §8
322 lines
9.4 KiB
Go
322 lines
9.4 KiB
Go
package main
|
|
|
|
// The manager's kept data on the bus (design 39 bus-and-switch §2, §3, B11, B22): the state `licences`, one key per
|
|
// licence by its key form (§10), and the state `bindings`, one key per consumer, its value the form the consumers
|
|
// have always read. Written by the manager alone.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
|
|
. "claude-licence-manager/internal/licences"
|
|
)
|
|
|
|
// BusStore keeps the manager's data in the bus's own storage.
|
|
type BusStore struct {
|
|
LicencesState State
|
|
BindingsState State
|
|
LeasesState State
|
|
OfferedState State
|
|
UsageLog Log
|
|
AuditLog Log
|
|
Now func() time.Time
|
|
}
|
|
|
|
func (s *BusStore) get(st State, key string, into any) (bool, error) {
|
|
e, err := st.Get(key)
|
|
if err != nil || e == nil || len(e.Value) == 0 || string(e.Value) == "null" {
|
|
return false, err
|
|
}
|
|
return true, json.Unmarshal(e.Value, into)
|
|
}
|
|
|
|
func (s *BusStore) Licences(context.Context) ([]Licence, error) {
|
|
keys, err := s.LicencesState.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := []Licence{}
|
|
for _, k := range keys {
|
|
var l Licence
|
|
if ok, err := s.get(s.LicencesState, k, &l); err != nil {
|
|
return nil, err
|
|
} else if ok {
|
|
out = append(out, l)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out, nil
|
|
}
|
|
|
|
func (s *BusStore) Licence(_ context.Context, name string) (*Licence, error) {
|
|
var l Licence
|
|
ok, err := s.get(s.LicencesState, KeyFor(name), &l)
|
|
if err != nil || !ok || l.Name != name {
|
|
return nil, err
|
|
}
|
|
return &l, nil
|
|
}
|
|
|
|
func (s *BusStore) LicenceForAccount(ctx context.Context, account string) (*Licence, error) {
|
|
all, err := s.Licences(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, l := range all {
|
|
if l.AccountUUID == account {
|
|
return &l, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *BusStore) SaveLicence(_ context.Context, l Licence) error {
|
|
if l.Name == "" {
|
|
return errors.New("a licence has a name")
|
|
}
|
|
_, err := s.LicencesState.Put(KeyFor(l.Name), l)
|
|
return err
|
|
}
|
|
|
|
func (s *BusStore) Bindings(context.Context) ([]Binding, error) {
|
|
keys, err := s.BindingsState.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := []Binding{}
|
|
for _, k := range keys {
|
|
if b, err := s.binding(k); err != nil {
|
|
return nil, err
|
|
} else if b != nil {
|
|
out = append(out, *b)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Consumer < out[j].Consumer })
|
|
return out, nil
|
|
}
|
|
|
|
func (s *BusStore) binding(consumer string) (*Binding, error) {
|
|
var v BindingState
|
|
ok, err := s.get(s.BindingsState, consumer, &v)
|
|
if err != nil || !ok {
|
|
return nil, err
|
|
}
|
|
return &Binding{Consumer: consumer, Licence: v.Licence, Kind: v.Kind, Generation: v.Generation, Since: v.Since,
|
|
Why: v.Why, By: v.By}, nil
|
|
}
|
|
|
|
func (s *BusStore) Binding(_ context.Context, consumer string) (*Binding, error) {
|
|
return s.binding(consumer)
|
|
}
|
|
|
|
// Bind writes a consumer's key at its next generation: one more than the higher of its own and the floor (B11).
|
|
func (s *BusStore) Bind(_ context.Context, consumer, licence string, m Move) (Binding, error) {
|
|
before, err := s.binding(consumer)
|
|
if err != nil {
|
|
return Binding{}, err
|
|
}
|
|
generation := m.floor(consumer)
|
|
if before != nil {
|
|
generation = max(generation, before.Generation)
|
|
}
|
|
b := Binding{Consumer: consumer, Licence: licence, Kind: m.Kind, Generation: generation + 1,
|
|
Since: s.Now().UTC().Format(time.RFC3339), Why: m.Why, By: m.By}
|
|
_, err = s.BindingsState.Put(consumer, BindingState{Licence: b.Licence, Kind: b.Kind, Generation: b.Generation,
|
|
Since: b.Since, Why: b.Why, By: b.By})
|
|
return b, err
|
|
}
|
|
|
|
func (s *BusStore) Unbind(_ context.Context, consumer string) (bool, error) {
|
|
before, err := s.binding(consumer)
|
|
if err != nil || before == nil {
|
|
return false, err
|
|
}
|
|
return true, s.BindingsState.Delete(consumer)
|
|
}
|
|
|
|
// Advance gives every consumer bound to licence its next generation, keeping its licence and since.
|
|
func (s *BusStore) Advance(ctx context.Context, licence string, m Move) ([]Binding, error) {
|
|
all, err := s.Bindings(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []Binding
|
|
for _, b := range all {
|
|
if b.Licence != licence {
|
|
continue
|
|
}
|
|
b.Generation = max(b.Generation, m.floor(b.Consumer)) + 1
|
|
b.Kind, b.Why, b.By = m.Kind, m.Why, m.By
|
|
if _, err := s.BindingsState.Put(b.Consumer, BindingState{Licence: b.Licence, Kind: b.Kind,
|
|
Generation: b.Generation, Since: b.Since, Why: b.Why, By: b.By}); err != nil {
|
|
return out, err
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ---- leases and offers (§4, §5) ---------------------------------------------------------------------------------
|
|
|
|
// Lease takes a lease by creating its key with a lifetime of d (§4): a create fails while another holds it, the bus's
|
|
// own compare-and-set. A key its own holder holds is taken again.
|
|
func (s *BusStore) Lease(_ context.Context, key, holder string, d time.Duration) (bool, error) {
|
|
v := LeaseValue{Holder: holder, Until: s.Now().Add(d).UTC().Format(time.RFC3339)}
|
|
_, err := s.LeasesState.CreateTTL(key, v, d)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if !errors.Is(err, stdio.ErrChanged) {
|
|
return false, err
|
|
}
|
|
var held LeaseValue
|
|
if ok, gerr := s.get(s.LeasesState, key, &held); gerr != nil || !ok || held.Holder != holder {
|
|
return false, gerr
|
|
}
|
|
_, err = s.LeasesState.PutTTL(key, v, d)
|
|
return err == nil, err
|
|
}
|
|
|
|
// Unlease deletes a lease its holder holds; another's is left to it or its lifetime.
|
|
func (s *BusStore) Unlease(_ context.Context, key, holder string) error {
|
|
var held LeaseValue
|
|
if ok, err := s.get(s.LeasesState, key, &held); err != nil || !ok || held.Holder != holder {
|
|
return err
|
|
}
|
|
return s.LeasesState.Delete(key)
|
|
}
|
|
|
|
// offeredFor is how long the outcome of an offered grant is kept (§5): longer than a refresh token lives.
|
|
const offeredFor = 90 * 24 * time.Hour
|
|
|
|
// OfferedKey is a fingerprint's key in `offered`: through the licence key form (B13).
|
|
func OfferedKey(fingerprint string) string { return KeyFor(fingerprint) }
|
|
|
|
func (s *BusStore) Outcome(_ context.Context, fingerprint string) (Outcome, error) {
|
|
var v OfferedValue
|
|
if ok, err := s.get(s.OfferedState, OfferedKey(fingerprint), &v); err != nil || !ok {
|
|
return "", err
|
|
}
|
|
return v.Outcome, nil
|
|
}
|
|
|
|
func (s *BusStore) RecordOutcome(_ context.Context, fingerprint, node, account string, o Outcome, why string) error {
|
|
return s.putOffered(fingerprint, OfferedValue{Node: node, Account: account, Outcome: o, Why: why,
|
|
At: s.Now().UTC().Format(time.RFC3339)}, offeredFor)
|
|
}
|
|
|
|
func (s *BusStore) putOffered(fingerprint string, v OfferedValue, ttl time.Duration) error {
|
|
_, err := s.OfferedState.PutTTL(OfferedKey(fingerprint), v, ttl)
|
|
return err
|
|
}
|
|
|
|
// ---- the logs (§6, §7) ------------------------------------------------------------------------------------------
|
|
|
|
func (s *BusStore) RecordUsage(_ context.Context, licence string, at int64, r UsageReading, raw map[string]any) error {
|
|
_, err := s.UsageLog.Append(KeyFor(licence), UsageEntry{At: time.UnixMilli(at).UTC().Format(time.RFC3339Nano),
|
|
Reading: r, Raw: raw})
|
|
return err
|
|
}
|
|
|
|
// Usage is the latest readings, newest first, of one licence or every one, none before since, the vendor's answer
|
|
// with raw (§35).
|
|
func (s *BusStore) Usage(ctx context.Context, licence string, since time.Time, limit int, raw bool) ([]UsageRow, error) {
|
|
names := map[string]string{}
|
|
all, err := s.Licences(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, l := range all {
|
|
names[KeyFor(l.Name)] = l.Name
|
|
}
|
|
keys := ""
|
|
if licence != "" {
|
|
keys = KeyFor(licence)
|
|
}
|
|
entries, err := s.newest(s.UsageLog, keys, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := []UsageRow{}
|
|
for _, e := range entries {
|
|
var u UsageEntry
|
|
if json.Unmarshal(e.Value, &u) != nil {
|
|
continue
|
|
}
|
|
at, _ := time.Parse(time.RFC3339Nano, u.At)
|
|
if at.Before(since) {
|
|
continue
|
|
}
|
|
name := names[e.Key]
|
|
if name == "" {
|
|
name = e.Key
|
|
}
|
|
row := UsageRow{Licence: name, At: at.UnixMilli(), Reading: u.Reading}
|
|
if raw {
|
|
row.Raw = u.Raw
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// newest is up to limit entries of keys, newest first: read back from the log's last sequence, a window that doubles
|
|
// until it holds limit entries or reaches the first.
|
|
func (s *BusStore) newest(l Log, keys string, limit int) ([]stdio.LogEntry, error) {
|
|
info, err := l.Info()
|
|
if err != nil || info.Last == 0 {
|
|
return nil, err
|
|
}
|
|
for span := uint64(max(limit, 1)); ; span *= 2 {
|
|
from := uint64(1)
|
|
if info.Last > span {
|
|
from = info.Last - span + 1
|
|
}
|
|
var got []stdio.LogEntry
|
|
for next := from; next != 0; {
|
|
page, err := l.Read(next, keys, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
got = append(got, page.Entries...)
|
|
next = page.Next
|
|
}
|
|
if len(got) >= limit || from <= max(info.First, 1) {
|
|
if len(got) > limit {
|
|
got = got[len(got)-limit:]
|
|
}
|
|
for i, j := 0, len(got)-1; i < j; i, j = i+1, j-1 {
|
|
got[i], got[j] = got[j], got[i]
|
|
}
|
|
return got, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Audit appends one entry under its kind, with when; a log that does not take it fails the act (B20).
|
|
func (s *BusStore) Audit(_ context.Context, what string, detail map[string]any) error {
|
|
kind := "drift"
|
|
for _, k := range AuditKinds {
|
|
if k == what {
|
|
kind = k
|
|
}
|
|
}
|
|
entry := map[string]any{"at": s.Now().UTC().Format(time.RFC3339Nano)}
|
|
if kind != what {
|
|
entry["what"] = what
|
|
}
|
|
for k, v := range detail {
|
|
entry[k] = v
|
|
}
|
|
if _, err := s.AuditLog.Append(kind, entry); err != nil {
|
|
return fmt.Errorf("the audit did not take the %s entry, so the act is not done: %w", what, err)
|
|
}
|
|
return nil
|
|
}
|