// Package supervise orchestriert den End-to-End Supervise-Flow. // // Auto-routet je nach Device-State: // - unsupervised → superviseFresh (MCInstall.SetCloudConfiguration) // - already supervised → SuperviseViaBackup (MobileBackup2.Restore-Trick) // // Beide Pfade enden mit Reboot + Verify via MCInstall.GetCloudConfiguration. package supervise import ( "encoding/json" "errors" "fmt" "log" "os" "time" "github.com/raynis/rebreak-supervise-magic/internal/cert" "github.com/raynis/rebreak-supervise-magic/internal/device" "github.com/raynis/rebreak-supervise-magic/internal/mcinstall" ) // Options steuert die Variation. type Options struct { OrgName string DryRun bool Verbose bool BackupPath string // wenn != "", schreibt current cloud-config als JSON dorthin VOR Write } // Supervise — Haupt-Entry. Apple's MCInstall.SetCloudConfiguration firet 14002 // auf ALLEN post-activated devices (auch unsupervised — Apple's empty-cloud-config- // shell zählt schon als "cloud config present"). Daher: immer MobileBackup2-Path. // // Auf already-supervised devices ist MobileBackup2 sowieso required (re-supervise). // Auf unsupervised devices (post factory-reset) ist MobileBackup2 cleaner als // MCInstall weil clean-state weniger conflicts hat. func Supervise(udid string, opts Options) error { // 2026-05-28 ENV-OVERRIDE: REBREAK_FORCE_MCINSTALL=1 zwingt den MCInstall- // Pfad (statt MobileBackup2). Für Tests auf activated+unsupervised devices // wo Memory's "14002-Hypothese" empirisch zu validieren ist. Ändert den // Default-Air-Re-Supervise-Pfad NICHT. if os.Getenv("REBREAK_FORCE_MCINSTALL") == "1" { fmt.Printf("[router] FORCE_MCINSTALL=1 → superviseFresh path (MCInstall.SetCloudConfiguration)\n") return superviseFresh(udid, opts) } conn, err := device.Connect(udid) if err == nil { isSupervised, _ := conn.IsSupervised() conn.Close() if isSupervised { fmt.Printf("[router] device supervised → MobileBackup2-Restore (re-supervise)\n") } else { fmt.Printf("[router] device unsupervised → MobileBackup2-Restore (fresh-supervise)\n") } } return SuperviseViaBackup(udid, opts) } // superviseFresh nutzt MCInstall.SetCloudConfiguration für unsupervised Devices. // Apple's 14002-Check ist hier nicht aktiv (kein existing cloud-config). func superviseFresh(udid string, opts Options) error { logf := makeLogger(opts.Verbose) logf("[fresh-flow] step 1/6: connecting to %s ...", udid) conn, err := device.Connect(udid) if err != nil { return fmt.Errorf("step 1: %w", err) } defer conn.Close() logf("[fresh-flow] step 2/6: loading supervision identity ...") id, err := cert.LoadOrCreate() if err != nil { return fmt.Errorf("step 2: %w", err) } logf(" ✓ cert %d bytes", len(id.CertDER)) if opts.DryRun { logf("[fresh-flow] step 3-6: DRY-RUN — skipping MCInstall + reboot") return nil } logf("[fresh-flow] step 3/6: opening MCInstall ...") mc, err := mcinstall.Open(conn.Device()) if err != nil { return fmt.Errorf("step 3: %w", err) } logf("[fresh-flow] step 4/6: SetCloudConfiguration ...") if _, err := mc.Supervise(mcinstall.SuperviseConfig{ OrganizationName: opts.OrgName, CertDER: id.CertDER, AllowPairing: true, }); err != nil { mc.Close() return fmt.Errorf("step 4: %w", err) } mc.Close() logf(" ✓ cloud-config set") logf("[fresh-flow] step 5/6: rebooting ...") if err := conn.Reboot(); err != nil { return fmt.Errorf("step 5: %w", err) } logf("[fresh-flow] step 6/6: waiting + verifying ...") conn2, err := device.WaitForReconnect(udid, 90*time.Second) if err != nil { return fmt.Errorf("step 6: reconnect: %w", err) } defer conn2.Close() logf(" ✓ device back online — DONE") return nil } // Unsupervise — reverse-Flow. Aktuell only via MCInstall path implemented // für unsupervised→supervised reverse. Für TL-supervised → unsupervised // müssten wir auch den Backup-Pfad nutzen. func Unsupervise(udid string, opts Options) error { logf := makeLogger(opts.Verbose) logf("[unsupervise] device-unsupervise nicht implementiert in dieser Phase.") logf("[unsupervise] Manuell: Apple Configurator 2 → iPhone → Profile → Remove.") return errors.New("unsupervise: not implemented (Phase 2 — falls relevant)") } func backupCurrentConfig(conn *device.Conn, path string, logf func(string, ...any)) error { mc, err := mcinstall.Open(conn.Device()) if err != nil { return err } defer mc.Close() if err := mc.HelloHostIdentifier(); err != nil { return err } curr, err := mc.GetCloudConfiguration() if err != nil || curr == nil { return fmt.Errorf("no existing config") } backupBytes, err := json.MarshalIndent(curr, "", " ") if err != nil { return err } if err := os.WriteFile(path, backupBytes, 0o600); err != nil { return err } logf(" ✓ backed up current cloud-config → %s (%d bytes)", path, len(backupBytes)) return nil } func makeLogger(verbose bool) func(format string, args ...any) { if !verbose { return func(format string, args ...any) { fmt.Printf(format+"\n", args...) } } return func(format string, args ...any) { log.Printf(format, args...) } }