{
"$id": "https://cmn.dev/schemas/v1/cmn.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CMN Domain Entry Point",
"type": "object",
"required": ["$schema", "capsules", "capsule_signature"],
"properties": {
"$schema": {
"const": "https://cmn.dev/schemas/v1/cmn.json"
},
"capsules": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/capsule_entry" },
"description": "First entry is this host domain's own capsule; additional entries MAY describe mirrored origin domains."
},
"capsule_signature": {
"$ref": "#/$defs/signature",
"description": "Signature over the JCS-canonical capsules array, verified with capsules[0].key"
}
},
"additionalProperties": false,
"$defs": {
"capsule_entry": {
"type": "object",
"required": ["uri", "serial", "key", "history", "endpoints"],
"properties": {
"uri": {
"type": "string",
"pattern": "^cmn://(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
"description": "Domain URI of the capsule origin: cmn://{origin_domain}"
},
"serial": {
"type": "integer",
"minimum": 1,
"description": "Monotonically increasing domain-state serial for anti-rollback pinning."
},
"key": {
"type": "string",
"pattern": "^ed25519\\.[1-9A-HJ-NP-Za-km-z]+$",
"description": "Current Ed25519 public key of the entry's origin domain in {algorithm}.{base58} format"
},
"history": {
"type": "array",
"description": "Historical keys for rotation/revocation. Retired entries require an old-key signature authorizing replaced_by.",
"items": { "$ref": "#/$defs/key_history_entry" }
},
"endpoints": {
"$ref": "#/$defs/endpoints",
"description": "Typed array of endpoint definitions. Taste-only domains provide only a taste endpoint."
}
},
"additionalProperties": false
},
"domain": {
"type": "string",
"pattern": "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
"description": "Lowercase RFC1123 fully-qualified domain name"
},
"key_history_entry": {
"type": "object",
"required": ["key", "status", "retired_at_epoch_ms"],
"properties": {
"key": {
"type": "string",
"pattern": "^ed25519\\.[1-9A-HJ-NP-Za-km-z]+$",
"description": "Historical Ed25519 public key in {algorithm}.{base58} format"
},
"status": {
"type": "string",
"enum": ["retired", "revoked"],
"description": "retired = normal rotation with proof; revoked = compromised key"
},
"retired_at_epoch_ms": {
"type": "integer",
"minimum": 0,
"description": "When this key left active use (milliseconds since Unix epoch)"
},
"replaced_by": {
"type": "string",
"pattern": "^ed25519\\.[1-9A-HJ-NP-Za-km-z]+$",
"description": "Successor key authorized by this historical key"
},
"effective_serial": {
"type": "integer",
"minimum": 1,
"description": "Domain-state serial at which this key rotation became effective. If omitted, verifiers use the containing capsule serial."
},
"rotation_signature": {
"$ref": "#/$defs/signature",
"description": "Signature by key over the cmn-key-rotation-v1 statement"
},
"revoked_at_epoch_ms": {
"type": "integer",
"minimum": 0,
"description": "Required when status=revoked; signatures produced at or after this time must be rejected"
}
},
"allOf": [
{
"if": {
"properties": { "status": { "const": "retired" } },
"required": ["status"]
},
"then": { "required": ["replaced_by", "rotation_signature"] }
},
{
"if": {
"properties": { "status": { "const": "revoked" } },
"required": ["status"]
},
"then": { "required": ["revoked_at_epoch_ms"] }
}
],
"additionalProperties": false
},
"content_hash": {
"type": "string",
"pattern": "^[a-z0-9]+\\.[1-9A-HJ-NP-Za-km-z]+$",
"description": "Content hash with algorithm prefix (e.g., b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2)"
},
"signature": {
"type": "string",
"pattern": "^[a-z0-9]+\\.[1-9A-HJ-NP-Za-km-z]+$",
"description": "Signature format: {algorithm}.{base58}, e.g., ed25519.3yMR7vZQ9hL2xKJd..."
},
"archive_format": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z0-9][a-z0-9+._-]*$",
"description": "Archive transport format identifier. Clients MUST select decoders by this field, not by filename suffix."
},
"endpoint_type": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z0-9][a-z0-9._-]*$",
"description": "Endpoint type identifier (e.g., mycelium, spore, archive, taste)"
},
"endpoint_mycelium": {
"type": "object",
"required": ["type", "url", "hash"],
"properties": {
"type": { "const": "mycelium" },
"url": {
"type": "string",
"pattern": "\\{hash\\}",
"description": "Template URL for fetching mycelium, e.g. https://example.com/cmn/mycelium/{hash}.json"
},
"hash": {
"$ref": "#/$defs/content_hash",
"description": "Content hash of the primary mycelium manifest. Authoritative source for domain metadata (name, bio, nutrients) and featured spores. Used for change detection and display."
},
"hashes": {
"type": "array",
"items": { "$ref": "#/$defs/content_hash" },
"description": "Optional additional mycelium shard hashes for large domains. Each points to an overflow shard whose spore list is merged for search and indexing. Metadata from these shards is ignored — only the primary hash is authoritative."
}
},
"additionalProperties": false
},
"endpoint_spore": {
"type": "object",
"required": ["type", "url"],
"properties": {
"type": { "const": "spore" },
"url": {
"type": "string",
"pattern": "\\{hash\\}",
"description": "Template URL for fetching spores, e.g. https://example.com/cmn/spore/{hash}.json"
}
},
"additionalProperties": false
},
"endpoint_archive": {
"type": "object",
"required": ["type", "url", "format"],
"properties": {
"type": { "const": "archive" },
"url": {
"type": "string",
"pattern": "\\{hash\\}",
"description": "Template URL for archives. MUST include {hash}, e.g. https://example.com/cmn/archive/{hash}.tar.zst"
},
"format": {
"$ref": "#/$defs/archive_format",
"description": "Archive format identifier, e.g. tar+zstd or tar+gzip"
},
"delta_url": {
"type": "string",
"pattern": "(?=.*\\{hash\\})(?=.*\\{old_hash\\})",
"description": "Optional template URL for archive deltas. MUST include {hash} (target) and {old_hash} (cached base). Direction is old_hash -> hash."
}
},
"additionalProperties": false
},
"endpoint_taste": {
"type": "object",
"required": ["type", "url"],
"properties": {
"type": { "const": "taste" },
"url": {
"type": "string",
"pattern": "\\{hash\\}",
"description": "Template URL for fetching taste reports, e.g. https://example.com/cmn/taste/{hash}.json"
}
},
"additionalProperties": false
},
"endpoint_extension": {
"type": "object",
"required": ["type", "url"],
"properties": {
"type": {
"allOf": [
{ "$ref": "#/$defs/endpoint_type" },
{ "not": { "enum": ["mycelium", "spore", "archive", "taste"] } }
],
"description": "Extension endpoint type identifier"
},
"url": {
"type": "string",
"minLength": 1,
"description": "Template URL for this endpoint type"
}
},
"additionalProperties": true
},
"endpoint_entry": {
"allOf": [
{ "not": { "required": ["protocol_version"] } },
{
"oneOf": [
{ "$ref": "#/$defs/endpoint_mycelium" },
{ "$ref": "#/$defs/endpoint_spore" },
{ "$ref": "#/$defs/endpoint_archive" },
{ "$ref": "#/$defs/endpoint_taste" },
{ "$ref": "#/$defs/endpoint_extension" }
]
}
]
},
"endpoints": {
"type": "array",
"items": { "$ref": "#/$defs/endpoint_entry" },
"description": "Typed array of endpoint definitions. Each entry has a 'type' field. Multiple archive entries with different formats are allowed. Clients SHOULD try entries in listed order."
}
}
}