Documentation
¶
Overview ¶
Package review builds the encrypted review bundle: the two specs, the computed changelog, and the per-change structural blocks of the specs.
It is the single source of truth for the bundle's on-the-wire shape (Payload), its encryption (Encrypt), and the per-change fingerprint manifest (Manifest); a decryptor on the receiving side mirrors the same layout.
The bundle is zero-knowledge by construction: Encrypt seals the Payload with a fresh AES-256-GCM key and returns the ciphertext and the key separately. The caller uploads only the ciphertext and keeps the key out of band, so the server receives a blob it cannot read. This package makes no assumption about where the bundle is uploaded or consumed; that is the caller's concern.
Blocks: each change with its source context ¶
Extract slices the specs into just the structural blocks that contain changes, so each change carries its enclosing source text without the full specs:
blocks := review.Extract(changes, baseDocs, revDocs, baseTexts, revTexts)
Each Block carries its key/title (e.g. "POST /users" or "components/schemas/User"), the ids and fingerprints of the changes inside it, and the block's source-text slice on each side with its starting line.
Block selection ¶
A change is keyed to the smallest indexed block whose origin span contains its source line, not by its (operation, path): line keying follows $refs, so a change inside a $ref'd component keys to the component rather than to each endpoint that references it, and the per-endpoint changes all group onto that one block. When no source line resolves (e.g. a change detected after --flatten-allof, whose merged schema has no single location), it falls back to the operation it names, then, for a top-level change with no operation, the rule's Area (the OpenAPI object the rule concerns, e.g. "security" or "tags"; see checker.Area), then an "other changes" bucket.
Slicing ¶
Slicing relies on kin-openapi origin end positions (openapi3.Origin.Key.EndLine/EndColumn) so a block's full extent is known, not just its start. The specs must be loaded with IncludeOrigin = true.
Status and limitations ¶
Indexed block types are operations, path items, named components (schemas, security schemes, responses, parameters, request bodies, headers), the top-level sections (info/servers/tags/security), and schemas $ref'd from another file (sliced from that file via the per-file texts Extract takes). Composed mode indexes every spec in the set, disambiguating same-named blocks by file. All external-$ref shapes slice from the file the schema lives in: whole-file refs (./User.yaml), refs into a components-structured file (./defs.yaml#/components/schemas/User, even without openapi:/info:), and refs to a schema under an arbitrary top-level key (./schemas.yaml#/User, the Swagger-2-era "definitions bag" shape; see TestExtract_ArbitraryTopLevelKeyRefSlicedFromExternalFile). Known gaps:
- Because blocks are keyed off the changelog, a block whose only change has no changelog entry (e.g. a description-only edit) is not extracted; that schema-shape completeness is a later phase.
A block's slice is raw source text, so changed lines within it that have no changelog entry are still present.
Index ¶
Constants ¶
const BlobVersion = 1
BlobVersion is the first byte of the uploaded blob. It lets the decryptor reject a format it doesn't understand instead of trying to decrypt garbage. Bump it only on an incompatible layout change.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Block ¶
type Block struct {
Key string `json:"key" yaml:"key"` // stable identity, e.g. "POST /users" or "components/schemas/User"
Title string `json:"title" yaml:"title"` // human header
ChangeIDs []string `json:"change_ids" yaml:"change_ids"` // rule ids of the changes in this block (for display/debug)
Fingerprints []string `json:"fingerprints" yaml:"fingerprints"` // per-change fingerprints, aligned with ChangeIDs; the stable key a consumer joins each change to its block on
BaseFile string `json:"base_file,omitempty" yaml:"base_file,omitempty"` // basename of the base slice's source file (a $ref'd file differs from the root)
BaseText string `json:"base_text" yaml:"base_text"` // source slice on the base side ("" if absent)
BaseLineStart int `json:"base_line_start" yaml:"base_line_start"` // 1-based first line of BaseText in the base spec
RevFile string `json:"rev_file,omitempty" yaml:"rev_file,omitempty"` // basename of the revision slice's source file
RevText string `json:"rev_text" yaml:"rev_text"` // source slice on the revision side ("" if absent)
RevLineStart int `json:"rev_line_start" yaml:"rev_line_start"` // 1-based first line of RevText in the revision spec
}
Block is one extracted structural block: its source-text slice on each side, plus the ids of the changes that fall inside it. Empty BaseText/RevText means that side has no sliceable source (e.g. an added or removed block, or a location that did not resolve to a block).
func Extract ¶
func Extract(changes checker.Changes, baseDocs, revDocs []*openapi3.T, baseTexts, revTexts map[string]string) []Block
Extract groups changes by their enclosing structural block and slices each block's text, ordered by first appearance. The docs must be loaded with IncludeOrigin (end positions). baseTexts/revTexts map each contributing file's path, as reported on element origins, to its raw source (see load.NewSpecInfoWithCapture), so a block slices from the file it lives in.
type Change ¶
type Change struct {
Fingerprint string `json:"fingerprint" yaml:"fingerprint"`
Level int `json:"level" yaml:"level"`
}
Change is one manifest entry sent alongside the encrypted bundle in cleartext: a change's fingerprint (see checker.Fingerprint) and its level, so a server can track per-change state without reading the bundle.
type Payload ¶
type Payload struct {
BaseSpec string `json:"base_spec" yaml:"base_spec"`
RevisionSpec string `json:"revision_spec" yaml:"revision_spec"`
BaseFilename string `json:"base_filename" yaml:"base_filename"`
RevisionFilename string `json:"revision_filename" yaml:"revision_filename"`
Changes json.RawMessage `json:"changes" yaml:"changes"`
Mode string `json:"mode" yaml:"mode"`
// Composed marks a bundle built from a set of specs per side (composed
// mode): there is no single spec or filename, so BaseSpec/RevisionSpec and
// the filenames are empty and the blocks carry the comparison.
Composed bool `json:"composed,omitempty" yaml:"composed,omitempty"`
Blocks []Block `json:"blocks,omitempty" yaml:"blocks,omitempty"`
// ToolVersion is the oasdiff version that produced the bundle (build.Version,
// e.g. "v1.25.1"; "main" for a dev build). It travels inside the encrypted
// bundle, so the receiving review page can tell when a review came from an
// outdated oasdiff and nudge an upgrade. Empty for bundles from older
// clients that predate this field. The GitHub Action inherits it, it runs
// this same CLI with --open.
ToolVersion string `json:"tool_version,omitempty" yaml:"tool_version,omitempty"`
// Platform is where the bundle was produced, from the PLATFORM environment
// variable: "github-action" when the CLI runs inside the oasdiff GitHub
// Action (which sets it in its image), empty for a plain CLI invocation. It
// lets the review page tailor an upgrade nudge (bump the action vs set up
// the action).
Platform string `json:"platform,omitempty" yaml:"platform,omitempty"`
}
Payload is the cleartext review bundle and the single source of truth for its wire shape: a decryptor on the receiving side mirrors these json tags. Encrypt seals it with a fresh key and only the ciphertext is uploaded, so the server receives a blob it cannot read.
BaseSpec/RevisionSpec hold each spec's bytes verbatim (YAML stays YAML text); this JSON object is only the envelope. Changes is the changelog the caller already computed, embedded raw: the server can't recompute what it can't read. Blocks is the per-change structural slices (see Extract); empty when the changelog is empty, since every change resolves to some block.
func (Payload) Encrypt ¶
Encrypt marshals the payload to JSON and encrypts it with a freshly generated 256-bit key using AES-256-GCM. It returns the upload blob and the key. The blob layout is: version(1) || nonce(12) || ciphertext+tag. The key is returned to the caller, which keeps it out of band (never uploaded), so the server receives ciphertext it cannot read. A decryptor on the receiving side reverses this exact layout.