{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://real-life.org/rltp/v1/schemas/encounter-credential.schema.json",
  "title": "RLTP Encounter Credential (rltp-encounter@0.19)",
  "description": "Normative wire format per RLTP Encounter Layer, section 7.2. WIRE FORM UNCHANGED SINCE 0.19 (the version strings below are the wire versions); the length bounds were added by casting 0.20 section 7.5 and narrow admitted values only. RLTP-owned type (EncounterCredential); contexts pinned by value (exactly two, in order, no additions — meaning by pinning, never JSON-LD processing). Root and credentialSubject CLOSED; validity-controlling VC properties absent by construction. Timestamps pattern-enforced with parse-time calendar checks; keys verified decoded. EVERY PROPERTY IS UPPER-BOUNDED, which is what guarantees the 2048-byte JCS cap the Access Layer 5.3 applies to a transported credential. Two distinct measured numbers (spec 7.5): the SCHEMA maximum, the largest document this schema admits, is 1388 bytes (channel at 64 C0 control characters, 6 bytes each under JCS escaping) and 1068 bytes with channel in UNESCAPED one-byte ASCII — that document is a size construction, not a valid credential, since a 56-character ceremony names no registered ceremony and a 49-character binding decodes to no SHA-256 multihash. The largest document that also PASSES acceptance (spec 5.6) is 1349 bytes, and 1029 with unescaped one-byte ASCII: ceremony encounter-scan@0.19 (19 characters), binding 47 characters. Both are below 2048. Removing any one bound makes a schema-valid credential above 2048 bytes constructible.",
  "type": "object",
  "required": [
    "@context",
    "type",
    "issuer",
    "validFrom",
    "credentialSubject",
    "proof"
  ],
  "additionalProperties": false,
  "properties": {
    "@context": {
      "type": "array",
      "minItems": 2,
      "maxItems": 2,
      "prefixItems": [
        {
          "const": "https://www.w3.org/ns/credentials/v2"
        },
        {
          "const": "https://real-life.org/rltp/v1"
        }
      ],
      "items": false
    },
    "type": {
      "type": "array",
      "description": "Bounded by construction, order-insensitive: exactly two members, and the two 'contains' constraints below name distinct constants, so no single member can satisfy both — both slots are therefore pinned to those constants and the absent maxLength on 'items' costs nothing. Stated because the credential's size guarantee (spec 7.5) requires every property to be bounded.",
      "minItems": 2,
      "maxItems": 2,
      "allOf": [
        {
          "contains": {
            "const": "VerifiableCredential"
          }
        },
        {
          "contains": {
            "const": "EncounterCredential"
          }
        }
      ],
      "items": {
        "type": "string"
      }
    },
    "issuer": {
      "$ref": "#/$defs/didKey"
    },
    "validFrom": {
      "$ref": "#/$defs/rfc3339utc"
    },
    "credentialSubject": {
      "type": "object",
      "required": [
        "id",
        "format",
        "ceremony",
        "challenge",
        "enactmentBinding"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "$ref": "#/$defs/didKey"
        },
        "format": {
          "const": "rltp-encounter-credential/0.19"
        },
        "ceremony": {
          "$ref": "#/$defs/ceremonyId"
        },
        "challenge": {
          "$ref": "#/$defs/challenge"
        },
        "enactmentBinding": {
          "$ref": "#/$defs/multibaseSha256"
        },
        "channel": {
          "type": "string",
          "maxLength": 64
        }
      }
    },
    "proof": {
      "$ref": "#/$defs/dataIntegrityProof"
    }
  },
  "$defs": {
    "didKey": {
      "type": "string",
      "pattern": "^did:key:z6Mk[1-9A-HJ-NP-Za-km-z]{44}$",
      "description": "did:key over an Ed25519 public key; MUST additionally decode to multicodec ed25519-pub + 32 bytes (spec 2.3)"
    },
    "ceremonyId": {
      "type": "string",
      "description": "registered ceremony identifier and version, at most 56 characters: label at most 48, each version part at most 3 digits (spec 7.5). The registered label of this profile is 19 characters; the bound leaves room for future registrations while keeping the field finite, which the credential's size guarantee requires.",
      "maxLength": 56,
      "pattern": "^[a-z0-9-]{1,48}@[0-9]{1,3}\\.[0-9]{1,3}$",
      "examples": [
        "encounter-scan@0.19"
      ]
    },
    "challenge": {
      "type": "string",
      "description": "22-88 base64url characters, at least 128 bits of cryptographically random material; UUID v4 is non-conformant",
      "pattern": "^[A-Za-z0-9_-]{22,88}$"
    },
    "multibaseSha256": {
      "type": "string",
      "pattern": "^(u[A-Za-z0-9_-]{46}|z[1-9A-HJ-NP-Za-km-z]{44,48})$",
      "description": "multibase-encoded multihash (sha2-256: 0x12 0x20 + 32 bytes) over JCS-canonicalized input; emit 'u' (46 chars after header), accept 'u'/'z' per CID 1.0; decoded multihash verified at parse"
    },
    "rfc3339utc": {
      "type": "string",
      "description": "RFC3339 UTC 'Z', seconds 00-59, AT MOST 3 fractional-second digits (spec 2.3) — 24 characters maximum; syntactic gate, calendar validity is a parse-time check. Fractional seconds carry no protocol meaning, and spec 2.3 makes that a rule rather than an assertion: EVERY Encounter comparison — every comparison the Encounter Layer requires, and every comparison a companion document delegates to it, but NOT a companion's own time windows — normalizes EVERY operand, artifact timestamps, stored t_ch, and the local 'now', to whole seconds by truncation toward the past (delete the '.' and everything between it and the 'Z') before comparing, and the section 9 parameters are added afterwards. Because those parameters are whole seconds, truncation can only ever widen acceptance at a boundary, never narrow it. Normalization governs comparison ONLY; the bytes hashed, signed and digested are always the artifact's own, so an implementation that truncates before JCS breaks the proof. Producers SHOULD emit whole seconds (one instant, one serialization, one digest). The length bound itself is byte economy: an unbounded fraction is an unbounded field in a size-capped artifact (7.5).",
      "maxLength": 24,
      "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{1,3})?Z$"
    },
    "dataIntegrityProof": {
      "type": "object",
      "required": [
        "type",
        "cryptosuite",
        "created",
        "verificationMethod",
        "proofPurpose",
        "proofValue"
      ],
      "additionalProperties": false,
      "properties": {
        "type": {
          "const": "DataIntegrityProof"
        },
        "cryptosuite": {
          "const": "eddsa-jcs-2022"
        },
        "created": {
          "$ref": "#/$defs/rfc3339utc"
        },
        "verificationMethod": {
          "type": "string",
          "pattern": "^did:key:z6Mk[1-9A-HJ-NP-Za-km-z]{44}#z6Mk[1-9A-HJ-NP-Za-km-z]{44}$"
        },
        "proofPurpose": {
          "const": "assertionMethod"
        },
        "proofValue": {
          "type": "string",
          "description": "multibase base58btc of an Ed25519 signature, which is exactly 64 bytes (RFC 8032): 'z' plus 64 to 88 base58 characters, so 65 to 89 characters and nothing else. Upper end: 58^87 <= 2^512-1 < 58^88, so 88 digits suffice and 89 are never needed. Lower end: base58btc renders each leading zero byte as one '1', so the all-zero 64-byte string is the shortest at 64. A value outside this interval is not the encoding of 64 bytes and could never have verified (spec 2.3).",
          "minLength": 65,
          "maxLength": 89,
          "pattern": "^z[1-9A-HJ-NP-Za-km-z]+$"
        }
      }
    }
  }
}
