#!/bin/sh
# satsignal-anchor — npm provenance adapter for satsignal.provenance.v1
# ---------------------------------------------------------------------------
# A sibling of the GitLab CI, Bitbucket, and Docker BuildKit 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 for a
# published npm package and POSTs it to Satsignal, which commits its
# SHA-256 on-chain and returns a portable .mbnt bundle. POSIX-sh only
# (sha256sum + curl + jq) — same manifest shape as the GitLab/Bitbucket
# adapters and the GitHub Action, no SDK, no Satsignal account at
# verify time.
#
# npm provenance (`npm publish --provenance`) is a registry-publish
# concern, not a CI-platform pipeline — it has no `include: remote:`
# and the attestations live in the npm registry, not the pipeline. So
# this is a standalone script (like docker-buildx.satsignal.sh), run
# after the package is published. The subject is the published tarball
# (what `npm install` actually fetches); the SLSA + npm-publish
# attestations the registry serves are anchored as a reference —
# Satsignal timestamps the attestation set, it does not re-issue it
# (the "plug into Sigstore/SLSA, don't compete" posture).
#
# USAGE — after `npm publish --provenance`:
#
#     curl -fsSL https://satsignal.cloud/npm-provenance.satsignal.sh | sh
#     # run from the package dir (reads ./package.json), or pin the spec:
#     SATSIGNAL_NPM_SPEC='@scope/pkg@1.2.3' \
#       curl -fsSL https://satsignal.cloud/npm-provenance.satsignal.sh | sh
#     # or anchor a local pack without publishing (subject = that tarball):
#     ./npm-provenance.satsignal.sh ./pkg-1.2.3.tgz
#
# Package resolution (in order): SATSIGNAL_NPM_SPEC, then ./package.json
# (`.name` + `.version`). An optional positional arg is a local .tgz to
# hash as the subject instead of downloading the published tarball.
#
# 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_NPM_SPEC    name@version (supports @scope/pkg@version)
#     SATSIGNAL_NPM_REGISTRY  default https://registry.npmjs.org
#     SATSIGNAL_SUBJECT_TYPE  subject.type override (default "package")
#     SATSIGNAL_API_BASE      default https://app.satsignal.cloud
#     SATSIGNAL_NO_REGISTRY   set non-empty to skip the registry probe
#                             (requires a local .tgz positional arg;
#                             anchors the tarball alone, no attestation)
#
# Attestation reference (best-effort, never fatal): this GETs
# `<registry>/-/npm/v1/attestations/<name>@<version>` (the document
# the npm CLI itself verifies — npm publish attestation + SLSA
# provenance) and anchors its sha256 as one `npm`-type reference. If
# the package was published without `--provenance`, the endpoint 404s,
# or the registry is unreachable, the script warns and anchors the
# tarball digest alone (still chain-bound). 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

: "${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:-package}"
REG="${SATSIGNAL_NPM_REGISTRY:-https://registry.npmjs.org}"
REG="${REG%/}"
LOCAL_TGZ="${1:-}"

# Resolve name + version: explicit spec wins, else ./package.json. The
# version is the substring after the LAST '@' so scoped names work
# (@scope/pkg@1.2.3 -> name='@scope/pkg' version='1.2.3').
NAME=""
VERSION=""
if [ -n "${SATSIGNAL_NPM_SPEC:-}" ]; then
  VERSION="${SATSIGNAL_NPM_SPEC##*@}"
  NAME="${SATSIGNAL_NPM_SPEC%@*}"
elif [ -f package.json ]; then
  NAME=$(jq -r '.name // ""' package.json)
  VERSION=$(jq -r '.version // ""' package.json)
fi
if [ -z "$NAME" ] || [ -z "$VERSION" ] || [ "$NAME" = "$VERSION" ]; then
  echo "satsignal: could not resolve npm name@version — set" >&2
  echo "  SATSIGNAL_NPM_SPEC='name@version' or run from a package dir" >&2
  exit 1
fi

# Registry metadata (skipped only with SATSIGNAL_NO_REGISTRY + a local
# tarball). dist.{tarball,integrity,shasum} pin the published artifact;
# dist.attestations presence is npm's own signal that the package was
# published with --provenance.
TARBALL_URL=""
INTEGRITY=""
SHASUM=""
HAS_ATT=""
if [ -z "${SATSIGNAL_NO_REGISTRY:-}" ]; then
  # Scoped names must percent-encode the '/' for the metadata path.
  URLNAME=$(printf '%s' "$NAME" | sed 's|/|%2f|')
  if META=$(curl -sS -f "$REG/$URLNAME" 2>/dev/null); then
    DIST=$(printf '%s' "$META" | jq -c --arg v "$VERSION" '.versions[$v].dist // {}')
    TARBALL_URL=$(printf '%s' "$DIST" | jq -r '.tarball // ""')
    INTEGRITY=$(printf '%s' "$DIST" | jq -r '.integrity // ""')
    SHASUM=$(printf '%s' "$DIST" | jq -r '.shasum // ""')
    HAS_ATT=$(printf '%s' "$DIST" | jq -r 'if .attestations then "1" else "" end')
  else
    echo "satsignal: note — registry metadata for $NAME@$VERSION" >&2
    echo "  unreachable; relying on the local tarball if provided." >&2
  fi
fi

# Subject: a local tarball arg wins (anchor exactly that artifact),
# else the published tarball the registry serves to installers.
WORKTGZ=""
if [ -n "$LOCAL_TGZ" ] && [ -f "$LOCAL_TGZ" ]; then
  TGZ_PATH="$LOCAL_TGZ"
elif [ -n "$TARBALL_URL" ]; then
  WORKTGZ=$(mktemp)
  curl -sS -f -o "$WORKTGZ" "$TARBALL_URL" || {
    echo "satsignal: failed to download $TARBALL_URL" >&2
    rm -f "$WORKTGZ"; exit 1; }
  TGZ_PATH="$WORKTGZ"
else
  echo "satsignal: no subject tarball — publish first (so the" >&2
  echo "  registry serves it) or pass a local .tgz as the argument." >&2
  exit 1
fi
DIGEST="sha256:$(sha256sum "$TGZ_PATH" | cut -d' ' -f1)"
[ -n "$WORKTGZ" ] && rm -f "$WORKTGZ"

# Attestation set (best-effort, non-fatal). The npm CLI verifies
# exactly this document; we anchor its sha256 as one reference.
ATTS_JSON='[]'
if [ -z "${SATSIGNAL_NO_REGISTRY:-}" ]; then
  if ATT=$(curl -sS -f "$REG/-/npm/v1/attestations/$NAME@$VERSION" 2>/dev/null) \
     && [ -n "$ATT" ] \
     && [ "$(printf '%s' "$ATT" | jq -r '(.attestations // []) | length')" != "0" ]; then
    ATT_SHA=$(printf '%s' "$ATT" | sha256sum | cut -d' ' -f1)
    ATTS_JSON=$(jq -n --arg d "sha256:$ATT_SHA" '[{type:"npm",digest:$d}]')
  fi
fi
if [ "$(printf '%s' "$ATTS_JSON" | jq 'length')" = "0" ]; then
  echo "satsignal: note — no provenance attestation for $NAME@$VERSION" >&2
  echo "  in the registry; anchoring the tarball digest alone (still" >&2
  echo "  chain-bound). Publish with: npm publish --provenance" >&2
fi

# Best-effort git context (this often runs from the package source
# tree). Empty values are dropped by the jq builder below.
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 / the Docker
# adapter — only the value sources differ (the npm registry instead of
# CI vars / a buildx metadata file). Empty identity/claims values are
# dropped so the manifest stays minimal and re-canonicalizes
# identically across reruns. INTEGRITY (sha512) + SHASUM (sha1) ride in
# claims so an installer can cross-check; subject.digest is the
# schema-required sha256 of the same tarball bytes.
jq -n \
  --arg pkg     "$NAME" \
  --arg ver     "$VERSION" \
  --arg stype   "$SATSIGNAL_SUBJECT_TYPE" \
  --arg digest  "$DIGEST" \
  --arg reg     "$REG" \
  --arg commit  "$GIT_COMMIT" \
  --arg ref     "$GIT_REF" \
  --arg tarball "$TARBALL_URL" \
  --arg integ   "$INTEGRITY" \
  --arg shasum  "$SHASUM" \
  --argjson atts "$ATTS_JSON" \
  '
  def norm($d): if ($d|startswith("sha256:")) then $d else "sha256:"+$d end;
  {
    schema: "satsignal.provenance.v1",
    source:  { type: "npm", id: $pkg },
    subject: { type: $stype, digest: norm($digest) },
    identity: ( {
        provider: "npm", package: $pkg, version: $ver, registry: $reg,
        commit: $commit, ref: $ref
      } | with_entries(select(.value != "")) ),
    claims: ( {
        tarball: $tarball, integrity: $integ, shasum: $shasum
      } | 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  "npm ${NAME}@${VERSION}" \
  '{ 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' "$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
