package tenantstore import ( "fmt" "strings" "time" "github.com/klarkxy/nekonest/relaycore" corestore "github.com/klarkxy/nekonest/relaycore/store" ) var ( _ corestore.Store = (*DB)(nil) _ relaycore.PrincipalSynchronizer = (*DB)(nil) ) func credentialDigest(credential relaycore.Credential) (string, error) { value := strings.TrimSpace(credential.Value) switch credential.Kind { case relaycore.CredentialRaw: if value == "" { return "", fmt.Errorf("credential is required") } return hashToken(value), nil case relaycore.CredentialSHA256: value = strings.ToLower(value) if len(value) != 64 { return "", fmt.Errorf("invalid sha256 credential digest") } for _, char := range value { if !strings.ContainsRune("0123456789abcdef", char) { return "", fmt.Errorf("invalid sha256 credential digest") } } return value, nil default: return "", fmt.Errorf("unsupported credential kind %q", credential.Kind) } } // SyncDevice installs a control-plane-approved identity without ever requiring // the plaintext bearer to leave the client/Relay admission boundary. func (db *DB) SyncDevice(id, name, osName string, credential relaycore.Credential) error { id = strings.TrimSpace(id) if id == "" { return fmt.Errorf("device id is required") } digest, err := credentialDigest(credential) if err != nil { return err } name = strings.TrimSpace(name) if name == "" { name = "Cloud host" } osName = strings.TrimSpace(osName) if osName == "" { osName = "unknown" } now := time.Now().Unix() _, err = db.conn.Exec( `INSERT INTO devices (id, name, os, token_hash, created_at, last_seen, active_agents, revoked_at) VALUES (?, ?, ?, ?, ?, ?, 0, 0) ON CONFLICT(id) DO UPDATE SET name = excluded.name, os = excluded.os, token_hash = excluded.token_hash, revoked_at = 0`, id, name, osName, digest, now, now, ) return err } // RevokeDevice invalidates the bearer while retaining ciphertext history and // audit-relevant local state until the tenant retention policy purges it. func (db *DB) RevokeDevice(id string) error { result, err := db.conn.Exec( `UPDATE devices SET token_hash = ?, revoked_at = ?, active_agents = 0 WHERE id = ? AND revoked_at = 0`, hashToken("revoked:"+generateToken()), time.Now().Unix(), strings.TrimSpace(id), ) if err != nil { return err } changed, err := result.RowsAffected() if err != nil { return err } if changed == 0 { return fmt.Errorf("device not found or already revoked") } return nil } // SyncPhone mirrors an approved phone credential digest into this tenant's // isolated store. Device grants remain separate and are never created here. func (db *DB) SyncPhone(id, name string, credential relaycore.Credential) error { id = strings.TrimSpace(id) if id == "" { return fmt.Errorf("phone id is required") } digest, err := credentialDigest(credential) if err != nil { return err } name = strings.TrimSpace(name) if name == "" { name = "Cloud phone" } now := time.Now().Unix() _, err = db.conn.Exec( `INSERT INTO phone_identities (id, name, token_hash, ed25519_public, x25519_public, created_at, last_seen, revoked_at) VALUES (?, ?, ?, '', '', ?, ?, 0) ON CONFLICT(id) DO UPDATE SET name = excluded.name, token_hash = excluded.token_hash, revoked_at = 0`, id, name, digest, now, now, ) return err }