link

package
v1.801.113 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package link is the unified AI login manager's registry: the org+user-scoped record of WHICH provider accounts (Claude Max, ChatGPT Plus, a Hanzo API key, a raw provider key) a developer has signed into, ON WHICH MACHINES, with each account's latest usage snapshot. It is the cross-machine view console renders as "AI Providers / Accounts" and the source the redundancy route policy reads.

THE ATOM is a Link: one (user, device, provider, account) binding plus its kind, status, last-seen, and the latest usage snapshot. A device is the (machine, host, os) projection shared by a machine's Links — not a separate stored entity, so there is no device/link join and no orphan-device GC. A user's accounts across every machine are just that user's Links.

NO SECRET LIVES HERE. The provider's OAuth token / API key stays device-local (exactly as @hanzo/usage keeps it, reading each provider's own login to meter usage); the registry holds LINK METADATA + usage snapshots only. The collector (@hanzo/usage's reporter) registers a Link and pushes its usage using the SAME IAM bearer it already carries — never a provider secret.

BILLING FOLLOWS THE ACCOUNT, not the registry. This store never touches commerce — it holds no metering client, so it is structurally incapable of creating a charge. A subscription account's inference bills the user's monthly plan (metered here for visibility only); an api-key / hanzo account's inference bills via commerce on the existing gateway path, unchanged. Each Link carries the mode that says which (BillingMode), so a usage event's billing is explicit.

ISOLATION: org is the tenant key and user is the owner key; every read and write leads with `org=? AND user=?` bound predicates, so a caller sees and mutates only their OWN accounts within their OWN org — fail-closed.

Index

Constants

View Source
const (
	// KindSubscription is a provider account signed in with the user's own
	// subscription login (Claude Max, ChatGPT Plus). Its inference bills the
	// user's monthly plan; the registry meters it for visibility only.
	KindSubscription = "subscription"
	// KindAPIKey is an account credentialed by an API key (a raw provider key,
	// or a Hanzo hk- key). Its inference bills via commerce on the gateway path.
	KindAPIKey = "apikey"
)

Link kinds — how an account is credentialed, which decides how its usage bills.

View Source
const (
	StatusLinked  = "linked"
	StatusRevoked = "revoked"
)

Link statuses. linked is active; revoked is a logged-out account whose sessions were stopped. A revoked Link is retained (not deleted) so its usage history and the audit trail survive a log-out.

View Source
const (
	// BillingPlan: the user's own subscription pays; NO commerce charge.
	BillingPlan = "plan"
	// BillingCommerce: usage bills via commerce (the gateway meter), as today.
	BillingCommerce = "commerce"
)

Billing modes — the value a Link and a route candidate carry so "how does this account's usage bill" is explicit, never inferred at the charge site.

Variables

This section is empty.

Functions

func BillingMode

func BillingMode(kind string) string

BillingMode returns how an account of this kind bills its usage: a subscription bills the user's plan (no commerce charge), everything else bills via commerce. It is the ONE place the subscription-vs-api-key distinction is decided, so a usage event's billing is a pure function of the account, never re-derived.

func Mount

func Mount(app *zip.App, deps cloud.Deps) error

Mount wires the /v1/links surface. The sessions seam is set from the agents in-process adapter (adapters.go) so a revoke can stop the affected sessions.

func Shutdown

func Shutdown(context.Context) error

Shutdown closes the store. Idempotent.

Types

type Link struct {
	ID       string
	Org      string
	User     string // the owning subject (validated principal); a user sees only their own
	Machine  string // stable machine id (the device key)
	Host     string // hostname (device label)
	OS       string // platform label (darwin|linux|windows|…)
	Provider string // matches @hanzo/usage providerRegistry id (claude|codex|hanzo|openai|…)
	Account  string // the subscription/account id or label (e.g. an account email)
	Plan     string // the plan name from the provider identity (e.g. "Claude Max"); display only
	Kind     string // subscription | apikey
	Status   string // linked | revoked
	LastSeen int64  // unix; bumped on every usage report
	Usage    string // JSON of the latest Usage projection ("" until first report)

	CreatedAt int64
	UpdatedAt int64
}

Link is one (user, device, provider, account) binding. Tenant isolation is the (org, user) pair, enforced on every query. It never stores a secret.

type RouteCandidate

type RouteCandidate struct {
	Provider    string  `json:"provider"`
	Account     string  `json:"account,omitempty"`
	Plan        string  `json:"plan,omitempty"`
	Kind        string  `json:"kind"`    // subscription | apikey
	Billing     string  `json:"billing"` // plan | commerce (BillingMode(Kind))
	Available   bool    `json:"available"`
	HeadroomPct float64 `json:"headroomPct"` // remaining capacity 0..100
	Machine     string  `json:"machine,omitempty"`
	Host        string  `json:"host,omitempty"`
	LinkID      string  `json:"linkId"`
	Reason      string  `json:"reason,omitempty"` // why unavailable, when Available=false
}

RouteCandidate is one account the policy would route to, in preference order, annotated with how it bills and whether it currently has rate-limit headroom.

type RoutePlan

type RoutePlan struct {
	Candidates  []RouteCandidate `json:"candidates"`
	Primary     *RouteCandidate  `json:"primary,omitempty"`
	GeneratedAt string           `json:"generatedAt"`
}

RoutePlan is the ordered redundancy plan for a user's accounts: the candidates in preference order plus the primary the caller would try first.

func Plan

func Plan(links []Link, now time.Time) RoutePlan

Plan builds the redundancy route plan from a user's LINKED accounts. Ordering: subscription accounts first (the flat-rate pool the user already pays for — used first, and failed over between for redundancy: two Claude Max accounts), then api-key/hanzo accounts (the metered API route). Within each group the order is (available first, then most headroom, then most-recently seen). The primary is the first available candidate; if every account is rate-limited it falls back to the first api-key account (the pay-per-call backstop is always usable) — else nil, an honest "nothing routable right now".

Only linked accounts should be passed (Store.ListLinked); a revoked account is never a candidate. The function is pure and total: any Links in, one plan out.

type SessionMatch

type SessionMatch struct {
	Subject  string
	Host     string
	Provider string
	Account  string
}

SessionMatch selects the live sessions a revoke stops. Fields are ANDed with the org; an empty field is "any". A link revoke matches {Host,Provider,Account} (the device+account the sessions ran under); a device revoke matches {Host}. Subject is the REVOKING user; the adapter turns it into the session Actor so a stop only ever reaches that user's OWN sessions — Host/Provider/Account (attacker-set at link upsert) can then only narrow WITHIN them, never widen to a co-tenant's.

type Sessions

type Sessions interface {
	Stop(ctx context.Context, org string, m SessionMatch) (int, error)
	CountActive(ctx context.Context, org string, m SessionMatch) (int, error)
}

Sessions is the seam to the agent-session control plane (clients/agents, in-process). Revoke stops the sessions that ran under a revoked account/device; the device view counts a machine's active sessions. A nil seam (unit test / no agents mounted) makes revoke skip the stop and the count report 0 — the registry truth (the revoked row) is unaffected.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the login-manager database. ONE SQLite file ({DataDir}/link.db) holds every org's Links; tenancy is the (org, subject) pair. It holds NO metering client — it is structurally incapable of charging commerce, and no usage series: account-usage lives in its own subsystem (clients/usage), so link owns links and nothing usage.

func (*Store) Close

func (s *Store) Close() error

func (*Store) Get

func (s *Store) Get(ctx context.Context, org, subject, id string) (Link, error)

Get returns one Link by id within the caller's (org, subject) scope, or errNotFound. The (org, subject, id) triple is the key, so another tenant's or another user's id resolves to errNotFound — never a cross-scope read.

func (*Store) List

func (s *Store) List(ctx context.Context, org, subject string) ([]Link, error)

List returns every Link the (org, subject) owns, most-recently-updated first (revoked rows included, so the dashboard can show recent log-outs).

func (*Store) ListDevice

func (s *Store) ListDevice(ctx context.Context, org, subject, machine string) ([]Link, error)

ListDevice returns the accounts on one machine within the caller's scope, most-recently-updated first.

func (*Store) ListLinked

func (s *Store) ListLinked(ctx context.Context, org, subject string) ([]Link, error)

ListLinked returns only the ACTIVE (status=linked) accounts the (org, subject) owns — the set the route policy considers. Revoked accounts are excluded so a logged-out account is never a routing candidate.

func (*Store) Revoke

func (s *Store) Revoke(ctx context.Context, org, subject, id string, now int64) (Link, bool, error)

Revoke marks one Link revoked within the caller's scope and returns it (so the handler can stop the sessions that ran under that account). ok=false when no such Link exists in this (org, subject) — a cross-scope id can neither revoke nor probe. An already-revoked Link is returned as-is (idempotent).

func (*Store) RevokeDevice

func (s *Store) RevokeDevice(ctx context.Context, org, subject, machine string, now int64) ([]Link, error)

RevokeDevice marks EVERY still-linked account on one machine revoked within the caller's scope and returns the rows it revoked (so their sessions can be stopped). A device with no linked accounts revokes nothing (empty slice).

func (*Store) Upsert

func (s *Store) Upsert(ctx context.Context, x Link) (Link, error)

Upsert registers a Link, keyed by its (org, subject, machine, provider, account) identity. A first report INSERTs (using x.ID / x.CreatedAt); a repeat UPDATEs in place — bumping last_seen, refreshing the device labels/plan/kind, re-activating a revoked account (status→linked), and replacing the usage snapshot ONLY when a fresh one is supplied (an empty usage on a heartbeat keeps the last good snapshot, mirroring @hanzo/usage's keep-stale-over-flapping rule). It returns the stored row (with its authoritative id, which on a repeat is the original, not x.ID). Org+subject are part of the identity, so a caller can only ever write within their OWN (org, subject) scope.

type Usage

type Usage struct {
	SessionPct   float64 `json:"sessionPct"`             // primary window used %, 0..100
	WeeklyPct    float64 `json:"weeklyPct"`              // secondary window used %, 0..100
	ResetsAt     string  `json:"resetsAt,omitempty"`     // primary window reset, RFC3339
	Tokens       int64   `json:"tokens"`                 // absolute token total when known
	InputTokens  int64   `json:"inputTokens,omitempty"`  //
	OutputTokens int64   `json:"outputTokens,omitempty"` //
	SpendCents   int64   `json:"spendCents"`             // provider spend (0 for a subscription)
	Currency     string  `json:"currency,omitempty"`     //
	Confidence   string  `json:"confidence,omitempty"`   // exact|estimated|percentOnly|unknown
	UpdatedAt    string  `json:"updatedAt,omitempty"`    // snapshot time, RFC3339
}

Usage is the projection of a provider's @hanzo/usage UsageSnapshot the collector pushes and the dashboard renders: the rate-limit windows, token totals, and spend. It is stored as JSON on the Link and parsed for the route policy's headroom. Money is USD cents end-to-end. Spend is always 0 for a pure subscription account (there is no per-call charge — the plan is flat).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL