package fleet import ( "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "fmt" "time" ) // PolicySnapshot is a shareable frozen policy bundle. type PolicySnapshot struct { Token string `json:"token"` PolicyJSON json.RawMessage `json:"policy_json"` ExpiresAt string `json:"expires_at,omitempty"` CreatedAt string `json:"created_at"` } // CreatePolicySnapshot stores a policy JSON blob and returns an opaque token. func (s *Store) CreatePolicySnapshot(policy map[string]any, ttl time.Duration) (*PolicySnapshot, error) { raw, err := json.Marshal(policy) if err != nil { return nil, fmt.Errorf("marshal policy: %w", err) } token, err := randomToken(16) if err != nil { return nil, err } now := time.Now().UTC() expires := "" if ttl > 0 { expires = now.Add(ttl).Format(time.RFC3339) } _, err = s.db.Exec(` INSERT INTO policy_snapshots (token, policy_json, expires_at, created_at) VALUES (?, ?, ?, ?) `, token, string(raw), nullIfEmpty(expires), now.Format(time.RFC3339)) if err != nil { return nil, fmt.Errorf("insert policy snapshot: %w", err) } return &PolicySnapshot{ Token: token, PolicyJSON: raw, ExpiresAt: expires, CreatedAt: now.Format(time.RFC3339), }, nil } // GetPolicySnapshot loads a snapshot by token (public summon link). func (s *Store) GetPolicySnapshot(token string) (*PolicySnapshot, error) { row := s.db.QueryRow(` SELECT token, policy_json, expires_at, created_at FROM policy_snapshots WHERE token = ? `, token) var snap PolicySnapshot var expires sql.NullString var created string if err := row.Scan(&snap.Token, &snap.PolicyJSON, &expires, &created); err != nil { return nil, err } if expires.Valid { snap.ExpiresAt = expires.String t, err := time.Parse(time.RFC3339, expires.String) if err == nil && time.Now().After(t) { return nil, sql.ErrNoRows } } snap.CreatedAt = created return &snap, nil } func randomToken(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil } func nullIfEmpty(s string) sql.NullString { if s == "" { return sql.NullString{} } return sql.NullString{String: s, Valid: true} }