safety

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package safety implements §12 "Write safety": the four risk levels, the confirmation policy, --dry-run and the single blast-radius cap (--max-docs).

Nothing in this package performs I/O against Payload and nothing here reads the process environment or the clock — callers inject what they need, so every rule below is a table test.

Index

Constants

View Source
const (
	// DefaultMaxDocs is defaults.max_bulk (§4.2): the default blast-radius cap
	// on a bulk write. It is NOT a page size — see CheckLimitFlag.
	DefaultMaxDocs = 100
	// ChunkSize is how many ids go into one `?where={"id":{"in":[…]}}` request
	// during §12.3 phase 3.
	ChunkSize = 100
)
View Source
const (
	CmdFind            = "find"
	CmdGet             = "get"
	CmdCount           = "count"
	CmdDescribe        = "describe"
	CmdCollections     = "collections"
	CmdExplain         = "explain"
	CmdAccess          = "access"
	CmdCan             = "can"
	CmdWhoami          = "whoami"
	CmdDownload        = "download"
	CmdDiscover        = "discover"
	CmdDoctor          = "doctor"
	CmdCache           = "cache"
	CmdRaw             = "raw"
	CmdVersionsList    = "versions list"
	CmdVersionsGet     = "versions get"
	CmdVersionsDiff    = "versions diff"
	CmdGlobalsList     = "globals list"
	CmdGlobalsGet      = "globals get"
	CmdCreate          = "create"
	CmdUpdate          = "update"
	CmdUpload          = "upload"
	CmdDuplicate       = "duplicate"
	CmdPublish         = "publish"
	CmdUnpublish       = "unpublish"
	CmdDelete          = "delete"
	CmdRestore         = "restore"
	CmdGlobalsUpdate   = "globals update"
	CmdVersionsRestore = "versions restore"
)

Canonical command names. These are the strings PayCLI uses in the envelope's "command" field and in an audit Event, so the same constant identifies an operation everywhere.

View Source
const (
	ActionCreate         = "create"
	ActionUpdate         = "update"
	ActionDelete         = "delete"
	ActionTrash          = "trash"
	ActionRestore        = "restore"
	ActionPublish        = "publish"
	ActionUnpublish      = "unpublish"
	ActionUpload         = "upload"
	ActionRestoreVersion = "restore_version"
	ActionRaw            = "raw"
)

Audit Event.Action values (§12.7).

View Source
const SampleLimit = 5

SampleLimit is how many ids --dry-run shows in data.sample_ids (§12.2).

Variables

This section is empty.

Functions

func CheckLimitFlag

func CheckLimitFlag(command string, selector Selector, limitSet bool) error

CheckLimitFlag implements §12.3's flag split: --limit is page size and is meaningless on a bulk write, so it is rejected rather than silently reinterpreted as a blast-radius cap.

limitSet must be the flag's Changed bit, not "limit != 0" — the default page size of 20 is not a user request.

func Chunk

func Chunk(ids []any, size int) [][]any

Chunk splits resolved ids into ChunkSize-sized batches for §12.3 phase 3. The final batch may be short; an empty input yields no batches.

func IsBulkWriteVerb

func IsBulkWriteVerb(command string) bool

IsBulkWriteVerb reports whether --where on this command is a bulk write.

func ResolveMaxDocs

func ResolveMaxDocs(flag int, flagSet bool, configMaxBulk int, all bool) int

ResolveMaxDocs applies the §4.5 precedence for the blast-radius cap: --max-docs, else defaults.max_bulk from config, else DefaultMaxDocs. --all lifts the cap entirely and is reported as 0 ("no cap").

A non-positive flag value is NOT a cap and is not silently reinterpreted here: callers must have rejected it with ValidateMaxDocs first.

func ValidateMaxDocs

func ValidateMaxDocs(flag int, flagSet bool) error

ValidateMaxDocs rejects an explicit non-positive --max-docs.

