{
  "protocol": "kinwilder.publisher.v1",
  "version": 1,
  "signature": "Ed25519, RFC 8032; 32-byte public key, 64-byte signature, unpadded canonical base64url",
  "signedBytes": "UTF-8(kinwilder.publisher.v1\\n) followed by canonical JSON payload; the domain ends in one actual LF byte, not a literal backslash-n",
  "canonicalization": "Payload is a flat object with the exact action fields below. Sort ASCII property names lexicographically; no whitespace; integers in base 10; Unicode NFC strings encoded as UTF-8 without ASCII escaping; JSON escape only quote, backslash, LF, TAB (other control/format/surrogate characters forbidden); references are a JSON array of URL strings. No floats, duplicate keys, null, booleans, or unknown fields. Reference client uses Python json.dumps(ensure_ascii=False,sort_keys=True,separators=(',',':'),allow_nan=False).",
  "envelope": [
    "payload",
    "signature",
    "work"
  ],
  "commonPayloadFields": {
    "v": 1,
    "action": "action name",
    "group": "directory group id",
    "key": "base64url public key",
    "ts": "integer UTC Unix seconds, within 300 seconds",
    "nonce": "base64url 16 random bytes; unique per key"
  },
  "actions": {
    "enroll": {
      "endpoint": "POST /api/v1/enrollments",
      "additionalFields": []
    },
    "proof": {
      "endpoint": "POST /api/v1/proofs",
      "additionalFields": [
        "enrollment",
        "proofUrl"
      ]
    },
    "statement": {
      "endpoint": "POST /api/v1/statements",
      "additionalFields": [
        "title",
        "body",
        "references",
        "license"
      ],
      "license": "CC-BY-4.0 attribution to group pseudonym; 8–180 character title, 40–9000 character body, at most 8 reference URLs"
    },
    "rotate": {
      "endpoint": "POST /api/v1/rotate",
      "additionalFields": [
        "newKey"
      ],
      "additionalEnvelopeField": "newSignature: successor signs the same domain-separated payload; predecessor signature is also required"
    },
    "revoke": {
      "endpoint": "POST /api/v1/revoke",
      "additionalFields": [
        "reason"
      ],
      "reason": "8–240 plain text characters"
    }
  },
  "work": {
    "challenge": "GET /api/v1/challenge",
    "submit": {
      "token": "opaque returned token",
      "counter": "canonical decimal string, no leading zeroes"
    },
    "calculation": "SHA-256(UTF-8(token + ':' + counter)) must have the returned number of leading zero bits (currently 18). Tokens expire after 15 minutes and are single-use. GET a fresh token per write; do work before setting the signed timestamp."
  },
  "enrollment": "An editor must independently establish a first-party channel using at least two evidence domains, then inspect the exact fresh enrollment challenge posted at proofUrl on that channel. Choosing a group name, uploading a logo, possessing a new key, or repeating historical claims is not enrollment. Channel takeover/seizure and stolen keys remain possible; continuity is dated, scoped and revocable.",
  "publication": "All statements begin pending. SSH-only editors publish with original commentary and source/context links. Public statement endpoints expose signed payloads for verification, labelled authenticated publisher claims. A signature does not establish factual truth or a person's identity.",
  "privacy": "No email, legal name, reader account or application IP logging. Per-key quotas and proof of work replace IP quotas. HTTPS hides content in transit but the network edge can observe connections. Use the published onion address through Tor for stronger network separation. No absolute anonymity guarantee.",
  "limits": "16 KiB JSON request; no attachments, media, HTML, encoded files, credentials, stolen data or victim records. References are never fetched by the intake service. 20 accepted writes per key per rolling 24 hours, 240 authenticated publisher writes per hour, with a separate 60/hour anonymous enrollment/proof quota; fresh enrollments capped at 500; pending statements capped at 1000. Nonpublished pending/held/rejected statements eligible for pruning 30 days after receipt; nonapproved enrollments 30 days after expiry (enrollment challenge lasts 7 days); receipts after 7 days and used-work records after 1 day. Pruning is periodic. Fourteen daily private local backups; public statements, key history and audit retained.",
  "recovery": "Revocation holds pending statements. Rotation requires old and new key signatures. Lost/compromised key recovery requires a fresh editorial channel-continuity review; no name-based password reset. Editors can revoke by SSH. Previous public claims retain their signature and key history.",
  "submissionGuide": "https://kinwilder.com/submit/faq",
  "operatorNotifications": {
    "recipient": "root@kinwilder.com",
    "provider": "Zoho Mail, Canada account region",
    "actions": [
      "enroll",
      "proof",
      "statement",
      "rotate",
      "revoke"
    ],
    "schedule": "Checked about every five minutes; same-kind requests may be grouped, delivery may be delayed. Mail-provider acceptance is not proof of inbox delivery.",
    "included": [
      "action type",
      "opaque queue references",
      "receipt times"
    ],
    "excluded": [
      "submitted body",
      "group name",
      "keys",
      "proof/reference URLs",
      "client IP",
      "attachments"
    ],
    "rejectedTraffic": "Invalid, unsigned and rate-limited requests do not generate individual emails.",
    "ordinaryEmail": "Direct editorial email is not an authenticated publisher submission or anonymous source mailbox.",
    "retention": "Delivered server notification metadata is pruned after fourteen days; pending metadata is kept for retry and operator follow-up. Zoho mailbox retention is separate."
  }
}
