Documentation
¶
Overview ¶
Package approval implements the ApprovalRequest FSM service.
States: PENDING -> APPROVED | DENIED | EXPIRED
Transitions are single-direction and fail-closed: any attempt to decide an already-decided approval returns ErrAlreadyDecided. Every state change emits an audit event with actor_type=human (for decisions) or actor_type=system (for expirations).
Pure business logic: storage is injected via the Store interface so this package can be tested with an in-memory fake.
Index ¶
- Variables
- func Decide(ctx context.Context, st Store, id uuid.UUID, decidedByType types.ActorType, ...) (types.ApprovalRequest, error)
- func ExpireStale(ctx context.Context, st Store, olderThan time.Duration) (int, error)
- func RequestApproval(ctx context.Context, st Store, req types.ApprovalRequest) (types.ApprovalRequest, error)
- type Store
Constants ¶
This section is empty.
Variables ¶
var ErrAlreadyDecided = types.ErrApprovalAlreadyDecided
ErrAlreadyDecided is re-exported here for callers that import only this package. It IS the store's sentinel — one value, defined in internal/types — so errors.Is matches whichever layer raised it.
Functions ¶
func Decide ¶
func Decide(ctx context.Context, st Store, id uuid.UUID, decidedByType types.ActorType, decision types.ApprovalDecision) (types.ApprovalRequest, error)
Decide transitions an existing approval request to decision.State (which the caller sets — APPROVED or DENIED; ExpireStale below bypasses Decide entirely for EXPIRED, since a sweep is not a decision). decision.DecidedBy is the principal that decided and decidedByType is its actor type (human for an OIDC session or a LocalMode operator; system for a bare admin-token caller). The audit event records that exact type so an admin-token decision is not mislabelled as a human approval (invariant 4/6 attribution honesty).
func ExpireStale ¶
ExpireStale transitions all PENDING approvals that were requested before the cutoff (time.Now().UTC().Add(-olderThan)) to EXPIRED and emits one audit event per expiration. Returns the number of approvals expired.
func RequestApproval ¶
func RequestApproval(ctx context.Context, st Store, req types.ApprovalRequest) (types.ApprovalRequest, error)
RequestApproval creates a new PENDING approval, or returns the existing PENDING approval when one already exists for the same run+kind+scope hash (deduplication guard).
Types ¶
type Store ¶
type Store interface {
// CreateApproval persists a new PENDING approval and returns it.
CreateApproval(ctx context.Context, a types.ApprovalRequest) (types.ApprovalRequest, error)
// GetApproval fetches an approval by id.
GetApproval(ctx context.Context, id uuid.UUID) (types.ApprovalRequest, error)
// ListApprovals returns approvals filtered by state (empty = all).
ListApprovals(ctx context.Context, stateFilter types.ApprovalState) ([]types.ApprovalRequest, error)
// DecideApproval transitions state from PENDING to decision.State; returns
// ErrAlreadyDecided if the approval is not PENDING.
DecideApproval(ctx context.Context, id uuid.UUID, decision types.ApprovalDecision) (types.ApprovalRequest, error)
// Record appends an audit event (approval.decide, approval.expire).
Record(ctx context.Context, ev types.AuditEvent) error
}
Store is the narrow persistence interface the approval FSM needs. The real implementation is internal/store; tests supply a fake.