publish

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

errors.go — sentinel errors for the P1.3 ZK opt-in flags.

Each Error() returns the upstream stable code from paygate-zk/docs/prds/konareef-integration-contract-v1.md § 6 (PRD 4 error taxonomy) so the CLI surface, structured logs, and downstream consumers all see the same identifier.

Package publish orchestrates the local half of `konareef pod publish` — turning a pod directory into a signed bundle ready to POST to reef-core's `/api/pods` endpoint.

Prepare composes the three existing primitives:

  • internal/pod — parse + schema validation
  • internal/canon — canonicalization + pod_hash (SHA-256)
  • internal/identity — secp256k1 ECDSA over the canonical bytes

It is deliberately offline-only: anything that needs the network (the actual POST, the install URL, the published-pod row) lives in the Submit function, so the local steps can be `--dry-run`-tested without a running reef-core.

tarball.go — the deterministic pod content tarball packed and uploaded alongside the signed manifest.

The signature only covers pod_hash, which is derived from canon.Canonicalize(pod.toml, podDir) — the canonical manifest plus a `[_files]` section binding every other pod file's SHA-256 into the hash. That's enough to *verify* a pod a publisher already has on disk, but reef-core's install path (Task B3) needs the actual bytes too. PackTarball ships them: a gzip tar of the pod directory that, once extracted, reproduces pod_hash bit-for-bit when re-run through canon.Canonicalize. That reproduction is the whole feature's integrity check — see the Prepare test in publish_test.go.

The walk mirrors internal/canon's buildFilesSection inclusion rules (read that file first) with one deliberate difference: canon excludes the root pod.toml and pod.lock from its `[_files]` listing because pod.toml is the document being hashed (listing it would be circular) and pod.lock is excluded from the hash for the same reason a lockfile normally is. Neither exclusion means "don't ship the file" — install needs pod.toml on disk to recompute the hash at all, and pod.lock (once dependency locking lands) needs to travel with the pod like any other content. So PackTarball includes both; everything else (hidden dotfiles/dirs skipped, symlinks and non-regular files hard-rejected) matches canon exactly.

Two guarantees hold across the pack/extract boundary, both enforced with the same predicate or constant on both sides so they can never drift apart:

  • Hidden exclusion is symmetric. isHiddenName is the one place that decides whether a path component is a hidden dotfile/dir; pack uses it to skip such entries during the walk, extract uses it to reject any tarball entry that carries one. A legitimate PackTarball output never contains a hidden entry, so one showing up on extraction means the tarball was hand-crafted or tampered with after packing — never a case for silently dropping the entry.
  • Publishable implies installable. maxDecompressedTarballSize caps both ExtractTarball's total decompressed write (the zip-bomb guard) and PackTarball's total uncompressed content (checked while packing, distinct from the 1 MiB *compressed* cap below). Without the pack-time check, a pod could squeeze under the compressed cap while decompressing past what an installer will accept — passing publish but failing install.

zk_session.go — ZK-session orchestration for `konareef pod publish` per PRD 4 § 4.3.

The four publish flags (--circuit-id, --pin-circuit-vkey, --disclosure-policy, --zk) carry distinct semantics with a fail-closed combination matrix. ValidateZKFlags is the single parse-time gate; downstream code can assume any ZKFlags value passing through it is structurally valid.

PreflightPinCheck is the pre-session vkey pin check per PRD 4 § 4.3.1. It is invoked AFTER ValidateZKFlags and BEFORE any network submission. A pin failure halts the session with vkeystore.ErrCircuitPinMismatch — no fold request reaches PayGate.

SealDisclosurePolicy is the witness-time enforcement point per PRD 4 § 4.3.3. Once called, the witness's disclosure policy is irrevocable; a second call returns ErrDisclosurePolicySealed.

EnsureSaltForTypeD provisions a 32-byte salt for the pod's lineage_id and persists it via the supplied saltstore backend for Type-D publishes (PRD 4 § B.1). No-op for Type-C and non-ZK.

Index

Constants

This section is empty.

Variables

View Source
var ErrAnchorBroadcastUnsupported = errors.New("ERR_ANCHOR_BROADCAST_UNSUPPORTED")

