{
  "policy": "geovector-cms-article-html",
  "version": 1,
  "summary": "Allowlist and structural rules a GeoVector CMS receiver applies to the `content` field of POST /api/publish. Markup outside this document is answered with 400 { error: \"unsafe_content\", violations } and never stored.",
  "globalAttributes": [
    "class",
    "dir",
    "id",
    "lang",
    "title"
  ],
  "elements": {
    "a": [
      "href",
      "hreflang",
      "rel"
    ],
    "abbr": [],
    "article": [],
    "aside": [],
    "b": [],
    "blockquote": [
      "cite"
    ],
    "br": [],
    "caption": [],
    "cite": [],
    "code": [],
    "col": [
      "span"
    ],
    "colgroup": [
      "span"
    ],
    "dd": [],
    "details": [
      "open"
    ],
    "div": [],
    "dl": [],
    "dt": [],
    "em": [],
    "figcaption": [],
    "figure": [],
    "footer": [],
    "h1": [],
    "h2": [],
    "h3": [],
    "h4": [],
    "h5": [],
    "h6": [],
    "header": [],
    "hr": [],
    "i": [],
    "img": [
      "alt",
      "decoding",
      "height",
      "loading",
      "src",
      "width"
    ],
    "kbd": [],
    "li": [
      "value"
    ],
    "main": [],
    "mark": [],
    "ol": [
      "reversed",
      "start",
      "type"
    ],
    "p": [],
    "pre": [],
    "q": [
      "cite"
    ],
    "s": [],
    "samp": [],
    "section": [],
    "small": [],
    "span": [],
    "strong": [],
    "sub": [],
    "summary": [],
    "sup": [],
    "table": [],
    "tbody": [],
    "td": [
      "colspan",
      "headers",
      "rowspan"
    ],
    "tfoot": [],
    "th": [
      "abbr",
      "colspan",
      "headers",
      "rowspan",
      "scope"
    ],
    "thead": [],
    "time": [
      "datetime"
    ],
    "tr": [],
    "u": [],
    "ul": [],
    "var": [],
    "wbr": []
  },
  "voidElements": [
    "br",
    "col",
    "hr",
    "img",
    "wbr"
  ],
  "urlAttributes": [
    "cite",
    "href",
    "src"
  ],
  "url": {
    "safeSchemes": [
      "http",
      "https",
      "mailto"
    ],
    "relativeAllowed": true,
    "strippedCodePointRanges": [
      [
        0,
        32
      ],
      [
        127,
        127
      ]
    ],
    "entityDecoding": {
      "passes": 1,
      "semicolonOptional": true,
      "decimal": true,
      "hex": true,
      "named": {
        "amp": "&",
        "lt": "<",
        "gt": ">",
        "quot": "\"",
        "apos": "'",
        "colon": ":",
        "sol": "/",
        "period": ".",
        "tab": "\t",
        "newline": "\n"
      }
    }
  },
  "jsonLd": {
    "type": "application/ld+json",
    "additionalAttributes": [
      "id"
    ],
    "bodyForbiddenCharacter": "<",
    "bodyMustParseAsJson": true
  },
  "structure": {
    "attributeValuesMustBeQuoted": true,
    "commentsAllowed": false,
    "doctypesAllowed": false,
    "processingInstructionsAllowed": false,
    "bareLessThanAllowed": false,
    "rewritesContent": false,
    "tagNamesAreCaseInsensitive": true
  },
  "rules": [
    {
      "id": "disallowed-element",
      "description": "An element outside the allowlist, or a <script> that is not the permitted JSON-LD form."
    },
    {
      "id": "disallowed-attribute",
      "description": "An attribute not in globalAttributes and not listed for this element. Inline `style` is always reported here."
    },
    {
      "id": "event-handler-attribute",
      "description": "An attribute whose name starts with `on`. Rejected before the allowlist is consulted, so widening the allowlist can never admit one."
    },
    {
      "id": "unsafe-url",
      "description": "A URL attribute whose scheme, after entity decoding and control-character stripping, is not in url.safeSchemes."
    },
    {
      "id": "unquoted-attribute-value",
      "description": "An attribute value not wrapped in single or double quotes. Unquoted values are where a hand-written tokenizer and a browser most easily disagree."
    },
    {
      "id": "malformed-markup",
      "description": "A comment, doctype, processing instruction, bare `<` in text, unterminated tag or attribute, or an end tag for a void element."
    },
    {
      "id": "invalid-json-ld",
      "description": "JSON-LD script content containing `<`, or content that does not parse as JSON."
    }
  ],
  "limits": {
    "maxReportedViolations": 20
  },
  "notes": [
    "The receiver validates and never rewrites. Accepted content is stored byte-identical, which is what keeps the publisher’s content hash — and therefore drift detection — meaningful. A receiver that silently strips markup reports every article as drifted.",
    "Reject on the first violation. The list exists so a publisher fixing a template sees more than one problem per round trip, not so a receiver can store a partially acceptable body.",
    "Be stricter than an HTML parser, not equally strict. Every `<` must begin a well-formed tag from the allowlist. Everything our renderer emits satisfies that, because it escapes text — so there is no construct a browser reads as a tag that a conforming scanner reads as inert text.",
    "Text between tags needs no inspection. Entities in text decode to characters, never to markup.",
    "Compare the bearer token in constant time over fixed-length digests: hash_equals (PHP), hmac.compare_digest (Python), subtle.ConstantTimeCompare (Go), OpenSSL.secure_compare (Ruby), crypto.timingSafeEqual (Node)."
  ]
}
