#!/bin/sh
# satsignal-anchor — Docker BuildKit provenance adapter for satsignal.provenance.v1
# ---------------------------------------------------------------------------
# A sibling of the GitLab CI (gitlab-ci.satsignal.yml) and Bitbucket
# (bitbucket-pipelines.satsignal.yml) adapters for the
# satsignal.provenance.v1 ingest schema (spec:
# https://proof.satsignal.cloud/spec-provenance). It is a thin
# translator: it fills the canonical provenance manifest from a
# `docker buildx build` and POSTs it to Satsignal, which commits its
# SHA-256 on-chain and returns a portable .mbnt bundle. POSIX-sh only
# (sha256sum + curl + jq, plus the `docker` you already have) — same
# manifest shape as the GitLab/Bitbucket adapters and the GitHub
# Action, no SDK, no Satsignal account at verify time.
#
# Docker BuildKit is a build *tool*, not a CI platform — it has no
# `include: remote:` and runs anywhere buildx runs (any CI, or your
# laptop). So this is a standalone script, not a pipeline file. The
# subject is the image you ship (`containerimage.digest`); the SLSA
# provenance attestation BuildKit pushes alongside it (the OCI
# attestation-manifest, content-addressed by the registry) is anchored
# as a reference — Satsignal timestamps the attestation, it does not
# re-issue it (the "plug into SLSA/Sigstore, don't compete" posture).
#
# USAGE — build with provenance + a metadata file, then run this:
#
#     docker buildx build --provenance=true \
#       --metadata-file=buildx-metadata.json \
#       -t registry.example.com/acme/app:1.4.2 --push .
#
#     curl -fsSL https://satsignal.cloud/docker-buildx.satsignal.sh \
#       | sh -s -- buildx-metadata.json
#     # or: save the file and run  ./docker-buildx.satsignal.sh buildx-metadata.json
#
# The metadata file (buildx `--metadata-file`) is the only positional
# arg; it defaults to ./buildx-metadata.json. From it this reads
# `containerimage.digest` (the subject), `image.name` (the pushed
# reference) and `buildx.build.ref` (the BuildKit build record).
#
# Required environment:
#     SATSIGNAL_API_KEY    sk_... key with the anchors:create scope
#     SATSIGNAL_FOLDER     folder slug to file the proof under (e.g. "releases")
#     SATSIGNAL_MATTER     legacy alias of SATSIGNAL_FOLDER (still accepted)
#
# Optional environment:
#     SATSIGNAL_METADATA_FILE  override the positional metadata-file arg
#     SATSIGNAL_SUBJECT_TYPE   subject.type override (default "image";
#                              use "container" if you anchor a runtime)
#     SATSIGNAL_API_BASE       default https://app.satsignal.cloud
#     SATSIGNAL_NO_REGISTRY    set non-empty to skip the registry probe
#                              for the SLSA attestation (anchor the
#                              image digest only — still chain-bound,
#                              just without the attestation reference)
#
# Attestation reference (best-effort, never fatal): this runs
# `docker buildx imagetools inspect <ref> --raw` on the pushed tag and
# anchors the OCI digest of each `attestation-manifest` referrer (the
# value the registry already content-addresses — stable, no JSON
# re-serialization). If the build was not `--push`ed, the registry is
# unreachable, or no attestation is attached, the script warns and
# anchors the image digest alone. It is honest about what it anchored.
#
# Outputs (written to the working directory):
#     satsignal-proof.json     the API response (txid, manifest_hash,
#                              also written as satsignal-receipt.json (compat copy)
#                              embeddable chain-anchor-v1 envelope)
#     satsignal-manifest.json  the canonical manifest that was anchored
#     satsignal-<digest>.mbnt  the proof bundle — verify offline per
#                              /spec-provenance §5, no Satsignal call
# ---------------------------------------------------------------------------
set -eu

for bin in jq curl sha256sum; do
  command -v "$bin" >/dev/null 2>&1 || {
    echo "satsignal: required command not found: $bin" >&2; exit 1; }
done

META="${SATSIGNAL_METADATA_FILE:-${1:-buildx-metadata.json}}"
: "${SATSIGNAL_API_KEY:?set SATSIGNAL_API_KEY (key with anchors:create)}"
: "${SATSIGNAL_FOLDER:=${SATSIGNAL_MATTER:?set SATSIGNAL_FOLDER (folder slug; SATSIGNAL_MATTER also accepted)}}"
SATSIGNAL_API_BASE="${SATSIGNAL_API_BASE:-https://app.satsignal.cloud}"
SATSIGNAL_SUBJECT_TYPE="${SATSIGNAL_SUBJECT_TYPE:-image}"

[ -f "$META" ] || {
  echo "satsignal: buildx metadata file not found: $META" >&2
  echo "  build with: docker buildx build --provenance=true --metadata-file=$META ..." >&2
  exit 1; }

# `image.name` can be a comma-separated list of pushed refs — take the
# first as the canonical reference. `containerimage.digest` is the
# platform image manifest digest (already sha256:<64hex>) = the subject
# you `docker pull`. `buildx.build.ref` is the local build record.
IMAGE_NAME=$(jq -r '.["image.name"] // "" | split(",")[0]' "$META")
IMAGE_DIGEST=$(jq -r '.["containerimage.digest"] // ""' "$META")
BUILDKIT_REF=$(jq -r '.["buildx.build.ref"] // ""' "$META")
[ -n "$IMAGE_DIGEST" ] || {
  echo "satsignal: no containerimage.digest in $META — was this built" >&2
  echo "  with BuildKit and a --metadata-file? (multi-arg builds need --push)" >&2
  exit 1; }