ErrAnchorBroadcastUnsupported is returned by EnsureVkeyAnchor when no on-chain anchor exists yet for (circuit_id, vkey_sha256) AND the BSV wallet integration has not landed. The publisher must wait for P1.8 (BSV wallet wiring) before --pin-circuit-vkey can initiate a fresh anchor transaction.

View Source
var ErrCircuitIDRequiredForPinCircuitVkey = errors.New("ERR_CIRCUIT_ID_REQUIRED_FOR_PIN_CIRCUIT_VKEY")

ErrCircuitIDRequiredForPinCircuitVkey is returned at flag-parse time when --pin-circuit-vkey is set without --circuit-id. The runtime path fetches the discovery manifest and looks up the vkey sha256 for flags.CircuitID before anchoring, so an empty CircuitID is internally inconsistent — there is no manifest entry to anchor against. Terminal; the publisher must supply --circuit-id before re-invoking. (Round-6 B1.)

View Source
var ErrDisclosurePolicySealed = errors.New("ERR_DISCLOSURE_POLICY_SEALED")

ErrDisclosurePolicySealed is returned when SealDisclosurePolicy is called twice on the same witness — once sealed, the witness's disclosure policy is irrevocable per PRD 4 § 4.3.3.

View Source
var ErrDisclosurePolicyViolation = errors.New("ERR_DISCLOSURE_POLICY_VIOLATION")

ErrDisclosurePolicyViolation is returned by konareef verify when the --disclosure-policy assertion does not match the bundle's embedded `disclosure` field. PRD 4 § 6 stable code.

View Source
var ErrInvalidDisclosurePolicy = errors.New("ERR_INVALID_DISCLOSURE_POLICY")

ErrInvalidDisclosurePolicy is returned when --disclosure-policy is supplied with a value other than the literal upper-case strings "C" or "D". Terminal.

View Source
var ErrPaygateZKDomainMissingPrefix = errors.New("ERR_PAYGATE_ZK_DOMAIN_MISSING_PREFIX")

ErrPaygateZKDomainMissingPrefix is returned by PublisherBaseDomain when the discovery manifest's `paygate_zk_domain` is not the expected service host (must start with `paygate-zk.`). The contract is documented at PRD 2 § 5.3 and internal/publish/discovery.go: the value MUST be the bare service host (e.g. `paygate-zk.example.com`), not a publisher base domain.

View Source
var ErrTarballContentTooLarge = errors.New("ERR_TARBALL_CONTENT_TOO_LARGE")

ErrTarballContentTooLarge is returned by PackTarball when the sum of uncompressed entry sizes exceeds maxDecompressedTarballSize — the same cap ExtractTarball enforces on decompression. Without this check a pod could squeeze under the compressed cap yet decompress to more than an installer will accept; catching it at pack time means nothing PackTarball emits can fail ExtractTarball's zip-bomb guard. Terminal — the publisher must trim the pod directory.

View Source
var ErrTarballHiddenEntry = errors.New("ERR_TARBALL_HIDDEN_ENTRY")

ErrTarballHiddenEntry is returned by ExtractTarball when an entry's path contains a hidden dotfile/dir component (see isHiddenName). PackTarball's walk excludes every such entry, so a legitimate tarball never carries one — seeing one on extraction means the tarball was hand-crafted or tampered with after packing, smuggling bytes pod_hash never covered. Terminal — the tarball is treated as hostile and nothing further is extracted from it.

View Source
var ErrTarballTooLarge = errors.New("ERR_TARBALL_TOO_LARGE")

ErrTarballTooLarge is returned by PackTarball when the compressed output exceeds maxCompressedTarballSize. Terminal — the publisher must trim the pod directory before republishing.

View Source
var ErrTarballUnsafeEntry = errors.New("ERR_TARBALL_UNSAFE_ENTRY")

ErrTarballUnsafeEntry is returned by ExtractTarball when an entry would escape destDir (absolute path or `..` traversal), is a symlink/hardlink/device rather than a plain file or directory, or would push total decompressed bytes past maxDecompressedTarballSize. Terminal — the tarball is treated as hostile and nothing further is extracted from it.

View Source
var ErrZkRequiresCircuitID = errors.New("ERR_ZK_REQUIRES_CIRCUIT_ID")

ErrZkRequiresCircuitID is returned at flag-parse time when --zk is set without --circuit-id. Terminal; the publisher must supply --circuit-id before re-invoking.

View Source
var ErrZkRequiresDisclosurePolicy = errors.New("ERR_ZK_REQUIRES_DISCLOSURE_POLICY")

ErrZkRequiresDisclosurePolicy is returned at flag-parse time when --zk is set without --disclosure-policy. Terminal; the publisher must supply --disclosure-policy {C|D}.

Functions

func EnsureSaltForTypeD

func EnsureSaltForTypeD(flags ZKFlags, spec *pod.Spec, backend saltstore.Backend) ([16]byte, [32]byte, error)

EnsureSaltForTypeD provisions a 32-byte salt for the pod's lineage_id and persists it via the supplied saltstore backend. Idempotent: if a salt is already stored for the lineage_id, the stored value is returned unchanged.

No-op for Type C (the salt rides in the public bundle and is not stored separately) and for non-ZK publishes.

When backend is nil (the publisher's environment has no usable salt-storage backend), returns saltstore.ErrSaltStorageUnavailable so the caller can halt the publish before any submission.

Spec: paygate-zk @ konareef-integration-contract-v1.md § 4.3.3 + P1.7.1 plan (saltstore.Resolve).

func ExtractTarball added in v0.1.2

func ExtractTarball(data []byte, destDir string) error

ExtractTarball decompresses and unpacks a PackTarball-produced gzip tar into destDir, which must already exist. Every entry is checked before anything is written:

  • absolute paths and any entry whose cleaned name escapes destDir via a `..` component are rejected (path traversal / zip-slip);
  • any entry with a hidden dotfile/dir path component (see isHiddenName) is rejected — PackTarball's walk excludes every such entry, so one appearing here means the tarball was hand-crafted or tampered with, carrying content pod_hash never covered;
  • only regular files and directories are extracted — symlinks, hardlinks, and device/FIFO entries are rejected outright, since a symlink extracted into the caller's workspace could point anywhere on the host;
  • total decompressed bytes are capped at maxDecompressedTarballSize (a zip-bomb guard: a small compressed input must not be able to inflate into an unbounded write).

Any rejection is ErrTarballUnsafeEntry or ErrTarballHiddenEntry (wrapped with detail) and leaves whatever was already written on disk — callers extract into a fresh scratch directory, never a location shared with anything else, so partial output is inert.

func PackTarball added in v0.1.2

func PackTarball(podDir string) ([]byte, error)

PackTarball walks podDir and produces a gzip-compressed tar of its contents, suitable for the `content_tarball` wire field. The output is fully deterministic for a given directory: entries are sorted by relative path, permissions are normalized to 0644 (files) / 0755 (dirs), and every mod time is zeroed — so re-packing an unchanged pod byte-for-byte reproduces the prior tarball, and two publishers building the same pod content independently get identical bytes.

The walk mirrors internal/canon's buildFilesSection rules: hidden dotfiles and dot-directories (and their subtrees) are skipped; symlinks and non-regular files (FIFOs, sockets, devices) are a hard error, never a silent skip — the same reasoning applies here as there, a symlink could smuggle arbitrary host content into what's supposed to be a faithful copy of the pod directory. Unlike canon's `[_files]` listing, the root pod.toml (and pod.lock, if present) ARE included — they're physical pod content, just excluded from canon's hash listing to avoid circularity.

Returns ErrTarballTooLarge if the compressed result exceeds the 1 MiB cap, or ErrTarballContentTooLarge if the sum of uncompressed entry sizes exceeds maxDecompressedTarballSize (never truncated — either way the publisher must trim the pod directory).

func PreflightPinCheck

func PreflightPinCheck(ctx context.Context, resolver Resolver, flags ZKFlags, manifest DiscoveryManifest) error

PreflightPinCheck is the pre-session vkey pin check per PRD 4 § 4.3.1. When flags.ZK is true:

  1. Look up the manifest's vkey_sha256 for flags.CircuitID; absent → return a wrapped error (the manifest does not advertise the requested circuit).
  2. Resolve the vkey through the resolver (cache → Tier 1 → Tier 2); the resolver itself enforces the SHA-256(vkey) == vkey_sha256 contract via vkeystore.VerifyPin.
  3. On vkeystore.ErrCircuitPinMismatch: propagate; the caller MUST halt with no submission.
  4. On vkeystore.ErrVkeyUnavailable: propagate; the caller MUST halt — verification cannot proceed without the vkey.

When flags.ZK is false this function is a no-op (legacy publish does not pin to a circuit).

func PublisherBaseDomain

func PublisherBaseDomain(zkServiceHost string) (string, error)

PublisherBaseDomain converts a discovery `paygate_zk_domain` service host into the publisher base domain expected by vkeystore.ResolveRequest.PublisherDomain. The conversion is a pure case-insensitive `paygate-zk.` prefix strip; the remainder MUST be a non-empty base domain (the caller passes it to vkeystore.ValidatePublisherDomain, which applies the strict DNS-hostname grammar).

Returns ErrPaygateZKDomainMissingPrefix when zkServiceHost is empty or does not start with `paygate-zk.` (case-insensitive). The prefix-stripped remainder is also checked for non-emptiness so a pathological input like `paygate-zk.` alone is rejected.

MR !21 round-2 B2 (note 680): fixes the live ZK publish + standalone pin flows when reef-core returns `paygate_zk_domain= "paygate-zk.example.com"`. Before this helper the value was passed raw to the resolver, which rejected it with ErrDomainAlreadyPrefixed.

func SealDisclosurePolicy

func SealDisclosurePolicy(flags ZKFlags, w *Witness) error

SealDisclosurePolicy is the witness-time enforcement point per PRD 4 § 4.3.3. It is the single permitted place to set witness.DisclosurePolicy. Once sealed, the witness's policy is irrevocable for the session — a downstream serialiser cannot subvert the publisher's choice.

When flags.ZK is false this function is a no-op (legacy publish has no witness).

func ValidateZKFlags

func ValidateZKFlags(flags ZKFlags) error

ValidateZKFlags enforces the parse-time fail-closed matrix per PRD 4 § 4.3.4. Returns one of:

  • nil — the combination is valid; the caller may proceed.
  • ErrZkRequiresCircuitID — --zk without --circuit-id.
  • ErrZkRequiresDisclosurePolicy — --zk without --disclosure-policy.
  • ErrInvalidDisclosurePolicy — --disclosure-policy with a value other than the literal upper-case strings "C" or "D".
  • ErrCircuitIDRequiredForPinCircuitVkey — --pin-circuit-vkey without --circuit-id (round-6 B1). The runtime path fetches the discovery manifest and looks up VkeySha256For(flags.CircuitID) before anchoring; an empty CircuitID is internally inconsistent.

Non-ZK invocations are accepted unchanged: --circuit-id is usable standalone. --pin-circuit-vkey is NOT usable standalone — it requires an explicit --circuit-id (round-6 B1).

Types

type AnchorState

type AnchorState int

AnchorState reports the result of EnsureVkeyAnchor.

const (
	// AnchorAlreadyExists means the on-chain anchor for
	// (circuit_id, SHA-256(vkey)) is already recorded; no new
	// broadcast is needed.
	AnchorAlreadyExists AnchorState = iota

	// AnchorBroadcasted means a new anchor transaction was
	// constructed, signed, and broadcast. Not reachable in v1; the
	// BSV wallet integration is owed by P1.8.
	AnchorBroadcasted
)

func EnsureVkeyAnchor

func EnsureVkeyAnchor(ctx context.Context, anchorBackend vkeystore.Tier2AnchorBackend, circuitID string, vkeyBytes []byte) (AnchorState, error)

EnsureVkeyAnchor implements the --pin-circuit-vkey behaviour:

  1. Look up the existing on-chain anchor hash for circuitID via the Tier-2 backend.
  2. If an anchor exists AND SHA-256(vkeyBytes) == anchor hash: return AnchorAlreadyExists (the flag is a no-op).
  3. If an anchor exists but the hash MISMATCHES: return vkeystore.ErrCircuitPinMismatch — the publisher must re-anchor or upgrade the circuit; do not proceed.
  4. If no anchor exists (ErrAnchorNotFound): return ErrAnchorBroadcastUnsupported — the broadcast path is owed by a future PRD.
  5. Any other Tier-2 error: propagate verbatim.

The function never has side effects on the chain in v1.

func (AnchorState) String

func (s AnchorState) String() string

String returns a stable lowercase identifier for logging.

type DiscoveryManifest

type DiscoveryManifest interface {
	// PaygateZKDomainHost returns the host portion (no scheme) of the
	// PayGate ZK service domain.
	PaygateZKDomainHost() string

	// VkeySha256For returns the vkey_sha256 hex string committed in
	// the manifest for circuitID, or ("", false) if the circuit is
	// absent.
	VkeySha256For(circuitID string) (string, bool)
}

DiscoveryManifest is the slice of the PRD 2 § 5.3 manifest this package consumes. The caller fetches and decodes the full manifest; this package only needs the paygate-zk domain + the circuits list.

func FetchDiscoveryManifest

func FetchDiscoveryManifest(ctx context.Context, serverURL string) (DiscoveryManifest, error)

FetchDiscoveryManifest GETs <serverURL>/api/discovery and decodes the slice consumed by PreflightPinCheck.

type PreparedPod

type PreparedPod struct {
	Handle         string
	PodName        string
	PodVersion     string
	PodHash        [32]byte
	CanonicalBytes []byte
	Signature      []byte
	PublicKeyHex   string

	// ContentTarball is the deterministic gzip tar of podDir produced
	// by PackTarball (Task B2). Extracting it and re-running
	// canon.Canonicalize MUST reproduce PodHash — that reproduction is
	// what lets `konareef install` (Task B3) trust the tarball bytes
	// instead of the signature alone. Set by Prepare; always non-empty
	// on success, since a demo-era pod must always ship its content.
	ContentTarball []byte

	// Spec is the parsed pod.toml. Carried through so the CLI can
	// derive the lineage_id for Type-D salt provisioning without
	// re-parsing the manifest. Set by Prepare.
	Spec *pod.Spec
}

PreparedPod is the local result of `Prepare`: the canonical manifest bytes, the SHA-256 pod_hash they produced, and the publisher's signature over those bytes — plus the metadata fields (`handle`, `pod_name`, `pod_version`, `publisher_pubkey`) that reef-core needs to register the pod.

Submit consumes one of these directly; the CLI's `--dry-run` mode just renders it.

func Prepare

func Prepare(podDir string, id *identity.Identity) (*PreparedPod, error)

Prepare runs the offline half of `konareef pod publish`:

  1. Read `<podDir>/pod.toml`.
  2. Parse + validate against the v0.1 schema. Any validation issue is fatal — `pod publish` is not the place to discover schema bugs.
  3. Canonicalize the manifest + the pod-directory contents (the synthetic `[_files]` section binds every other file in podDir into pod_hash, defeating post-sign file-swap attacks).
  4. `pod_hash = SHA-256(canonical_bytes)`.
  5. Sign the canonical bytes with `id`. The Sign primitive hashes internally, so the signature is over pod_hash (matches the publisher-signing-design v0 §"Stage 2" algorithm).
  6. Pack `podDir` into the deterministic content tarball (Task B2). A packing failure is fatal here rather than a degrade-to-no- tarball fallback: a demo-era pod must always ship its content, so the publisher sees a pathological (e.g. oversized) pod at publish time, not as a mystery later at install time.

The returned PreparedPod is ready for Submit or `--dry-run` rendering; no network call is performed here.

type Resolver

type Resolver interface {
	Resolve(ctx context.Context, req vkeystore.ResolveRequest) (*vkeystore.Vkey, error)
}

Resolver is the narrow interface PreflightPinCheck consumes from internal/vkeystore. Declaring it inline here lets the publish package depend on the abstraction (and the test wire a fake without a full vkeystore.Resolver).

type SubmitOpts

type SubmitOpts struct {
	PublicBundle     bool     // WI-P0-012
	CircuitID        string   // PRD 4 § 4.3.1
	ZkEnabled        bool     // PRD 4 § 4.3.4
	DisclosurePolicy string   // PRD 4 § 4.3.3 — "C" or "D"
	Witness          *Witness // PRD 4 § B.1 — sealed witness for v2 / Type-D path; nil for v1 / non-ZK
}

SubmitOpts groups the post-P1.0 publish-time flags. The zero value represents a legacy publish (no opt-ins). Each non-zero field maps 1-1 to a `submitRequest` field of the same name.

Round-4 B1: SubmitOpts gains a Witness *Witness field. For ZK publishes the caller sets Witness to the sealed witness after SealDisclosurePolicy has succeeded, and Submit threads the witness's lineage commitment into the wire body (Type-D) and into the v2 packer (PRD 4 § B.1). Witness is nil for v1 / non-ZK publishes; in that case Submit emits the byte-identical pre-P1.3 wire body.

Data-path discipline (PRD 4 § B.2): Witness.LineageID flows into the publish wire body; Witness.Salt stays LOCAL on the publisher — consumed only by the v2 bundle packer for index / value_hash derivation, then discarded; NEVER serialised into the public bundle output or the submit wire body.

type SubmitResponse

type SubmitResponse struct {
	InstallURL   string    `json:"install_url"`
	RegisteredAt time.Time `json:"registered_at"`
}

SubmitResponse is the 201 body from `POST /api/pods`.

func Submit

func Submit(serverURL string, p *PreparedPod, opts SubmitOpts) (*SubmitResponse, error)

Submit POSTs a PreparedPod to `<serverURL>/api/pods`. On 2xx it returns the parsed install URL + registration timestamp. Any non-2xx status, network failure, or unparseable response is a fatal error — there is no retry policy here; the CLI is the human-facing wrapper that interprets failures.

SubmitOpts carries the opt-in flags; the zero value preserves pre-P1.3 wire shape exactly.

type Witness

type Witness struct {
	// DisclosurePolicy is the per-session policy sealed by
	// SealDisclosurePolicy. After Sealed is true, it MUST NOT change.
	DisclosurePolicy string

	// Sealed is true once SealDisclosurePolicy has been called. A
	// second seal call returns ErrDisclosurePolicySealed.
	Sealed bool

	// LineageID is the 16-byte pod lineage identifier provisioned by
	// EnsureSaltForTypeD on Type-D publishes. Zero for Type-C and for
	// non-ZK publishes.
	LineageID [16]byte

	// Salt is the 32-byte CSPRNG-derived salt persisted by the
	// saltstore backend for Type-D publishes. Zero for Type-C and
	// non-ZK. Consumed LOCALLY by the v2 packer per PRD 4 § B.1 then
	// discarded; NEVER serialised over the wire (§ B.2).
	Salt [32]byte
}

Witness is the publisher's intent envelope passed into PayGate ZK proof construction. Reduced to the fields this PRD touches; the full payload (P, R, M_in, M_out, T_log_records) is the V2 packer's concern (P1.2).

type ZKFlags

type ZKFlags struct {
	// CircuitID is the value of --circuit-id. Empty when absent.
	// Mandatory when ZK is true (PRD 4 § 4.3.4 + § 4.3.1).
	CircuitID string

	// PinCircuitVkey is the value of --pin-circuit-vkey. True
	// triggers the on-chain anchor flow per PRD 4 § 4.3.2.
	PinCircuitVkey bool

	// DisclosurePolicy is the value of --disclosure-policy,
	// normalised to upper-case at parse time by the caller. Valid
	// values: "C", "D". Empty when absent. Mandatory when ZK is true
	// (PRD 4 § 4.3.4 + § 4.3.3).
	DisclosurePolicy string

	// ZK is the value of --zk. True activates the full ZK path:
	// witness construction, Spartan proof generation, v2 bundle
	// emission. Sets published_pods.zk_enabled = true on reef-core.
	ZK bool
}

ZKFlags captures the four ZK-related publish flags. The zero value represents a legacy publish (no ZK opt-in).

Jump to

Keyboard shortcuts

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