package store import ( "database/sql" "fmt" "time" ) // Appliance registration (v0.62.0, R-21 slice C). A box booted from the GENERIC secret-free ISO // registers itself here as an unclaimed appliance and polls for its credentials; the operator binds // it to a customer; ONE poll then delivers (customer-id + retrieval passphrase) and the record is // consumed. Keyed by (uuid, mac_set) — serials are unusable (N100 DMI "Default string") and cheap // boards duplicate SMBIOS UUIDs, so the MAC set is the tiebreaker. The appliance token is the box's // only pre-day-0 credential; only its sha256 is stored here, never the token itself. // Appliance statuses. const ( ApplianceRegistered = "registered" // seen, awaiting an operator bind ApplianceBound = "bound" // bound to a customer; delivery staged, not yet consumed ApplianceDelivered = "delivered" // credentials delivered once; every later poll → 410 ApplianceDiscarded = "discarded" // operator ignored it; token invalidated, polls → 404 ) // ApplianceRegistration is one unclaimed/bound appliance record. type ApplianceRegistration struct { ID int64 UUID string MACSet string // sorted, comma-joined physical MACs SSHHostPubkeys string // newline-joined authorized_keys-format lines HWSummary string // JSON blob (product, cpu, mem, mode hint) Status string CustomerID string // set at bind InstallMode string // staged at bind (appliance|byo) ExtraArgs string // staged at bind PairingCode string // v0.66.0 (R-27): stable 6-char code shown on the box console + the self-bind page FirstSeen time.Time LastSeen time.Time BoundAt *time.Time DeliveredAt *time.Time DiscardedAt *time.Time } // RegisterAppliance upserts by (uuid, mac_set) and stores the fresh token's hash. Re-registration // updates last_seen and never duplicates. A DISCARDED record stays discarded (sticky — the operator // said ignore; its poll keeps returning 404, no oracle). Any other existing record is RESET to // `registered` with the fresh token and its staged bind cleared — a box that is re-registering has no // token yet, so it is genuinely starting over; the operator re-binds. isNew is true only on first // insert (so the caller can log the first sighting). The token itself is never passed in — only its // hash. // v0.66.0 (R-27 slice 1): the caller passes a candidateCode (minted via configgen — store must not // import configgen, an import cycle); it is used ONLY on the first insert and stays STABLE across the // idempotent re-register. The effective stored code is returned so the register response can carry it. func (s *Store) RegisterAppliance(uuid, macSet, sshKeys, hwSummary, tokenHash, candidateCode string) (isNew bool, pairingCode string, err error) { tx, err := s.db.Begin() if err != nil { return false, "", err } defer tx.Rollback() var id int64 var status, existingCode string row := tx.QueryRow(`SELECT id, status, COALESCE(pairing_code,'') FROM appliance_registrations WHERE uuid = ? AND mac_set = ?`, uuid, macSet) switch err := row.Scan(&id, &status, &existingCode); err { case sql.ErrNoRows: if _, err := tx.Exec(` INSERT INTO appliance_registrations (uuid, mac_set, ssh_host_pubkeys, hw_summary, token_hash, status, pairing_code) VALUES (?, ?, ?, ?, ?, 'registered', ?)`, uuid, macSet, sshKeys, hwSummary, tokenHash, candidateCode); err != nil { return false, "", fmt.Errorf("register appliance insert: %w", err) } if err := tx.Commit(); err != nil { return false, "", err } return true, candidateCode, nil case nil: // The code stays STABLE across re-register; a pre-v0.66.0 row (empty code) is backfilled once. effectiveCode := existingCode if effectiveCode == "" { effectiveCode = candidateCode } // Existing record: refresh last_seen + identity + the token. Sticky-discard keeps its status; // everything else resets to registered (a re-registering box is starting over). if status == ApplianceDiscarded { if _, err := tx.Exec(`UPDATE appliance_registrations SET token_hash = ?, ssh_host_pubkeys = ?, hw_summary = ?, pairing_code = ?, last_seen = datetime('now') WHERE id = ?`, tokenHash, sshKeys, hwSummary, effectiveCode, id); err != nil { return false, "", fmt.Errorf("register appliance (discarded) update: %w", err) } } else { if _, err := tx.Exec(`UPDATE appliance_registrations SET token_hash = ?, ssh_host_pubkeys = ?, hw_summary = ?, pairing_code = ?, status = 'registered', customer_id = NULL, install_mode = NULL, extra_args = NULL, bound_at = NULL, delivered_at = NULL, last_seen = datetime('now') WHERE id = ?`, tokenHash, sshKeys, hwSummary, effectiveCode, id); err != nil { return false, "", fmt.Errorf("register appliance update: %w", err) } } if err := tx.Commit(); err != nil { return false, "", err } return false, effectiveCode, nil default: return false, "", fmt.Errorf("register appliance lookup: %w", err) } } // scanAppliance scans a full appliance row (column order fixed by applianceCols). const applianceCols = `id, uuid, mac_set, ssh_host_pubkeys, hw_summary, status, COALESCE(customer_id,''), COALESCE(install_mode,''), COALESCE(extra_args,''), COALESCE(pairing_code,''), first_seen, last_seen, bound_at, delivered_at, discarded_at` func scanAppliance(sc interface{ Scan(...any) error }) (*ApplianceRegistration, error) { var a ApplianceRegistration var firstSeen, lastSeen string var boundAt, deliveredAt, discardedAt sql.NullString if err := sc.Scan(&a.ID, &a.UUID, &a.MACSet, &a.SSHHostPubkeys, &a.HWSummary, &a.Status, &a.CustomerID, &a.InstallMode, &a.ExtraArgs, &a.PairingCode, &firstSeen, &lastSeen, &boundAt, &deliveredAt, &discardedAt); err != nil { return nil, err } a.FirstSeen = parseSQLiteTime(firstSeen) a.LastSeen = parseSQLiteTime(lastSeen) if boundAt.Valid && boundAt.String != "" { t := parseSQLiteTime(boundAt.String) a.BoundAt = &t } if deliveredAt.Valid && deliveredAt.String != "" { t := parseSQLiteTime(deliveredAt.String) a.DeliveredAt = &t } if discardedAt.Valid && discardedAt.String != "" { t := parseSQLiteTime(discardedAt.String) a.DiscardedAt = &t } return &a, nil } // ApplianceByToken resolves a token hash to its record (nil, nil when unknown — the poll maps that to // 404, indistinguishable from a discarded/never-registered token: no enumeration oracle). func (s *Store) ApplianceByToken(tokenHash string) (*ApplianceRegistration, error) { if tokenHash == "" { return nil, nil } a, err := scanAppliance(s.db.QueryRow(`SELECT `+applianceCols+` FROM appliance_registrations WHERE token_hash = ?`, tokenHash)) if err == sql.ErrNoRows { return nil, nil } return a, err } // ApplianceByPairingCode resolves a console pairing code to the ONE bindable (registered, not yet // bound/delivered/discarded) appliance carrying it — the customer-self-bind factor-1 lookup (v0.66.0, // R-27). Returns (nil, nil) when zero OR more than one registered appliance matches (an ambiguous // code binds nothing — the public handler maps that to the same generic failure as a wrong code, no // oracle). code is the RAW stored form (uppercased, no separator); the caller normalizes first. func (s *Store) ApplianceByPairingCode(code string) (*ApplianceRegistration, error) { if code == "" { return nil, nil } rows, err := s.db.Query(`SELECT `+applianceCols+` FROM appliance_registrations WHERE pairing_code = ? AND status = 'registered'`, code) if err != nil { return nil, err } defer rows.Close() var matches []*ApplianceRegistration for rows.Next() { a, err := scanAppliance(rows) if err != nil { return nil, err } matches = append(matches, a) if len(matches) > 1 { return nil, nil // ambiguous — bind nothing } } if err := rows.Err(); err != nil { return nil, err } if len(matches) != 1 { return nil, nil } return matches[0], nil } // GetAppliance fetches by row id (operator UI actions). func (s *Store) GetAppliance(id int64) (*ApplianceRegistration, error) { a, err := scanAppliance(s.db.QueryRow(`SELECT `+applianceCols+` FROM appliance_registrations WHERE id = ?`, id)) if err == sql.ErrNoRows { return nil, nil } return a, err } // ListUnclaimedAppliances returns the registered + bound (not-yet-delivered/discarded) records for the // operator's "Unclaimed appliances" section, newest activity first. func (s *Store) ListUnclaimedAppliances() ([]ApplianceRegistration, error) { rows, err := s.db.Query(`SELECT ` + applianceCols + ` FROM appliance_registrations WHERE status IN ('registered','bound') ORDER BY last_seen DESC`) if err != nil { return nil, err } defer rows.Close() var out []ApplianceRegistration for rows.Next() { a, err := scanAppliance(rows) if err != nil { return nil, err } out = append(out, *a) } return out, rows.Err() } // BindAppliance stages the delivery: bind an appliance to a customer + record what the box consumes // on its next poll (customer-id comes from the record; mode/extra ride here). Allowed from registered // or an already-bound-not-delivered state (operator re-bind / changed mind). Refuses once delivered or // discarded. func (s *Store) BindAppliance(id int64, customerID, mode, extraArgs string) error { res, err := s.db.Exec(`UPDATE appliance_registrations SET status = 'bound', customer_id = ?, install_mode = ?, extra_args = ?, bound_at = datetime('now') WHERE id = ? AND status IN ('registered','bound')`, customerID, mode, extraArgs, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("appliance %d not bindable (missing, delivered, or discarded)", id) } return nil } // MarkApplianceDelivered atomically flips bound→delivered EXACTLY ONCE. ok=true for the single winning // poll; ok=false for every later poll (already delivered) or a lost race — the handler maps ok=false // on a bound-looking record to 410. This is the one-shot delivery gate. func (s *Store) MarkApplianceDelivered(tokenHash string) (ok bool, err error) { res, err := s.db.Exec(`UPDATE appliance_registrations SET status = 'delivered', delivered_at = datetime('now') WHERE token_hash = ? AND status = 'bound'`, tokenHash) if err != nil { return false, err } n, _ := res.RowsAffected() return n == 1, nil } // DiscardAppliance marks a registration ignored and INVALIDATES its token (blanks the hash so no poll // can ever match it, belt-and-suspenders atop the status check). Sticky: a later re-registration of // the same (uuid, mac_set) keeps it discarded. func (s *Store) DiscardAppliance(id int64) error { res, err := s.db.Exec(`UPDATE appliance_registrations SET status = 'discarded', discarded_at = datetime('now'), token_hash = '' WHERE id = ?`, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("appliance %d not found", id) } return nil }