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
- func CheckLimitFlag(command string, selector Selector, limitSet bool) error
- func Chunk(ids []any, size int) [][]any
- func IsBulkWriteVerb(command string) bool
- func ResolveMaxDocs(flag int, flagSet bool, configMaxBulk int, all bool) int
- func ValidateMaxDocs(flag int, flagSet bool) error
- func WhereIDsIn(ids []any) map[string]any
- type Confirmer
- type DryRunRequest
- type DryRunResult
- type Level
- type Op
- type Plan
- type Request
- type Selector
Constants ¶
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 )
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" // The `pay blocks` family edits a document that arrives on stdin and puts // nothing on the wire, so every one of them is L0 — the same level as a // read, and for the same reason: nothing it does can be wrong on the // server. The write happens later, once, in `pay apply`. CmdBlocksList = "blocks ls" CmdBlocksMove = "blocks mv" CmdBlocksRemove = "blocks rm" CmdBlocksAdd = "blocks add" CmdBlocksCopy = "blocks cp" CmdBlocksSet = "blocks set" // CmdApply is the pipeline's write half: one PATCH, one document, // addressed by the id the upstream envelope carries. It is exactly as // risky as `pay update <id>` and is levelled with it. CmdApply = "apply" )
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.
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).
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 ¶
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 ¶
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 ¶
IsBulkWriteVerb reports whether --where on this command is a bulk write.
func ResolveMaxDocs ¶
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 ¶
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 ¶
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.
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.
type Level ¶
type Level int
Level is a §12.1 risk level.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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.