It must be called before anything is sent, on the scoped form as well as the bulk one. 0 and negative values cannot be honoured: Plan.Check reads MaxDocs <= 0 as "no cap", so passing them through would turn `--max-docs 0` into an UNBOUNDED write, while ResolveMaxDocs' fallback silently substitutes defaults.max_bulk and then reports a cap the user never typed. An explicit blast-radius instruction is never discarded without a word.

func WhereIDsIn

func WhereIDsIn(ids []any) map[string]any

WhereIDsIn builds the §12.3 phase-3 filter. PayCLI always resolves ids client-side and then addresses them explicitly, on PATCH exactly as on DELETE, so the blast radius equals what --dry-run printed on both verbs.

Types

type Confirmer

type Confirmer struct {
	// In is the confirmation input stream (stdin).
	In io.Reader
	// Out receives prompts, the bulk match count and the irreversibility
	// warning. This is stderr — never stdout.
	Out io.Writer
	// TTY is true when both In and Out are a terminal. It is computed by the
	// caller (only app.go may look at the real file descriptors).
	TTY bool
	// AssumeYes is --yes or PAY_YES=1.
	AssumeYes bool
	// ConfirmWrites is defaults.confirm_writes: opt in to prompting on L1.
	ConfirmWrites bool
	// Quiet suppresses the informational lines. It never suppresses a prompt,
	// because a prompt with no question is unanswerable.
	Quiet bool
	// DryRun short-circuits every prompt: --dry-run performs the read half
	// only, so there is nothing to confirm (§12.2).
	DryRun bool
	// contains filtered or unexported fields
}

Confirmer applies the §12.1 prompting policy. It writes only to Out (stderr in the real CLI) so the stdout envelope stays parseable, and it reads only from In.

func (*Confirmer) Confirm

func (c *Confirmer) Confirm(req Request) error

Confirm returns nil when the operation may proceed, and a confirmation_required error (exit 11) when it may not.

type DryRunRequest

type DryRunRequest struct {
	Method string          `json:"method"`
	URL    string          `json:"url"`
	Body   json.RawMessage `json:"body"`
}

DryRunRequest is the request --dry-run would have sent. It is printed verbatim (after redaction) so an agent can hand it to `pay raw` or curl.

type DryRunResult

type DryRunResult struct {
	WouldAffect int           `json:"would_affect"`
	SampleIDs   []any         `json:"sample_ids"`
	Truncated   bool          `json:"truncated"`
	Request     DryRunRequest `json:"request"`
}

DryRunResult is the §12.2 payload: data_kind "op_result", exit 0.

func NewDryRun

func NewDryRun(req DryRunRequest, total int, ids []any, noRedact bool) DryRunResult

NewDryRun builds the result. The URL always passes through redact.URL, and the body through redact.JSON unless --no-redact was given: a dry-run body can contain a password or an apiKey when the target is the auth collection, and §5.3 admits no exception for "it was only a preview".

total is the resolved match count (N from the count call); ids are the resolved ids, of which at most SampleLimit are shown.

func (DryRunResult) Envelope

func (r DryRunResult) Envelope(command string, meta output.Meta) *output.Envelope

Envelope wraps the result in the §12.2 envelope. meta.dry_run is forced true so it cannot disagree with the data, and the exit code is 0: a dry run that resolved its target successfully is a success.

type Level

type Level int

Level is a §12.1 risk level.

const (
	// L0 read — never prompts.
	L0 Level = iota
	// L1 scoped write — one document, recoverable where versions/trash exist.
	L1
	// L2 scoped destructive — one document, not recoverable.
	L2
	// L3 bulk — an unbounded number of documents.
	L3
)

func (Level) Audited

func (l Level) Audited() bool

Audited reports whether §12.7 requires an audit record. Reads are never audited; every write is.

func (Level) Name

func (l Level) Name() string

Name is the human label from §12.1's table.

func (Level) String

func (l Level) String() string

String renders the level as it appears in help text and errors.

type Op