# Best-effort: ask the registry for the SLSA provenance attestation
# BuildKit pushed next to the image. We anchor the OCI digest of the
# attestation-manifest referrer (content-addressed by the registry —
# stable across pulls, no predicate re-serialization). Never fatal.
ATTS_JSON='[]'
if [ -z "${SATSIGNAL_NO_REGISTRY:-}" ] && [ -n "$IMAGE_NAME" ] \
   && command -v docker >/dev/null 2>&1; then
  if RAW=$(docker buildx imagetools inspect "$IMAGE_NAME" --raw 2>/dev/null) \
     && [ -n "$RAW" ]; then
    ATTS_JSON=$(printf '%s' "$RAW" | jq -c '
      [ (.manifests // [])[]
        | select(.annotations["vnd.docker.reference.type"]
                 == "attestation-manifest")
        | { type: "slsa", digest: .digest } ]' 2>/dev/null || echo '[]')
  fi
fi
NATT=$(printf '%s' "$ATTS_JSON" | jq 'length')
if [ "$NATT" = "0" ]; then
  echo "satsignal: note — no SLSA attestation found in the registry;" >&2
  echo "  anchoring the image digest alone (still chain-bound). Build" >&2
  echo "  with --provenance=true --push to attach one." >&2
fi

# Best-effort git context (this often runs from the build's source
# tree). Empty values are dropped by the jq builder below, exactly
# like the GitLab/Bitbucket adapters' `with_entries(select(...))`.
GIT_COMMIT=""
GIT_REF=""
if command -v git >/dev/null 2>&1 \
   && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "")
  GIT_REF=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
fi

# Build the canonical manifest with jq (no shell-quoting hazards).
# Structurally identical to gitlab-ci.satsignal.yml — only the value
# sources differ (a buildx metadata file + a registry probe instead of
# CI predefined vars). Empty identity/claims values are dropped so the
# manifest stays minimal and re-canonicalizes identically across reruns.
jq -n \
  --arg repo    "$IMAGE_NAME" \
  --arg stype   "$SATSIGNAL_SUBJECT_TYPE" \
  --arg digest  "$IMAGE_DIGEST" \
  --arg commit  "$GIT_COMMIT" \
  --arg ref     "$GIT_REF" \
  --arg bkref   "$BUILDKIT_REF" \
  --arg pin     "$( [ -n "$IMAGE_NAME" ] && printf '%s@%s' "$IMAGE_NAME" "$IMAGE_DIGEST" || printf '' )" \
  --argjson atts "$ATTS_JSON" \
  '
  def norm($d): if ($d|startswith("sha256:")) then $d else "sha256:"+$d end;
  {
    schema: "satsignal.provenance.v1",
    source:  { type: "docker", id: $repo },
    subject: { type: $stype, digest: norm($digest) },
    identity: ( {
        provider: "docker", repo: $repo,
        commit: $commit, ref: $ref, buildkit_ref: $bkref
      } | with_entries(select(.value != "")) ),
    claims: ( { image: $pin } | with_entries(select(.value != "")) ),
    privacy: { onchain_mode: "hash_only" }
  }
  + ( if ($atts|length) == 0 then {}
      else { attestations: [ $atts[] | { type: .type, digest: norm(.digest) } ] }
      end )
  ' > satsignal-manifest.json

jq -n --slurpfile m satsignal-manifest.json \
  --arg matter "$SATSIGNAL_FOLDER" \
  --arg label  "docker buildx ${IMAGE_NAME:-image} ${IMAGE_DIGEST}" \
  '{ folder_slug: $matter, label: $label, manifest: $m[0] }' > satsignal-body.json

echo "POST ${SATSIGNAL_API_BASE}/api/v1/provenance/anchor (folder=${SATSIGNAL_FOLDER})"
code=$(curl -sS -o satsignal-proof.json -w '%{http_code}' \
  -X POST \
  -H "Authorization: Bearer ${SATSIGNAL_API_KEY}" \
  -H 'Content-Type: application/json' \
  --data @satsignal-body.json \
  "${SATSIGNAL_API_BASE}/api/v1/provenance/anchor")
if [ "$code" != "200" ]; then
  echo "anchor failed (HTTP $code):" >&2; cat satsignal-proof.json >&2; exit 1
fi

TXID=$(jq -r '.txid' satsignal-proof.json)
MH=$(jq -r '.manifest_hash' satsignal-proof.json)
BURL=$(jq -r '.bundle_url // empty' satsignal-proof.json); cp -f satsignal-proof.json satsignal-receipt.json  # legacy filename kept for downstream globs
echo "anchored: txid=$TXID manifest_hash=$MH"

# Pull the proof bundle so the receipt is self-contained (verify
# offline per /spec-provenance §5 — no Satsignal call at verify time).
if [ -n "$BURL" ]; then
  SHORT=$(printf '%s' "$IMAGE_DIGEST" | sed 's/^sha256://' | cut -c1-12)
  curl -sS -H "Authorization: Bearer ${SATSIGNAL_API_KEY}" \
    -o "satsignal-${SHORT:-proof}.mbnt" "$BURL" \
    && echo "bundle saved: satsignal-${SHORT:-proof}.mbnt"
fi
