{
"id": "social.smallbury.actor.trust",
"defs": {
"main": {
"key": "literal:self",
"type": "record",
"record": {
"type": "object",
"required": [
"trusted",
"keyEpochs"
],
"properties": {
"trusted": {
"type": "array",
"items": {
"ref": "#trustedEntry",
"type": "ref"
},
"description": "DIDs this actor trusts. Cleartext for AppView; does not by itself reveal per-post subset audiences. Listing a DID here may occur before they reciprocate (pre-emptive trust)."
},
"createdAt": {
"type": "string",
"format": "datetime"
},
"keyEpochs": {
"type": "array",
"items": {
"ref": "#keyEpoch",
"type": "ref"
},
"description": "Ordered content-key epochs. Only the latest epoch (no endedAt) is used for new posts and comments. Earlier epochs remain for decrypting historical content."
},
"updatedAt": {
"type": "string",
"format": "datetime"
},
"pendingPairwiseSecrets": {
"type": "array",
"items": {
"ref": "#pendingPairwiseSecret",
"type": "ref"
},
"description": "Temporary delivery of a pairwise trust secret to a peer during establishment (encrypted to the peer's profile encryptionPublicKey). Removed after the peer has retrieved and acknowledged. Used when the acceptor generates the secret (new-user path) or when the initiator pre-delivers it (existing-user path)."
}
}
},
"description": "Contacts this actor trusts, content-key epochs for E2EE, and optional in-flight pairwise-secret delivery during trust establishment. Trusted DIDs are cleartext for AppView indexing. Mutual trust is derived: A trusts B and B trusts A (each side's trusted array). Feed visibility requires mutual trust on both records. Per-epoch content keys use opaque slots (see #contentKeyWrap)."
},
"keyEpoch": {
"type": "object",
"required": [
"keyEpochId",
"startedAt",
"contentKeyWraps"
],
"properties": {
"endedAt": {
"type": "string",
"format": "datetime",
"description": "Set when a newer epoch is created. Omitted on the current epoch."
},
"startedAt": {
"type": "string",
"format": "datetime"
},
"keyEpochId": {
"type": "string",
"maxLength": 64,
"minLength": 1,
"description": "Stable identifier for this epoch (e.g. sequential \"0\", \"1\", …). Referenced by encrypted posts and comments."
},
"contentKeyWraps": {
"type": "array",
"items": {
"ref": "#contentKeyWrap",
"type": "ref"
},
"description": "One wrapped copy of this epoch's symmetric content key per intended recipient. Opaque slots so wraps are not labeled with recipient DIDs. May include dummy wraps."
}
}
},
"trustedEntry": {
"type": "object",
"required": [
"did",
"since"
],
"properties": {
"did": {
"type": "string",
"format": "did"
},
"since": {
"type": "string",
"format": "datetime",
"description": "When this actor added the contact to trusted."
}
}
},
"contentKeyWrap": {
"type": "object",
"required": [
"slot",
"wrappedKey"
],
"properties": {
"slot": {
"type": "string",
"maxLength": 128,
"minLength": 1,
"description": "Opaque lookup key. Recipients compute slot = Base64URL(HMAC-SHA256(pairwiseTrustSecret, keyEpochId)). pairwiseTrustSecret is agreed at trust establishment and stored only on the two clients—not published on the PDS."
},
"wrappedKey": {
"type": "bytes",
"maxLength": 512,
"description": "This epoch's symmetric content encryption key, encrypted to the recipient's encryptionPublicKey from social.smallbury.actor.profile."
}
}
},
"pendingPairwiseSecret": {
"type": "object",
"required": [
"peerDid",
"wrappedSecret"
],
"properties": {
"peerDid": {
"type": "string",
"format": "did",
"description": "The contact who should decrypt wrappedSecret."
},
"inviteId": {
"type": "string",
"maxLength": 128,
"minLength": 1,
"description": "Optional link to social.smallbury.graph.invite on the initiator's repo."
},
"wrappedSecret": {
"type": "bytes",
"maxLength": 512,
"description": "32-byte pairwiseTrustSecret for this pair, encrypted to peerDid's encryptionPublicKey from social.smallbury.actor.profile."
}
}
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1
}