hub v0.66.0 + ISO v1.20.0: customer self-bind (R-27 slice 1)
Let a customer bind their own freshly-installed appliance without the operator: operator "Send self-bind link" mints a 7-day tokenized capability link, emailed (Hungarian, sibling sender) to the customer, who opens a public /bind/<token> page and proves two factors — the console pairing code shown on the box screen + their retrieval passphrase — and the hub stages the bind via the same BindAppliance (provenance customer_selfbind). The box's ~30s appliance poll delivers. Viktor's three rulings verbatim: console pairing code (no appliance list ever rendered), operator-sent tokenized link, 5-attempt lockout -> "call support". Wrong code == wrong passphrase (one generic failure, no oracle, both factors compared unconditionally); expiry falls back to operator-bind unchanged. THE TRAP: one public prefix /bind/, exempt from auth+CSRF at both /login gate sites via a single isPublicBindPath predicate (tight trailing-slash match; ServeMux ..-cleans; handler rejects '/' in token). 9 tests (Scenarios A-F + F1/F2); 4 red-proofs verified red-then-green (lockout, oracle, widened-prefix, single-active). GC verdict: no appliance GC -> the 7-day TTL stands alone. Controller/agent untouched; R-27b deferred. Green: full hub build/vet/test (17 ok) + bash -n + hub confirm gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
This commit is contained in:
@@ -637,15 +637,37 @@ func (s *Store) migrate() error {
|
||||
bound_at DATETIME,
|
||||
delivered_at DATETIME,
|
||||
discarded_at DATETIME,
|
||||
pairing_code TEXT NOT NULL DEFAULT '',
|
||||
UNIQUE(uuid, mac_set)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_appliance_status ON appliance_registrations(status, last_seen DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_appliance_token ON appliance_registrations(token_hash);
|
||||
|
||||
-- selfbind_tokens (v0.66.0, R-27 slice 1 — customer self-bind): the 7-day tokenized capability
|
||||
-- link the operator emails. token_hash is sha256 at rest (never the token). Single-active per
|
||||
-- customer (delete-then-insert on re-mint). attempts lock at 5 (Viktor's ruling); consumed_at is
|
||||
-- the one-shot flip. NO appliance data here — the code+passphrase check happens at bind time.
|
||||
CREATE TABLE IF NOT EXISTS selfbind_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_id TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
locked INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at DATETIME NOT NULL,
|
||||
emailed_at DATETIME,
|
||||
consumed_at DATETIME
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_selfbind_token ON selfbind_tokens(token_hash);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// v0.66.0 (R-27 slice 1): pairing_code on pre-existing appliance rows (idempotent — errors if the
|
||||
// column already exists, which is fine on a fresh DB where the CREATE above already added it).
|
||||
s.db.Exec("ALTER TABLE appliance_registrations ADD COLUMN pairing_code TEXT NOT NULL DEFAULT ''")
|
||||
|
||||
// v0.51.0 dr_tier one-time legacy backfill — see the ALTER above; runs last so every table
|
||||
// it touches (hosts, customer_configs) exists on a fresh DB too (where it finds nothing).
|
||||
if drTierAlterErr == nil {
|
||||
|
||||
Reference in New Issue
Block a user