type Op struct {
	// Command is one of the Cmd* constants.
	Command string
	// Selector says how many documents are addressed.
	Selector Selector
	// Permanent is --permanent on delete: the real DELETE rather than the
	// soft-delete PATCH.
	Permanent bool
	// TrashEnabled is the manifest's answer for the target collection. When it
	// is false a delete cannot be undone and is therefore one level riskier
	// (§12.4).
	TrashEnabled bool
	// HardDelete is set when the command puts a raw DELETE on the wire
	// regardless of what trash says — today only `delete --where
	// --unsafe-passthrough-where`, which hands the filter to the server and
	// cannot express a soft delete. Every safety signal has to describe the
	// request that is actually sent, so this makes the operation read as a
	// hard, irreversible delete even on a trash-enabled collection where
	// --permanent was never passed.
	HardDelete bool
	// Method is the HTTP method for `pay raw`, which has no fixed risk level.
	Method string
	// Global is true when the target is a global rather than a collection.
	Global bool
}

Op is one operation about to be performed, described in the terms §12 cares about. It is deliberately data-only: Level, Action and the confirmation decision are pure functions of it.

func (Op) Action

func (o Op) Action() string

Action is the audit Event.Action for this operation.

§12.7's comment lists create|update|delete|trash|restore|publish|upload| restore_version. "unpublish" is added because folding it into "publish" would make the audit log unable to answer "who took this page offline", which is the question the log exists for.

func (Op) Irreversible

func (o Op) Irreversible() bool

Irreversible reports whether the operation destroys data with no PayCLI-side recovery path. §12.4 requires a stderr warning in exactly this case.

func (Op) IrreversibleReason

func (o Op) IrreversibleReason() string

IrreversibleReason explains WHY Irreversible() is true. The two causes need different words: "--permanent bypassed a trash that exists" is a statement about this command, while "the collection has no trash" is a claim about the project's schema — and printing the second one on a trash-enabled collection is simply false, which an agent may act on (concluding `pay restore` does not exist there). It returns "" when the operation is reversible.

func (Op) Level

func (o Op) Level() Level

Level classifies the operation per §12.1.

Two rows are not literally in the table and are derived from §12.4 instead: `delete <id>` without --permanent on a trash-enabled collection is a recoverable PATCH, so it is L1; on a collection without trash the same command is irreversible, so it stays L2. `restore` is the inverse of a soft delete and is therefore L1.

func (Op) SoftDelete

func (o Op) SoftDelete() bool

SoftDelete reports whether `pay delete` will issue the §12.4 soft-delete PATCH rather than a real DELETE.

type Plan

type Plan struct {
	// Command is the bulk verb, used in messages.
	Command string
	// Collection is the target slug.
	Collection string
	// Matched is N from `GET /{coll}/count?where=…`.
	Matched int
	// MaxDocs is the effective cap; 0 means --all was passed.
	MaxDocs int
	// All is true when --all lifted the cap.
	All bool
}

Plan is the outcome of §12.3 phases 1 and 2: how many documents match, what the cap was, and whether the operation may proceed.

func (Plan) Check

func (p Plan) Check() error

Check implements §12.3 step 2. It returns bulk_limit_exceeded (exit 5) when the match count is over the cap and --all was not passed.

type Request

type Request struct {
	// Op is the operation being confirmed.
	Op Op
	// Target is the collection or global slug, used in the summary line.
	Target string
	// Affected is the resolved match count for a bulk verb, or 1 for a scoped
	// one. A negative value means "not resolved".
	Affected int
	// Summary overrides the generated one-line description. It must never
	// contain a credential; callers pass slugs and counts, not request bodies.
	Summary string
}

Request is one confirmation question.

func (Request) Line

func (r Request) Line() string

Line is the one-line description printed before prompting and, for L3, printed even when --yes was passed (§12.1: bulk "always prints the resolved match count before acting").

type Selector

type Selector int

Selector says how many documents the operation addresses.

const (
	// SelectorNone — the operation creates a document or targets a global.
	SelectorNone Selector = iota
	// SelectorID — exactly one document, addressed by id.
	SelectorID
	// SelectorBulk — --where / --ids: an unbounded set.
	SelectorBulk
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL