#!/bin/sh
#
# Conformance check for a GeoVector CMS receiver.
#
#   ./verify-receiver.sh https://example.com <bearer-token>
#   ./verify-receiver.sh https://example.com <bearer-token> --read-only
#
# Exercises the contract in geovector-cms.openapi.yaml against a live endpoint.
# POSIX sh and curl only — a receiver written in PHP, Python, Go or Ruby cannot
# run our TypeScript test suite, and "we tested it in a language you don't use"
# is not evidence your implementation is right.
#
# WHAT IT WRITES
#
# By default this publishes and then deletes one article at the slug
# `geovector-conformance-check`. That round trip is the most valuable part of
# the run — it is the only way to find out whether stored content comes back
# byte-identical, which is what GeoVector's edit detection depends on. Pass
# --read-only to skip every request that writes.
#
# It also attempts to publish a body containing `<script>alert(1)</script>`.
# That request is *supposed* to be refused. If your receiver stores it, the
# check fails loudly and you have found stored XSS on your own domain before
# somebody else did. The script deletes the slug afterwards either way.
#
# Run it against staging first if writing to production makes you uneasy.

set -u

SITE="${1:-}"
TOKEN="${2:-}"
MODE="${3:-}"

if [ -z "$SITE" ] || [ -z "$TOKEN" ]; then
  echo "usage: $0 <site-url> <bearer-token> [--read-only]" >&2
  exit 2
fi

# Trim one trailing slash so "https://x.com/" and "https://x.com" behave alike.
SITE=$(printf '%s' "$SITE" | sed 's:/*$::')
ENDPOINT="$SITE/api/publish"
META="$SITE/api/publish/meta"
SLUG="geovector-conformance-check"

# Category sent on every publish. The spec asks receivers to map an unknown
# category to their default rather than reject it, but a strict one will refuse
# the payload before its content policy is ever reached — which makes the
# interesting checks inconclusive for a reason that has nothing to do with
# safety. Override when that happens: CATEGORY=news ./verify-receiver.sh ...
CATEGORY="${CATEGORY:-guide}"

READ_ONLY=0
[ "$MODE" = "--read-only" ] && READ_ONLY=1

PASS=0
FAIL=0
WARN=0

BODY_FILE=$(mktemp)
trap 'rm -f "$BODY_FILE" "$BODY_FILE.big" "$BODY_FILE.payload" "$BODY_FILE.payload2"' EXIT INT TERM

RED=''
GREEN=''
YELLOW=''
RESET=''
if [ -t 1 ]; then
  RED=$(printf '\033[31m')
  GREEN=$(printf '\033[32m')
  YELLOW=$(printf '\033[33m')
  RESET=$(printf '\033[0m')
fi

pass() {
  PASS=$((PASS + 1))
  printf '%s  PASS%s  %s\n' "$GREEN" "$RESET" "$1"
}

fail() {
  FAIL=$((FAIL + 1))
  printf '%s  FAIL%s  %s\n' "$RED" "$RESET" "$1"
  [ -n "${2:-}" ] && printf '        %s\n' "$2"
}

warn() {
  WARN=$((WARN + 1))
  printf '%s  WARN%s  %s\n' "$YELLOW" "$RESET" "$1"
  [ -n "${2:-}" ] && printf '        %s\n' "$2"
}

section() { printf '\n%s\n' "$1"; }

# Runs a request and leaves the response body in $BODY_FILE. Echoes the status.
# --max-time bounds a hung receiver; GeoVector's own client aborts at 30s.
request() {
  _method="$1"
  _url="$2"
  _auth="$3"
  _data="$4"

  if [ -n "$_auth" ]; then
    set -- -H "Authorization: Bearer $_auth"
  else
    set --
  fi
  if [ -n "$_data" ]; then
    set -- "$@" -H 'Content-Type: application/json' --data-binary "@$_data"
  fi

  curl -s -S -o "$BODY_FILE" -w '%{http_code}' \
    --max-time 45 -X "$_method" "$@" "$_url" 2>/dev/null || echo "000"
}

body_has() { grep -q -- "$1" "$BODY_FILE" 2>/dev/null; }

# Reads a boolean JSON field without depending on jq. Tolerates the whitespace a
# pretty-printing receiver emits — matching the bare word `true` anywhere in the
# body would misread `{"exists":false,"cached":true}`, and matching
# `"exists":true` exactly would misread `{ "exists": true }` as absent, which is
# the direction that turns a broken receiver into a pass.
field_is() {
  grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*$2" "$BODY_FILE" 2>/dev/null
}

# Writes a publish payload to $1 with the content in $2. The content is spliced
# into a JSON string literal, so any double quote in it must arrive already
# written as \" — this builds JSON with printf rather than depending on jq,
# which is not installed everywhere.
write_payload() {
  cat >"$1" <<EOF
{"title":"GeoVector conformance check","slug":"$SLUG","category":"$CATEGORY",
 "date":"2026-01-01","featured":false,"format":"html","content":"$2"}
EOF
}

# Asserts a content-policy probe was refused *by the policy*.
#
# Worth its own helper because the failure mode it guards against is a check
# that passes for the wrong reason: an unescaped quote in a test payload makes
# the JSON malformed, the receiver answers 400 invalid_json, and a bare
# "is it 400?" assertion calls that a pass — reporting a working content policy
# on a receiver that has none.
expect_content_rejected() {
  _status="$1"
  _label="$2"
  _consequence="$3"

  if [ "$_status" != "400" ]; then
    fail "$_label (got $_status)" "$_consequence"
    return
  fi
  if field_is error '"invalid_json"'; then
    fail "$_label" \
      'Rejected as malformed JSON, so the content policy was never reached. This is a bug in this script, not in your receiver — please report it.'
    return
  fi
  if field_is error '"invalid_payload"'; then
    fail "$_label" \
      'Rejected on a field rather than on its content, so the HTML policy was never reached. Most likely the category this script sends is not one you accept.'
    return
  fi
  if field_is error '"unsafe_content"'; then
    pass "$_label"
  else
    pass "$_label"
    warn "Rejection of \"$_label\" is not labelled unsafe_content" \
      'GeoVector shows a clearer message when the error field says so.'
  fi
}

SAFE_CONTENT='<article><h1>GeoVector conformance check</h1><main><p>Safe to delete.</p></main></article>'
UNSAFE_CONTENT='<article><h1>x</h1><main><p>x</p><script>alert(1)</script></main></article>'

printf 'GeoVector CMS receiver conformance\n'
printf 'endpoint: %s\n' "$ENDPOINT"
[ "$READ_ONLY" -eq 1 ] && printf 'mode:     read-only (write checks skipped)\n'

# --------------------------------------------------------------------------
section 'Transport'

case "$SITE" in
  https://*) pass 'Endpoint is HTTPS' ;;
  http://localhost*|http://127.0.0.1*|http://[::1]*)
    # Loopback never leaves the machine, so plain HTTP here is a local
    # development detail rather than a transport weakness.
    warn 'Endpoint is plain HTTP (loopback)' \
      'Fine for a local run. The deployed endpoint must be HTTPS.'
    ;;
  *) fail 'Endpoint is not HTTPS' \
       'The bearer token travels in the clear on every request.' ;;
esac

status=$(request GET "$ENDPOINT?slug=$SLUG" "$TOKEN" '')
if [ "$status" = "000" ]; then
  fail 'Endpoint is reachable' 'No response. Check the URL, DNS and TLS.'
  printf '\nAborting: nothing else can be checked.\n'
  exit 1
fi
pass 'Endpoint is reachable'

# --------------------------------------------------------------------------
section 'Authentication'

status=$(request GET "$ENDPOINT?slug=$SLUG" '' '')
[ "$status" = "401" ] &&
  pass 'GET without a token is 401' ||
  fail "GET without a token is 401 (got $status)" \
    'An unauthenticated read tells an attacker which articles exist.'

status=$(request GET "$ENDPOINT?slug=$SLUG" "wrong-$TOKEN" '')
[ "$status" = "401" ] &&
  pass 'GET with a wrong token is 401' ||
  fail "GET with a wrong token is 401 (got $status)"

if [ "$READ_ONLY" -eq 0 ]; then
  write_payload "$BODY_FILE.payload" "$SAFE_CONTENT"
  status=$(request POST "$ENDPOINT" '' "$BODY_FILE.payload")
  [ "$status" = "401" ] &&
    pass 'POST without a token is 401' ||
    fail "POST without a token is 401 (got $status)" \
      'Anyone on the internet can publish to this site.'

  printf '{"slug":"%s"}' "$SLUG" >"$BODY_FILE.payload"
  status=$(request DELETE "$ENDPOINT" '' "$BODY_FILE.payload")
  [ "$status" = "401" ] &&
    pass 'DELETE without a token is 401' ||
    fail "DELETE without a token is 401 (got $status)" \
      'Anyone on the internet can unpublish this site.'
fi

status=$(request GET "$META" '' '')
[ "$status" = "401" ] &&
  pass 'META without a token is 401' ||
  warn "META without a token is 401 (got $status)" \
    'Metadata is not secret, but answering it unauthenticated confirms the endpoint exists to anyone scanning.'

# --------------------------------------------------------------------------
section 'Slug validation'

status=$(request GET "$ENDPOINT" "$TOKEN" '')
[ "$status" = "400" ] &&
  pass 'GET with no slug is 400 slug_required' ||
  fail "GET with no slug is 400 (got $status)"

status=$(request GET "$ENDPOINT?slug=__verify__" "$TOKEN" '')
if [ "$status" = "200" ] && body_has '"exists"'; then
  if field_is exists true; then
    warn 'GET ?slug=__verify__ reports exists:false' \
      'It answered exists:true. Reserved probe slug — an article should not be stored there.'
  else
    pass 'GET ?slug=__verify__ is 200 exists:false'
  fi
else
  fail "GET ?slug=__verify__ is 200 exists:false (got $status)" \
    'GeoVector probes this reserved slug during setup. A 400 here breaks connecting the site.'
fi

# Traversal. The interesting failure is not the status code but a 5xx, which
# means the value reached the storage layer and something downstream choked.
status=$(request GET "$ENDPOINT?slug=..%2F..%2F..%2Fetc%2Fpasswd" "$TOKEN" '')
case "$status" in
  200)
    if field_is exists true; then
      fail 'GET with a traversal slug does not resolve' \
        'It answered exists:true. The slug reached storage unvalidated.'
    elif field_is exists false; then
      pass 'GET with a traversal slug is 200 exists:false'
    else
      warn 'GET with a traversal slug is 200 exists:false' \
        'The 200 carried no readable exists field, so the answer could not be interpreted.'
    fi
    ;;
  400) pass 'GET with a traversal slug is rejected (400)' ;;
  5*)
    fail "GET with a traversal slug is rejected (got $status)" \
      'A 5xx means the value reached your storage layer. Validate the slug before it gets there.'
    ;;
  *) warn "GET with a traversal slug returned $status" 'Expected 200 exists:false or 400.' ;;
esac

