review

package
v1.29.1 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

View Source
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.

func Manifest

func Manifest(changes checker.Changes) []Change

Manifest builds the {fingerprint, level} manifest. Fingerprints use checker.Fingerprint, so they match the ones in the encrypted changelog the bundle carries and the fingerprints on each Block.

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

func (p Payload) Encrypt() (blob, key []byte, err error)

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.

Jump to

Keyboard shortcuts

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