package historyreadback import ( "context" "os" "slices" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/release" ) // ReadReconstruction verifies the exact accepted authorization from fresh, // enrolled-root readback. It does not certify fresh installation or retire // independent recovery obligations carried in the returned status. func ReadReconstruction(ctx context.Context, d hostactors.Directory, q Request, g release.HistoryReconstruction, now time.Time) (Receipt, error) { r, err := ReadStatus(ctx, d, q, now) if err != nil { return Receipt{}, err } s := r.Status accepted := digest(g) if !validHex(accepted, 32) || g.Schema != 1 || g.Kind != "carlos-release-history" || g.Operation == "" || g.IssuedAt.IsZero() || g.Account != q.Scope.Binding.Account || g.App != q.Scope.Binding.App || g.Identity != s.Identity || g.Revision == 0 || g.Revision == ^uint64(0) || s.Revision != g.Revision+1 || s.Pending || s.LastReconstructionDigest != accepted || !s.AppComplete || s.AppRevision <= g.AppRevision || s.BakeComplete == 0 || s.BakeRevision <= g.BakeRevision || len(g.Hosts) == 0 || len(g.Hosts) > 256 { return Receipt{}, os.ErrPermission } seen := map[string]bool{} for _, expected := range g.Hosts { if seen[expected.Host] { return Receipt{}, os.ErrPermission } seen[expected.Host] = true i := slices.IndexFunc(s.Hosts, func(h release.HistoryHostStatus) bool { return h.Host == expected.Host }) if i < 0 || s.Hosts[i].Source != expected.Source || s.Hosts[i].Revision <= expected.Revision || !s.Hosts[i].RefusalStatus.Complete { return Receipt{}, os.ErrPermission } } if !seen[q.Scope.Binding.Host] { return Receipt{}, os.ErrPermission } return r, nil } // ReadRetirement verifies one effective retirement. Unrelated restrictions // remain in the receipt and must not be treated as cleared by this result. func ReadRetirement(ctx context.Context, d hostactors.Directory, q Request, g release.HistoryRecoveryRetirement, now time.Time) (Receipt, error) { r, err := ReadStatus(ctx, d, q, now) if err != nil { return Receipt{}, err } s := r.Status accepted := digest(g) if !validHex(accepted, 32) || g.Schema != 1 || g.Kind != "carlos-release-recovery-retirement" || g.Operation == "" || g.IssuedAt.IsZero() || g.Binding != q.Scope.Binding || g.Identity != s.Identity || s.Pending || s.RecoverySources == nil || !s.RecoverySources.Known || !validHex(g.OutcomeDigest, 32) || !validHex(g.VerifiedDigest, 32) || g.AttemptID == "" || g.VerifiedID == "" || g.AttemptID == g.VerifiedID { return Receipt{}, os.ErrPermission } for _, pending := range s.RecoverySources.Unresolved { if pending.Binding == g.Binding && (pending.Attempt.ID == g.AttemptID || pending.Attempt.ID == g.VerifiedID) { return Receipt{}, os.ErrPermission } } for _, h := range s.Hosts { if h.Host == g.Binding.Host && h.Source == g.Source && slices.Contains(h.RetiredRecoveries, g.AttemptID) && h.RetirementDigests[g.AttemptID] == accepted && h.RecoveryDigests[g.AttemptID] == g.OutcomeDigest && h.RecoveryDigests[g.VerifiedID] == g.VerifiedDigest { return r, nil } } return Receipt{}, os.ErrPermission }