if [ "$READ_ONLY" -eq 0 ]; then
  printf '{"slug":"../../../etc/passwd"}' >"$BODY_FILE.payload"
  status=$(request DELETE "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  [ "$status" = "400" ] &&
    pass 'DELETE with a traversal slug is 400' ||
    fail "DELETE with a traversal slug is 400 (got $status)" \
      'This is the path people forget, and on a filesystem-backed store it deletes arbitrary files.'

  write_payload "$BODY_FILE.payload" "$SAFE_CONTENT"
  sed 's/"slug":"[^"]*"/"slug":"Not A Slug"/' "$BODY_FILE.payload" >"$BODY_FILE.payload2"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload2")
  [ "$status" = "400" ] &&
    pass 'POST with a malformed slug is 400' ||
    fail "POST with a malformed slug is 400 (got $status)"
fi

# --------------------------------------------------------------------------
section 'Request handling'

if [ "$READ_ONLY" -eq 0 ]; then
  printf '{not json' >"$BODY_FILE.payload"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  [ "$status" = "400" ] &&
    pass 'POST with malformed JSON is 400' ||
    fail "POST with malformed JSON is 400 (got $status)"

  printf '{"title":"x"}' >"$BODY_FILE.payload"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  [ "$status" = "400" ] &&
    pass 'POST missing required fields is 400' ||
    fail "POST missing required fields is 400 (got $status)"

  # 11 MB, just past the 10 MB reference default.
  printf '{"title":"x","slug":"%s","category":"%s","date":"2026-01-01","featured":false,"format":"html","content":"' "$SLUG" "$CATEGORY" >"$BODY_FILE.big"
  head -c 11000000 /dev/zero | tr '\0' 'a' >>"$BODY_FILE.big"
  printf '"}' >>"$BODY_FILE.big"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.big")
  case "$status" in
    413) pass 'POST over the size cap is 413' ;;
    400) warn 'POST over the size cap is 413' \
           'Got 400. Rejected, but check the cap fires before you parse the body — otherwise 11 MB was already in memory.' ;;
    *)
      fail "POST over the size cap is 413 (got $status)" \
        'An unbounded body is a memory exhaustion path. If your documented cap is above 11 MB, raise the size in this script and re-run.'
      ;;
  esac

  # A locale-formatted date parses only in JavaScript. A receiver that takes it
  # will keep taking it right up until the sender meets a stricter one, so this
  # is checked against the receiver rather than assumed from the sender.
  printf '{"title":"x","slug":"%s","category":"%s","date":"August 06, 2026","featured":false,"format":"html","content":"%s"}' \
    "$SLUG" "$CATEGORY" "$SAFE_CONTENT" >"$BODY_FILE.payload"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "400" ]; then
    pass 'POST with a non-ISO date is 400'
  else
    warn "POST with a non-ISO date is 400 (got $status)" \
      'The contract specifies ISO-8601 for date and dateModified. Accepting other shapes works until the article has to be parsed by something that is not JavaScript.'
  fi

  # 30 February. Most lenient parsers roll it forward to 2 March instead of
  # failing, which turns a typo into a silently wrong publication date.
  printf '{"title":"x","slug":"%s","category":"%s","date":"2026-02-30","featured":false,"format":"html","content":"%s"}' \
    "$SLUG" "$CATEGORY" "$SAFE_CONTENT" >"$BODY_FILE.payload"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "400" ]; then
    pass 'POST with an impossible date is 400'
  else
    warn "POST with an impossible date is 400 (got $status)" \
      'Check the day exists after the format matches. A parser that accepts 2026-02-30 stores it as 2 March without telling anyone.'
  fi

  # These land in <link> and <meta> attributes the receiver renders, so they get
  # the same scheme rule as href and src.
  printf '{"title":"x","slug":"%s","category":"%s","date":"2026-01-01","featured":false,"format":"html","canonicalUrl":"javascript:alert(1)","content":"%s"}' \
    "$SLUG" "$CATEGORY" "$SAFE_CONTENT" >"$BODY_FILE.payload"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "400" ]; then
    pass 'POST with a javascript: canonicalUrl is 400'
  else
    warn "POST with a javascript: canonicalUrl is 400 (got $status)" \
      'If you render canonicalUrl or ogImageUrl into the page, require an absolute http(s) URL. If you ignore both fields entirely this is harmless.'
  fi

  # The scheme is https and a URL parser accepts this happily — parsers only
  # percent-encode when they serialise. Rendered raw into href="..." it closes
  # the attribute and opens a script tag, so a receiver that checked only the
  # scheme passes the check above and still has stored XSS.
  printf '{"title":"x","slug":"%s","category":"%s","date":"2026-01-01","featured":false,"format":"html","canonicalUrl":"https://x.test/a\\"><script>alert(1)</script>","content":"%s"}' \
    "$SLUG" "$CATEGORY" "$SAFE_CONTENT" >"$BODY_FILE.payload"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "400" ]; then
    pass 'POST with an attribute-breaking canonicalUrl is 400'
  else
    warn "POST with an attribute-breaking canonicalUrl is 400 (got $status)" \
      'Its scheme is https, so a scheme-only check lets it through. Also reject quotes, backticks, angle brackets and whitespace in the raw value. Harmless only if you never render these fields.'
  fi
fi

# --------------------------------------------------------------------------
section 'Content policy'

if [ "$READ_ONLY" -eq 0 ]; then
  write_payload "$BODY_FILE.payload" "$UNSAFE_CONTENT"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  expect_content_rejected "$status" 'POST with a <script> is refused' \
    'YOUR RECEIVER STORED AN EXECUTABLE SCRIPT. It will run on your domain for every visitor. Apply article-html-policy.v1.json before storing content.'

  write_payload "$BODY_FILE.payload" '<article><h1>x</h1><main><p><a href=\"javascript:alert(1)\">x</a></p></main></article>'
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  expect_content_rejected "$status" 'POST with a javascript: URL is refused' \
    'A link with a javascript: href executes on click. Check URL attribute schemes, after entity-decoding the value.'

  write_payload "$BODY_FILE.payload" '<article><h1>x</h1><main><p><img src=\"/a.png\" alt=\"a\" onerror=\"alert(1)\"></p></main></article>'
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  expect_content_rejected "$status" 'POST with an event handler attribute is refused' \
    'onerror fires with no user interaction at all. Reject any attribute whose name starts with on.'

  write_payload "$BODY_FILE.payload" '<article><h1>x</h1><main><p>x</p></main></article><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"FAQPage\"}</script>'
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "200" ]; then
    pass 'POST with FAQ JSON-LD is accepted'
  else
    fail "POST with FAQ JSON-LD is accepted (got $status)" \
      'Articles with an FAQ section carry a trailing ld+json block. Refusing it fails every such publish and drops the structured data the integration exists to deliver.'
  fi
fi

# --------------------------------------------------------------------------
section 'Round trip'

