hub v0.14.0: passphrase-authed host enrollment (Day-0 option C)
New POST /api/v1/host-enroll (handleHostEnroll): X-Retrieval-Password authed,
body {customer_id} -> {host_id, api_key}. Mint-once-reuse (201 first, 200
reuse) so re-running the host-bootstrap never orphans a running agent's key;
auth checked before any mint. Backed by new Store.GetHostByCustomer
(ORDER BY updated_at DESC LIMIT 1, idx_hosts_customer).
GET /config/{id} and global-key POST /admin/hosts left untouched. Exact-match
route (path == "/host-enroll") to avoid the /hosts/ prefix collision.
Tests: host_enroll_test.go (mint/reuse/401-no-mint/404/400) + GetHostByCustomer
store test; companion red-proof verified always-mint fails the reuse assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
This commit is contained in:
@@ -45,6 +45,46 @@ func TestUpsertHost_AndLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHostByCustomer(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
// none → nil, nil
|
||||
got, err := s.GetHostByCustomer("c1")
|
||||
if err != nil || got != nil {
|
||||
t.Fatalf("no host: got %+v / %v (want nil,nil)", got, err)
|
||||
}
|
||||
|
||||
// one → that host
|
||||
if err := s.UpsertHost(&Host{HostID: "c1-aaa111", CustomerID: "c1", APIKey: "k1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err = s.GetHostByCustomer("c1")
|
||||
if err != nil || got == nil || got.HostID != "c1-aaa111" || got.APIKey != "k1" {
|
||||
t.Fatalf("one host: got %+v / %v", got, err)
|
||||
}
|
||||
|
||||
// two for the same customer → most-recently-updated wins (never a duplicate on reuse).
|
||||
// updated_at is second-resolution, so set it explicitly to make the ordering deterministic.
|
||||
if err := s.UpsertHost(&Host{HostID: "c1-bbb222", CustomerID: "c1", APIKey: "k2"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.db.Exec(`UPDATE hosts SET updated_at='2026-01-01 00:00:00' WHERE host_id='c1-aaa111'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.db.Exec(`UPDATE hosts SET updated_at='2026-06-26 00:00:00' WHERE host_id='c1-bbb222'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err = s.GetHostByCustomer("c1")
|
||||
if err != nil || got == nil || got.HostID != "c1-bbb222" {
|
||||
t.Fatalf("two hosts: want most-recent c1-bbb222, got %+v / %v", got, err)
|
||||
}
|
||||
|
||||
// a different customer is unaffected
|
||||
if other, err := s.GetHostByCustomer("c2"); err != nil || other != nil {
|
||||
t.Fatalf("other customer: got %+v / %v (want nil,nil)", other, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveHostReport_BumpsRealityPreservesIntent(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {
|
||||
|
||||
@@ -1147,6 +1147,21 @@ func (s *Store) GetHost(hostID string) (*Host, error) {
|
||||
return h, err
|
||||
}
|
||||
|
||||
// GetHostByCustomer returns the customer's host, or nil (no error) if none exists.
|
||||
// Backs the passphrase-authed host-enroll mint-once-reuse path (Day-0 option C): on
|
||||
// the second enroll the existing credential is reused, not re-minted. A customer is
|
||||
// expected to have at most one host in the Day-0 model; if more than one ever exists,
|
||||
// the most-recently-updated wins (we never mint a duplicate on a reuse). Uses the
|
||||
// idx_hosts_customer index. Mirrors GetHostByAPIKey's nil-on-not-found contract.
|
||||
func (s *Store) GetHostByCustomer(customerID string) (*Host, error) {
|
||||
h, err := scanHost(s.db.QueryRow(`SELECT `+hostSelectCols+
|
||||
` FROM hosts WHERE customer_id = ? ORDER BY updated_at DESC LIMIT 1`, customerID).Scan)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return h, err
|
||||
}
|
||||
|
||||
// ListHosts returns all hosts (debug / host-domain views).
|
||||
func (s *Store) ListHosts() ([]Host, error) {
|
||||
rows, err := s.db.Query(`SELECT ` + hostSelectCols + ` FROM hosts ORDER BY host_id`)
|
||||
|
||||
Reference in New Issue
Block a user