if [ "$READ_ONLY" -eq 0 ]; then
  write_payload "$BODY_FILE.payload" "$SAFE_CONTENT"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "200" ]; then
    pass 'POST of a valid article is 200'
    if body_has '"url"'; then
      pass 'POST response carries the article url'
    else
      fail 'POST response carries the article url' \
        'GeoVector records this URL and later crawls it to attribute AI citations. Without it there is nothing to attribute to.'
    fi
    if field_is action '"(created|updated)"'; then
      pass 'POST response carries action'
    else
      warn 'POST response carries action' 'Expected "action":"created" or "updated".'
    fi
  else
    fail "POST of a valid article is 200 (got $status)" \
      "$(head -c 300 "$BODY_FILE")"
  fi

  # The head-metadata fields are optional and a receiver may ignore every one
  # of them — but it must not reject the article for carrying them. A schema
  # that rejects unknown properties fails here, and it would fail on every
  # field added to the protocol from now on.
  printf '{"title":"GeoVector conformance check","slug":"%s","category":"%s","date":"2026-01-01","featured":false,"format":"html","metaDescription":"A conformance check.","ogImageUrl":"https://example.com/og.png","locale":"en","noindex":false,"content":"%s"}' \
    "$SLUG" "$CATEGORY" "$SAFE_CONTENT" >"$BODY_FILE.payload"
  status=$(request POST "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "200" ]; then
    pass 'POST carrying optional head metadata is accepted'
  else
    fail "POST carrying optional head metadata is accepted (got $status)" \
      'Optional fields must be ignorable, not fatal. Allow unknown properties — every future version of this protocol adds some.'
  fi

  status=$(request GET "$ENDPOINT?slug=$SLUG" "$TOKEN" '')
  if [ "$status" = "200" ] && field_is exists true; then
    pass 'GET after publish reports exists:true'
  else
    fail "GET after publish reports exists:true (got $status)"
  fi

  status=$(request GET "$ENDPOINT?slug=$SLUG&include=content" "$TOKEN" '')
  if [ "$status" = "200" ]; then
    if body_has "$SAFE_CONTENT"; then
      pass 'Stored content comes back unmodified'
    else
      fail 'Stored content comes back unmodified' \
        'What came back is not what was sent. GeoVector hashes the HTML it sent and compares it with this, so a receiver that rewrites content reports every article as edited by hand. Validate and reject; do not sanitize and store.'
    fi
  else
    fail "GET ?include=content is 200 (got $status)" \
      'Without content read-back GeoVector cannot tell an edited article from an untouched one.'
  fi

  printf '{"slug":"%s"}' "$SLUG" >"$BODY_FILE.payload"
  status=$(request DELETE "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "200" ] && field_is action '"deleted"'; then
    pass 'DELETE removes the article'
  else
    fail "DELETE removes the article (got $status)" \
      "The test article is still live at $SLUG. Remove it by hand."
  fi

  status=$(request DELETE "$ENDPOINT" "$TOKEN" "$BODY_FILE.payload")
  if [ "$status" = "200" ] && field_is action '"not_found"'; then
    pass 'DELETE of an absent article is 200 not_found'
  else
    warn "DELETE of an absent article is 200 not_found (got $status)" \
      'GeoVector may unpublish something already gone. Treating that as an error makes retries impossible.'
  fi
fi

# --------------------------------------------------------------------------
section 'Discovery'

status=$(request GET "$META" "$TOKEN" '')
case "$status" in
  200)
    if field_is protocol '"geovector-cms"'; then
      pass 'META advertises the protocol'
    else
      fail 'META advertises the protocol' \
        'Expected "protocol":"geovector-cms". Setup uses this to tell a conforming receiver from an unrelated 200.'
    fi
    body_has '"categories"' &&
      pass 'META advertises categories' ||
      warn 'META advertises categories' 'Without it the setup UI falls back to a free-text category box.'

    # Not advertising capabilities is conforming — every receiver built before
    # the field exists is in that position, and absence means "assume the v1
    # baseline", not "supports nothing". It is worth a nudge because the
    # alternative is the sender discovering each limit at publish time.
    if body_has '"capabilities"'; then
      pass 'META advertises capabilities'
      if field_is imageMimeTypes '\[[[:space:]]*\]'; then
        warn 'Receiver declares it accepts no images' \
          'Conforming and honest. GeoVector does not act on this yet, so illustrations are still attached and still dropped — populate imageMimeTypes once you store them.'
      fi
      if field_is supportsDelete false; then
        warn 'Receiver declares DELETE is unsupported' \
          'Conforming and honest. GeoVector does not act on this yet, so the Unpublish control stays visible and will fail against this receiver.'
      fi
    else
      warn 'META does not advertise capabilities' \
        'Optional. Without it GeoVector cannot tell in advance whether you accept images, which locales you serve, or which optional fields survive — each is discovered when a publish half-works.'
    fi
    ;;
  404)
    warn 'META is not implemented (404)' \
      'Conforming, but the setup UI drops to a free-text category box the user can get wrong.'
    ;;
  *) fail "META is 200 or 404 (got $status)" ;;
esac

# --------------------------------------------------------------------------
printf '\n%d passed, %d failed, %d warnings\n' "$PASS" "$FAIL" "$WARN"

if [ "$FAIL" -gt 0 ]; then
  printf '%sNot conforming.%s Fix the failures above before connecting the site.\n' "$RED" "$RESET"
  exit 1
fi

if [ "$READ_ONLY" -eq 1 ]; then
  # Deliberately not "Conforming." — the content policy, the size cap and the
  # round trip were all skipped, and those are where receivers actually fail.
  printf 'No failures in the read-only checks. The content policy, size cap and\n'
  printf 'round trip were not exercised — re-run without --read-only before going live.\n'
  exit 0
fi

printf '%sConforming.%s\n' "$GREEN" "$RESET"
exit 0
